diff --git a/src/ServiceControl.AcceptanceTesting/ScenarioContext.cs b/src/ServiceControl.AcceptanceTesting/ScenarioContext.cs index 66b75be060..bef6364e19 100644 --- a/src/ServiceControl.AcceptanceTesting/ScenarioContext.cs +++ b/src/ServiceControl.AcceptanceTesting/ScenarioContext.cs @@ -1,6 +1,7 @@ namespace NServiceBus.AcceptanceTesting { using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Activation; @@ -66,13 +67,15 @@ public IMessage SyncProcessMessage(IMessage msg) public bool HasNativePubSubSupport { get; set; } - public string Trace { get; set; } + public string Trace { get { return string.Join(Environment.NewLine, traceQueue.ToArray()); } } public void AddTrace(string trace) { - Trace += DateTime.Now.ToString("HH:mm:ss.ffffff") + " - " + trace + Environment.NewLine; + traceQueue.Enqueue(String.Format("{0:HH:mm:ss.ffffff} - {1}", DateTime.Now, trace)); } + ConcurrentQueue traceQueue = new ConcurrentQueue(); + public void RecordEndpointLog(string endpointName,string level ,string message) { endpointLogs.Add(new EndpointLogItem diff --git a/src/ServiceControl.AcceptanceTesting/Support/EndpointConfiguration.cs b/src/ServiceControl.AcceptanceTesting/Support/EndpointConfiguration.cs index 298cc32fbe..86b77ad7ca 100644 --- a/src/ServiceControl.AcceptanceTesting/Support/EndpointConfiguration.cs +++ b/src/ServiceControl.AcceptanceTesting/Support/EndpointConfiguration.cs @@ -11,6 +11,7 @@ public EndpointConfiguration() TypesToExclude = new List(); TypesToInclude = new List(); GetBus = () => null; + StopBus = null; } public IDictionary EndpointMappings { get; set; } @@ -23,6 +24,8 @@ public EndpointConfiguration() internal Func GetBus { get; set; } + internal Action StopBus { get; set; } + public string EndpointName { get @@ -49,9 +52,10 @@ public string EndpointName public Type AuditEndpoint { get; set; } public bool SendOnly { get; set; } - public void SelfHost(Func getBus) + public void SelfHost(Func getBus, Action stopBus) { GetBus = getBus; + StopBus = stopBus; } string endpointName; diff --git a/src/ServiceControl.AcceptanceTesting/Support/EndpointRunner.cs b/src/ServiceControl.AcceptanceTesting/Support/EndpointRunner.cs index c0ee5d0808..6092f6432b 100644 --- a/src/ServiceControl.AcceptanceTesting/Support/EndpointRunner.cs +++ b/src/ServiceControl.AcceptanceTesting/Support/EndpointRunner.cs @@ -84,15 +84,21 @@ public Result Initialize(RunDescriptor run, EndpointBehavior endpointBehavior, } //we spin around each 5s since the callback mechanism seems to be shaky - await contextChanged.WaitAsync(TimeSpan.FromSeconds(5), stopToken); - - if (stopToken.IsCancellationRequested) + try + { + await contextChanged.WaitAsync(TimeSpan.FromSeconds(5), stopToken); + } + catch (OperationCanceledException) { - break; } foreach (var when in behavior.Whens) { + if (stopToken.IsCancellationRequested) + { + return; + } + if (executedWhens.Contains(when.Id)) { continue; @@ -104,7 +110,7 @@ public Result Initialize(RunDescriptor run, EndpointBehavior endpointBehavior, } } } - }, stopToken).Unwrap(); + }).Unwrap(); } return Result.Success(); } @@ -171,7 +177,14 @@ public Result Stop() } else { - bus.Dispose(); + if (configuration.StopBus != null) + { + configuration.StopBus(); + } + else + { + bus.Dispose(); + } } Cleanup(); diff --git a/src/ServiceControl.AcceptanceTests/Contexts/ManagementEndpointSetup.cs b/src/ServiceControl.AcceptanceTests/Contexts/ManagementEndpointSetup.cs index 52eb7d907f..ffe5053512 100644 --- a/src/ServiceControl.AcceptanceTests/Contexts/ManagementEndpointSetup.cs +++ b/src/ServiceControl.AcceptanceTests/Contexts/ManagementEndpointSetup.cs @@ -40,11 +40,11 @@ public BusConfiguration GetConfiguration(RunDescriptor runDescriptor, EndpointCo configurationBuilderCustomization(builder); - var startableBus = new Bootstrapper(configuration: builder).Bus; + var bootstrapper = new Bootstrapper(configuration: builder); LogManager.Configuration = SetupLogging(endpointConfiguration); - endpointConfiguration.SelfHost(() => startableBus); + endpointConfiguration.SelfHost(() => bootstrapper.Bus, () => bootstrapper.Stop()); return builder; diff --git a/src/ServiceControl.AcceptanceTests/CustomChecks/When_a_periodic_custom_check_fails.cs b/src/ServiceControl.AcceptanceTests/CustomChecks/When_a_periodic_custom_check_fails.cs index 432bce25d3..58cac74791 100644 --- a/src/ServiceControl.AcceptanceTests/CustomChecks/When_a_periodic_custom_check_fails.cs +++ b/src/ServiceControl.AcceptanceTests/CustomChecks/When_a_periodic_custom_check_fails.cs @@ -19,8 +19,10 @@ public class When_a_periodic_custom_check_fails : AcceptanceTest [Test] public void Should_result_in_a_custom_check_failed_event() { - var context = new MyContext(); - + var context = new MyContext + { + SignalrStarted = true + }; EventLogItem entry = null; Scenario.Define(context) @@ -37,12 +39,10 @@ public void Should_result_in_a_custom_check_failed_event() [Test] public void Should_raise_a_signalr_event() { - var context = new MyContext + var context = Scenario.Define(() => new MyContext { SCPort = port - }; - - Scenario.Define(context) + }) .WithEndpoint(c => c.AppConfig(PathToAppConfig)) .WithEndpoint() .WithEndpoint() @@ -51,12 +51,13 @@ public void Should_raise_a_signalr_event() Assert.IsNotNull(context.SignalrData); } - + public class MyContext : ScenarioContext { public bool SignalrEventReceived { get; set; } public string SignalrData { get; set; } public int SCPort { get; set; } + public bool SignalrStarted { get; set; } } public class EndpointThatUsesSignalR : EndpointConfigurationBuilder @@ -81,12 +82,14 @@ public void Start() { connection.JsonSerializer = Newtonsoft.Json.JsonSerializer.Create(SerializationSettingsFactoryForSignalR.CreateDefault()); connection.Received += ConnectionOnReceived; + connection.StateChanged += change => { context.SignalrStarted = change.NewState == ConnectionState.Connected; }; while (true) { try { connection.Start().Wait(); + break; } catch (AggregateException ex) @@ -125,7 +128,6 @@ public void Stop() public class EndpointWithFailingCustomCheck : EndpointConfigurationBuilder { - public EndpointWithFailingCustomCheck() { EndpointSetup(); @@ -133,15 +135,17 @@ public EndpointWithFailingCustomCheck() class FailingCustomCheck : PeriodicCheck { + private readonly MyContext context; bool executed; - public FailingCustomCheck() : base("MyCustomCheckId", "MyCategory", TimeSpan.FromSeconds(5)) + public FailingCustomCheck(MyContext context) : base("MyCustomCheckId", "MyCategory", TimeSpan.FromSeconds(5)) { + this.context = context; } public override CheckResult PerformCheck() { - if (executed) + if (executed && context.SignalrStarted) { return CheckResult.Failed("Some reason"); } @@ -149,7 +153,6 @@ public override CheckResult PerformCheck() executed = true; return CheckResult.Pass; - } } } diff --git a/src/ServiceControl.AcceptanceTests/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs b/src/ServiceControl.AcceptanceTests/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs index 54f2f4a39b..88636595ff 100644 --- a/src/ServiceControl.AcceptanceTests/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs +++ b/src/ServiceControl.AcceptanceTests/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs @@ -21,8 +21,8 @@ public void SubsequentBatchesShouldBeProcessed() var context = new MyContext(); Scenario.Define(context) - .WithEndpoint(ctx => ctx.AppConfig(PathToAppConfig)) - .WithEndpoint(ctx => ctx + .WithEndpoint(cfg => cfg.AppConfig(PathToAppConfig)) + .WithEndpoint(cfg => cfg .When(bus => { while (true) @@ -39,23 +39,13 @@ public void SubsequentBatchesShouldBeProcessed() } bus.SendLocal(new MessageThatWillFail()); - })) - .Done(ctx => - { - if (ctx.IssueRetry) + }) + .When(ctx => { object failure; - if (!TryGet("/api/errors/" + ctx.UniqueMessageId, out failure)) - { - return false; - } - - ctx.IssueRetry = false; - Post(String.Format("/api/errors/{0}/retry", ctx.UniqueMessageId)); - } - - return ctx.Done; - }) + return ctx.IssueRetry && TryGet("/api/errors/" + ctx.UniqueMessageId, out failure); + }, (bus, ctx) => Post(string.Format("/api/errors/{0}/retry", ctx.UniqueMessageId)))) + .Done(ctx => ctx.Done) .Run(TimeSpan.FromMinutes(3)); Assert.IsTrue(context.Done); diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs index 832681f309..3d74e8e799 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs @@ -12,6 +12,7 @@ using ServiceControl.Infrastructure; using ServiceControl.MessageFailures; + [Serializable] public class When_a_group_is_archived : AcceptanceTest { [Test] @@ -25,25 +26,46 @@ public void All_messages_in_group_should_get_archived() Scenario.Define(context) .WithEndpoint(c => c.AppConfig(PathToAppConfig)) .WithEndpoint(b => b.Given(bus => - { - bus.SendLocal(m => m.MessageNumber = 1); - bus.SendLocal(m => m.MessageNumber = 2); - })) - .Done(c => - { - if (c.FirstMessageId == null || c.SecondMessageId == null) - return false; - - if (!c.ArchiveIssued) { - List beforeArchiveGroups; + bus.SendLocal(m => m.MessageNumber = 1); + bus.SendLocal(m => m.MessageNumber = 2); + }) + .When(ctx => + { + if (ctx.ArchiveIssued || ctx.FirstMessageId == null || ctx.SecondMessageId == null) + { + return false; + } + List beforeArchiveGroups; if (!TryGetMany("/api/recoverability/groups/", out beforeArchiveGroups)) return false; - Post(String.Format("/api/recoverability/groups/{0}/errors/archive", beforeArchiveGroups[0].Id)); - c.ArchiveIssued = true; - } + foreach (var group in beforeArchiveGroups) + { + List failedMessages; + if (TryGetMany(string.Format("/api/recoverability/groups/{0}/errors", group.Id), out failedMessages)) + { + if (failedMessages.Count == 2) + { + ctx.GroupId = group.Id; + return true; + } + } + } + + return false; + + }, (bus, ctx) => + { + Post(string.Format("/api/recoverability/groups/{0}/errors/archive", ctx.GroupId)); + ctx.ArchiveIssued = true; + }) + ) + .Done(c => + { + if (c.FirstMessageId == null || c.SecondMessageId == null) + return false; if (!TryGet("/api/errors/" + c.FirstMessageId, out firstFailure, e => e.Status == FailedMessageStatus.Archived)) return false; @@ -170,12 +192,14 @@ public class MyMessage : ICommand public int MessageNumber { get; set; } } + [Serializable] public class MyContext : ScenarioContext { public string FirstMessageId { get; set; } public string SecondMessageId { get; set; } public bool ArchiveIssued { get; set; } public bool RetryIssued { get; set; } + public string GroupId { get; set; } } } } diff --git a/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs b/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs deleted file mode 100644 index 881da9bd3e..0000000000 --- a/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace ServiceControl.UnitTests.Expiration -{ - using System; - using System.Threading; - using NUnit.Framework; - using ServiceControl.Infrastructure; - - [TestFixture] - public class PeriodicExecutorTests - { - [Test] - public void If_execution_takes_longer_than_period_it_triggers_next_execution_immediately_after_previous() - { - var counter = 0; - var failure = false; - var lastEndTime = DateTime.MinValue; - var @event = new ManualResetEventSlim(false); - var delay = TimeSpan.Zero; - var executor = new PeriodicExecutor(ex => - { - delay = DateTime.Now - lastEndTime; - if (lastEndTime != DateTime.MinValue && delay > TimeSpan.FromMilliseconds(100)) - { - @event.Set(); - failure = true; - return; - } - counter++; - Thread.Sleep(2000); - lastEndTime = DateTime.Now; - if (counter == 2) - { - @event.Set(); - } - }, TimeSpan.FromSeconds(1)); - executor.Start(true); - @event.Wait(); - executor.Stop(); - Assert.IsFalse(failure, string.Format("Time between finishing previous execution and starting this longer than {0} ms",delay)); - } - - [Test] - public void If_execution_throws_it_does_not_kill_the_executor() - { - var first = true; - var success = false; - var @event = new ManualResetEventSlim(false); - var executor = new PeriodicExecutor(ex => - { - if (first) - { - first = false; - throw new Exception(); - } - success = true; - @event.Set(); - }, TimeSpan.FromSeconds(1)); - executor.Start(true); - @event.Wait(); - executor.Stop(); - Assert.IsTrue(success); - } - - [Test] - public void Can_shutdown_while_waiting() - { - var @event = new ManualResetEventSlim(false); - var executor = new PeriodicExecutor(ex => @event.Set(), TimeSpan.FromSeconds(10000)); - executor.Start(false); - @event.Wait(); - Thread.Sleep(1000); - executor.Stop(); - Assert.Pass(); - } - } -} diff --git a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj index c30242418c..32fdb4ccf9 100644 --- a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj +++ b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj @@ -105,7 +105,6 @@ - diff --git a/src/ServiceControl/Bootstrapper.cs b/src/ServiceControl/Bootstrapper.cs index 3c4a245f19..ad6178cf09 100644 --- a/src/ServiceControl/Bootstrapper.cs +++ b/src/ServiceControl/Bootstrapper.cs @@ -3,6 +3,7 @@ namespace Particular.ServiceControl using System; using System.Diagnostics; using System.IO; + using System.Linq; using System.Net; using System.ServiceProcess; using Autofac; @@ -12,9 +13,12 @@ namespace Particular.ServiceControl using NLog.Layouts; using NLog.Targets; using NServiceBus; + using NServiceBus.Configuration.AdvanceExtensibility; using NServiceBus.Features; using NServiceBus.Logging; using Particular.ServiceControl.Hosting; + using Raven.Client; + using Raven.Client.Embedded; using ServiceBus.Management.Infrastructure.Settings; using LogLevel = NLog.LogLevel; @@ -23,6 +27,10 @@ public class Bootstrapper public static IContainer Container { get; set; } public IStartableBus Bus { get; private set; } + ShutdownNotifier notifier = new ShutdownNotifier(); + EmbeddableDocumentStore documentStore = new EmbeddableDocumentStore(); + TimeKeeper timeKeeper; + public Bootstrapper(ServiceBase host = null, HostArguments hostArguments = null, BusConfiguration configuration = null) { LogManager.Use(); @@ -34,9 +42,15 @@ public Bootstrapper(ServiceBase host = null, HostArguments hostArguments = null, // .NET default limit is 10. RavenDB in conjunction with transports that use HTTP exceeds that limit. ServicePointManager.DefaultConnectionLimit = Settings.HttpDefaultConnectionLimit; + timeKeeper = new TimeKeeper(); + var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterType().SingleInstance(); + containerBuilder.RegisterInstance(notifier).ExternallyOwned(); + containerBuilder.RegisterInstance(timeKeeper).ExternallyOwned(); containerBuilder.RegisterType().PropertiesAutowired().SingleInstance(); + containerBuilder.RegisterInstance(documentStore).As().ExternallyOwned(); + Container = containerBuilder.Build(); if (configuration == null) @@ -45,6 +59,9 @@ public Bootstrapper(ServiceBase host = null, HostArguments hostArguments = null, configuration.AssembliesToScan(AllAssemblies.Except("ServiceControl.Plugin")); } + // HACK: Yes I know, I am hacking it to pass it to RavenBootstrapper! + configuration.GetSettings().Set("ServiceControl.EmbeddableDocumentStore", documentStore); + // Disable Auditing for the service control endpoint configuration.DisableFeature(); configuration.DisableFeature(); @@ -102,7 +119,7 @@ static bool IsExternalContract(Type t) public void Start() { - var Logger = LogManager.GetLogger(typeof(Bootstrapper)); + var Logger = LogManager.GetLogger(typeof(Bootstrapper)); if (Settings.MaintenanceMode) { Logger.InfoFormat("RavenDB is now accepting requests on {0}", Settings.StorageUrl); @@ -112,17 +129,21 @@ public void Start() } return; } - + Bus.Start(); } public void Stop() { + notifier.Dispose(); Bus.Dispose(); + timeKeeper.Dispose(); + documentStore.Dispose(); } static void ConfigureLogging() { + const long megaByte = 1073741824; if (NLog.LogManager.Configuration != null) { return; @@ -134,13 +155,25 @@ static void ConfigureLogging() var fileTarget = new FileTarget { ArchiveEvery = FileArchivePeriod.Day, - FileName = Path.Combine(Settings.LogPath, "logfile.txt"), - ArchiveFileName = Path.Combine(Settings.LogPath, "log.{#}.txt"), - ArchiveNumbering = ArchiveNumberingMode.Rolling, + FileName = Path.Combine(Settings.LogPath, "logfile.${shortdate}.txt"), + ArchiveFileName = Path.Combine(Settings.LogPath, "logfile.{#}.txt"), + ArchiveNumbering = ArchiveNumberingMode.DateAndSequence, Layout = simpleLayout, MaxArchiveFiles = 14, + ArchiveAboveSize = 30 * megaByte }; + var ravenFileTarget = new FileTarget + { + ArchiveEvery = FileArchivePeriod.Day, + FileName = Path.Combine(Settings.LogPath, "ravenlog.${shortdate}.txt"), + ArchiveFileName = Path.Combine(Settings.LogPath, "ravenlog.{#}.txt"), + ArchiveNumbering = ArchiveNumberingMode.DateAndSequence, + Layout = simpleLayout, + MaxArchiveFiles = 14, + ArchiveAboveSize = 30 * megaByte + }; + var consoleTarget = new ColoredConsoleTarget { Layout = simpleLayout, @@ -149,20 +182,18 @@ static void ConfigureLogging() var nullTarget = new NullTarget(); - nlogConfig.AddTarget("debugger", fileTarget); + // There lines don't appear to be necessary. The rules seem to work without implicitly adding the targets?!? nlogConfig.AddTarget("console", consoleTarget); + nlogConfig.AddTarget("debugger", fileTarget); + nlogConfig.AddTarget("raven", ravenFileTarget); nlogConfig.AddTarget("bitbucket", nullTarget); // Only want to see raven errors - nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Error, fileTarget)); - nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Error, consoleTarget)); + nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", Settings.RavenDBLogLevel, ravenFileTarget)); + nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Error, consoleTarget)); //Noise reduction - Only RavenDB errors on the console nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Debug, nullTarget) { Final = true }); //Will swallow debug and above messages - // Only want to see persistance errors - nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.RavenDB.Persistence.*", LogLevel.Error, fileTarget)); - nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.RavenDB.Persistence.*", LogLevel.Error, consoleTarget)); - nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.RavenDB.Persistence.*", LogLevel.Debug, nullTarget) { Final = true }); //Will swallow debug and above messages - + // Always want to see license logging regardless of default logging level nlogConfig.LoggingRules.Add(new LoggingRule("Particular.ServiceControl.Licensing.*", LogLevel.Info, fileTarget)); nlogConfig.LoggingRules.Add(new LoggingRule("Particular.ServiceControl.Licensing.*", LogLevel.Info, consoleTarget){ Final = true }); @@ -170,11 +201,21 @@ static void ConfigureLogging() // Defaults nlogConfig.LoggingRules.Add(new LoggingRule("*", Settings.LoggingLevel, fileTarget)); nlogConfig.LoggingRules.Add(new LoggingRule("*", LogLevel.Info, consoleTarget)); + + // Remove Console Logging when running as a service + if (!Environment.UserInteractive) + { + foreach (var rule in nlogConfig.LoggingRules.Where(p => p.Targets.Contains(consoleTarget)).ToList()) + { + nlogConfig.LoggingRules.Remove(rule); + } + } NLog.LogManager.Configuration = nlogConfig; var logger = LogManager.GetLogger(typeof(Bootstrapper)); logger.InfoFormat("Logging to {0} with LoggingLevel '{1}'", fileTarget.FileName, Settings.LoggingLevel.Name); + logger.InfoFormat("RavenDB logging to {0} with LoggingLevel '{1}'", ravenFileTarget.FileName, Settings.RavenDBLogLevel.Name); } string DetermineServiceName(ServiceBase host, HostArguments hostArguments) diff --git a/src/ServiceControl/EventLog/Definitions/FailedMessageGroupArchivedDefinition.cs b/src/ServiceControl/EventLog/Definitions/FailedMessageGroupArchivedDefinition.cs index 6d1bfea0e0..e40b186444 100644 --- a/src/ServiceControl/EventLog/Definitions/FailedMessageGroupArchivedDefinition.cs +++ b/src/ServiceControl/EventLog/Definitions/FailedMessageGroupArchivedDefinition.cs @@ -6,9 +6,8 @@ public class FailedMessageGroupArchivedDefinition : EventLogMappingDefinition string.Format("Archived {0} messages from group: {1}", m.MessageIds.Length, m.GroupName)); + Description(m => string.Format("Archived {0} messages from group: {1}", m.MessagesCount, m.GroupName)); RelatesToGroup(m => m.GroupId); - RelatesToMessages(m => m.MessageIds); } } } diff --git a/src/ServiceControl/EventLog/EventLogApiModule.cs b/src/ServiceControl/EventLog/EventLogApiModule.cs index 26a26a7d79..cff8c41548 100644 --- a/src/ServiceControl/EventLog/EventLogApiModule.cs +++ b/src/ServiceControl/EventLog/EventLogApiModule.cs @@ -5,6 +5,7 @@ using Raven.Client; using ServiceBus.Management.Infrastructure.Extensions; using ServiceBus.Management.Infrastructure.Nancy.Modules; + using ServiceControl.Infrastructure.Extensions; public class EventLogApiModule : BaseModule { @@ -16,9 +17,11 @@ public EventLogApiModule() { RavenQueryStatistics stats; var results = session.Query().Statistics(out stats).OrderByDescending(p => p.RaisedAt) - .ToArray(); + .Paging(Request) + .ToArray(); return Negotiate.WithModel(results) + .WithPagingLinksAndTotalCount(stats, Request) .WithEtagAndLastModified(stats); } }; diff --git a/src/ServiceControl/EventLog/GenericAuditHandler.cs b/src/ServiceControl/EventLog/GenericAuditHandler.cs index 5be5588fb9..591db14da2 100644 --- a/src/ServiceControl/EventLog/GenericAuditHandler.cs +++ b/src/ServiceControl/EventLog/GenericAuditHandler.cs @@ -14,6 +14,8 @@ public class GenericAuditHandler : IHandleMessages public IDocumentSession Session { get; set; } public IBus Bus { get; set; } + static string[] EmptyArray = new string[0]; + public void Handle(IEvent message) { //to prevent a infinite loop @@ -38,7 +40,11 @@ public void Handle(IEvent message) m.Description = logItem.Description; m.Id = logItem.Id; m.Category = logItem.Category; - m.RelatedTo = logItem.RelatedTo; + // Yes this is on purpose. + // The reason is because this data is not useful for end users, so for now we just empty it. + // At the moment too much data is being populated in this field, and this has significant down sides to the amount of data we are sending down to ServicePulse (it actually crashes it). + m.RelatedTo = EmptyArray; + }); } } diff --git a/src/ServiceControl/ExternalIntegrations/EventDispatcher.cs b/src/ServiceControl/ExternalIntegrations/EventDispatcher.cs index ceb21f758a..76976cb316 100644 --- a/src/ServiceControl/ExternalIntegrations/EventDispatcher.cs +++ b/src/ServiceControl/ExternalIntegrations/EventDispatcher.cs @@ -31,39 +31,44 @@ protected override void OnStart() void StartDispatcher() { - Task.Factory - .StartNew(() => DispatchEvents(tokenSource.Token), tokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) - .ContinueWith(t => + task = Task.Run(() => + { + try { -// ReSharper disable once PossibleNullReferenceException - t.Exception.Handle(ex => - { - Logger.Error("An exception occurred when dispatching external integration events", ex); - circuitBreaker.Failure(ex); - return true; - }); + DispatchEvents(tokenSource.Token); + } + catch (Exception ex) + { + Logger.Error("An exception occurred when dispatching external integration events", ex); + circuitBreaker.Failure(ex); if (!tokenSource.IsCancellationRequested) { StartDispatcher(); } - }, TaskContinuationOptions.OnlyOnFaulted); + } + }); } protected override void OnStop() { tokenSource.Cancel(); + task.Wait(); + tokenSource.Dispose(); } private void DispatchEvents(CancellationToken token) { while (!token.IsCancellationRequested) { - DispatchEventBatch(token); + if (DispatchEventBatch() && !token.IsCancellationRequested) + { + token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1)); + } } } - void DispatchEventBatch(CancellationToken token) + bool DispatchEventBatch() { using (var session = DocumentStore.OpenSession()) { @@ -74,8 +79,7 @@ void DispatchEventBatch(CancellationToken token) { Logger.Debug("Nothing to dispatch. Waiting..."); } - token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1)); - return; + return true; } var allContexts = awaitingDispatching.Select(r => r.DispatchContext).ToArray(); @@ -121,9 +125,12 @@ void DispatchEventBatch(CancellationToken token) } session.SaveChanges(); } + + return false; } CancellationTokenSource tokenSource; + Task task; RepeatedFailuresOverTimeCircuitBreaker circuitBreaker; static readonly ILog Logger = LogManager.GetLogger(typeof(EventDispatcher)); } diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index b4b5cfe5b2..1c656e3ab0 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -20,8 +20,6 @@ public override void Execute(HostArguments args) using (var service = new Host{ ServiceName = args.ServiceName} ) { - - using (var waitHandle = new ManualResetEvent(false)) { service.OnStopping = () => @@ -32,17 +30,33 @@ public override void Execute(HostArguments args) service.Run(); - Console.CancelKeyPress += (sender, e) => - { - service.OnStopping = () => { }; - e.Cancel = true; - waitHandle.Set(); - }; + var r = new CancelWrapper(waitHandle, service); + Console.CancelKeyPress += r.ConsoleOnCancelKeyPress; Console.WriteLine("Press Ctrl+C to exit"); waitHandle.WaitOne(); } } } + + class CancelWrapper + { + private readonly ManualResetEvent manualReset; + private readonly Host host; + + public CancelWrapper(ManualResetEvent manualReset, Host host) + { + this.manualReset = manualReset; + this.host = host; + } + + public void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + host.OnStopping = () => { }; + e.Cancel = true; + manualReset.Set(); + Console.CancelKeyPress -= ConsoleOnCancelKeyPress; + } + } } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Host.cs b/src/ServiceControl/Hosting/Host.cs index 9029631bd5..24cd58e067 100644 --- a/src/ServiceControl/Hosting/Host.cs +++ b/src/ServiceControl/Hosting/Host.cs @@ -26,7 +26,10 @@ protected override void OnStart(string[] args) protected override void OnStop() { - bootstrapper.Stop(); + if (bootstrapper != null) + { + bootstrapper.Stop(); + } OnStopping(); } diff --git a/src/ServiceControl/Infrastructure/Extensions/QueryableExtensions.cs b/src/ServiceControl/Infrastructure/Extensions/QueryableExtensions.cs index 1ac3cf312d..83dcf51e74 100644 --- a/src/ServiceControl/Infrastructure/Extensions/QueryableExtensions.cs +++ b/src/ServiceControl/Infrastructure/Extensions/QueryableExtensions.cs @@ -183,7 +183,39 @@ public static IDocumentQuery Sort(this IDocumentQuery return source; } - + + public static IOrderedQueryable Paging(this IOrderedQueryable source, Request request) + { + var maxResultsPerPage = 50; + + if (request.Query.per_page.HasValue) + { + maxResultsPerPage = request.Query.per_page; + } + + if (maxResultsPerPage < 1) + { + maxResultsPerPage = 50; + } + + var page = 1; + + if (request.Query.page.HasValue) + { + page = request.Query.page; + } + + if (page < 1) + { + page = 1; + } + + var skipResults = (page - 1) * maxResultsPerPage; + + return (IOrderedQueryable)source.Skip(skipResults) + .Take(maxResultsPerPage); + } + public static IRavenQueryable Paging(this IRavenQueryable source, Request request) { var maxResultsPerPage = 50; diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/ErrorCode.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/ErrorCode.cs new file mode 100644 index 0000000000..efb3e1f678 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/ErrorCode.cs @@ -0,0 +1,13 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + internal enum ErrorCode : uint + { + Success = 0, + + AlreadyExists = 183, + + InsufficientBuffer = 122, + + NoMoreItems = 259, + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApi.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApi.cs new file mode 100644 index 0000000000..3027ca06ba --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApi.cs @@ -0,0 +1,49 @@ +// ReSharper disable StringLiteralTypo +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System; + using System.Runtime.InteropServices; + + internal static class HttpApi + { + [DllImport("httpapi.dll", SetLastError = true)] + internal static extern ErrorCode HttpQueryServiceConfiguration( + IntPtr ServiceIntPtr, + HttpServiceConfigId ConfigId, + IntPtr pInputConfigInfo, + int InputConfigInfoLength, + IntPtr pOutputConfigInfo, + int OutputConfigInfoLength, + // ReSharper disable once OptionalParameterRefOut + [Optional()] + out int pReturnLength, + IntPtr pOverlapped); + + [DllImport("httpapi.dll", SetLastError = true)] + internal static extern ErrorCode HttpSetServiceConfiguration( + IntPtr ServiceIntPtr, + HttpServiceConfigId ConfigId, + IntPtr pConfigInformation, + int ConfigInformationLength, + IntPtr pOverlapped); + + [DllImport("httpapi.dll", SetLastError = true)] + internal static extern ErrorCode HttpDeleteServiceConfiguration( + IntPtr ServiceIntPtr, + HttpServiceConfigId ConfigId, + IntPtr pConfigInformation, + int ConfigInformationLength, + IntPtr pOverlapped); + + [DllImport("httpapi.dll", SetLastError = true)] + internal static extern ErrorCode HttpInitialize( + HttpApiVersion Version, + uint Flags, + IntPtr pReserved); + + [DllImport("httpapi.dll", SetLastError = true)] + internal static extern ErrorCode HttpTerminate( + uint Flags, + IntPtr pReserved); + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiConstants.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiConstants.cs new file mode 100644 index 0000000000..f0606602eb --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiConstants.cs @@ -0,0 +1,15 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + internal static class HttpApiConstants + { + public const uint InitializeConfig = 0x00000002; + + public static HttpApiVersion Version1 + { + get + { + return new HttpApiVersion(1, 0); + } + } + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiVersion.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiVersion.cs new file mode 100644 index 0000000000..8db44726ea --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpApiVersion.cs @@ -0,0 +1,17 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, Pack = 2)] + internal struct HttpApiVersion + { + public ushort Major; + public ushort Minor; + public HttpApiVersion(ushort majorVersion, ushort minorVersion) + { + Major = majorVersion; + Minor = minorVersion; + } + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenParam.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenParam.cs new file mode 100644 index 0000000000..2931eba712 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenParam.cs @@ -0,0 +1,14 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + internal struct HttpServiceConfigIPListenParam + { + + public ushort AddressLength; + public IntPtr Address; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenQuery.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenQuery.cs new file mode 100644 index 0000000000..1cf250cd66 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigIPListenQuery.cs @@ -0,0 +1,14 @@ + +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + internal struct HttpServiceConfigIPListenQuery + { + public int AddressCount; + public IntPtr AddressList; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigId.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigId.cs new file mode 100644 index 0000000000..3a2df02759 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigId.cs @@ -0,0 +1,10 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + internal enum HttpServiceConfigId + { + HttpServiceConfigIPListenList, + HttpServiceConfigSSLCertInfo, + HttpServiceConfigUrlAclInfo, + HttpServiceConfigMax + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigQueryType.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigQueryType.cs new file mode 100644 index 0000000000..3cb3e57a5d --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigQueryType.cs @@ -0,0 +1,9 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + internal enum HttpServiceConfigQueryType + { + HttpServiceConfigQueryExact, + HttpServiceConfigQueryNext, + HttpServiceConfigQueryMax + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslKey.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslKey.cs new file mode 100644 index 0000000000..57afb209f9 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslKey.cs @@ -0,0 +1,15 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + internal struct HttpServiceConfigSslKey + { + /// + /// Pointer to the port for the IP address. + /// + public IntPtr IPPort; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslParam.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslParam.cs new file mode 100644 index 0000000000..77daaaddf7 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslParam.cs @@ -0,0 +1,32 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct HttpServiceConfigSslParam + { + public int SslHashLength; + + public IntPtr SslHash; + + public Guid AppId; + + public string SslCertStoreName; + + public uint DefaultCertCheckMode; + + public int DefaultRevocationFreshnessTime; + + public int DefaultRevocationUrlRetrievalTimeout; + + [MarshalAs(UnmanagedType.LPWStr)] + public string DefaultSslCtlIdentifier; + + [MarshalAs(UnmanagedType.LPWStr)] + public string DefaultSslCtlStoreName; + + public uint DefaultFlags; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslQuery.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslQuery.cs new file mode 100644 index 0000000000..a9fff38d73 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslQuery.cs @@ -0,0 +1,15 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + internal struct HttpServiceConfigSslQuery + { + public HttpServiceConfigQueryType QueryDesc; + + public HttpServiceConfigSslKey KeyDesc; + + public uint Token; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslSet.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslSet.cs new file mode 100644 index 0000000000..0cc1beeed0 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigSslSet.cs @@ -0,0 +1,13 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + internal struct HttpServiceConfigSslSet + { + public HttpServiceConfigSslKey KeyDesc; + + public HttpServiceConfigSslParam ParamDesc; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclKey.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclKey.cs new file mode 100644 index 0000000000..f6aceb8f67 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclKey.cs @@ -0,0 +1,16 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct HttpServiceConfigUrlAclKey + { + [MarshalAs(UnmanagedType.LPWStr)] + public string UrlPrefix; + + public HttpServiceConfigUrlAclKey(string urlPrefix) + { + UrlPrefix = urlPrefix; + } + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclParam.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclParam.cs new file mode 100644 index 0000000000..39de925ccf --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclParam.cs @@ -0,0 +1,16 @@ +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct HttpServiceConfigUrlAclParam + { + [MarshalAs(UnmanagedType.LPWStr)] + public string StringSecurityDescriptor; + + public HttpServiceConfigUrlAclParam(string securityDescriptor) + { + StringSecurityDescriptor = securityDescriptor; + } + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclQuery.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclQuery.cs new file mode 100644 index 0000000000..8f53cdf9ea --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclQuery.cs @@ -0,0 +1,15 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct HttpServiceConfigUrlAclQuery + { + public HttpServiceConfigQueryType QueryDesc; + + public HttpServiceConfigUrlAclKey KeyDesc; + + public uint Token; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclSet.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclSet.cs new file mode 100644 index 0000000000..a1589059b4 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/Api/HttpServiceConfigUrlAclSet.cs @@ -0,0 +1,13 @@ + +namespace ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct HttpServiceConfigUrlAclSet + { + public HttpServiceConfigUrlAclKey KeyDesc; + + public HttpServiceConfigUrlAclParam ParamDesc; + } +} diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAcl/UrlReservation.cs b/src/ServiceControl/Infrastructure/Installers/UrlAcl/UrlReservation.cs new file mode 100644 index 0000000000..2c67309ab0 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Installers/UrlAcl/UrlReservation.cs @@ -0,0 +1,312 @@ +namespace ServiceBus.Management.Infrastructure.Installers +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel; + using System.Linq; + using System.Runtime.InteropServices; + using System.Security.AccessControl; + using System.Security.Principal; + using System.Text.RegularExpressions; + using ServiceBus.Management.Infrastructure.Installers.UrlAcl.Api; + + + public class UrlReservation + { + static Regex urlPattern = new Regex(@"^(?https?)://(?[^:/]+):?(?\d{0,5})/?(?[^:]*)/$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + const int GENERIC_EXECUTE = 536870912; + + public string Url {get; private set; } + + List securityIdentifiers = new List(); + + public UrlReservation(string url, params SecurityIdentifier[] securityIdentifiers) + { + if (!url.EndsWith("/")) + { + throw new ArgumentException("UrlAcl is invalid - it must have a trailing /"); + } + + var matchResults = urlPattern.Match(url); + if (matchResults.Success) + { + HTTPS = (matchResults.Groups["protocol"].Value.Equals("https", StringComparison.OrdinalIgnoreCase)); + HostName = matchResults.Groups["hostname"].Value; + if (string.IsNullOrEmpty(matchResults.Groups["port"].Value)) + { + Port = (HTTPS) ? 443 : 80; + } + else + { + Port = int.Parse(matchResults.Groups["port"].Value); + } + VirtualDirectory = matchResults.Groups["virtual"].Value; + Url = url; + } + else + { + throw new ArgumentException("UrlAcl is invalid"); + } + + if (securityIdentifiers != null) + { + this.securityIdentifiers.AddRange(securityIdentifiers); + } + } + + public ReadOnlyCollection Users + { + get + { + var users = securityIdentifiers.Select(sec => ((NTAccount) sec.Translate(typeof(NTAccount))).Value).ToList(); + return new ReadOnlyCollection(users); + } + } + + public void AddUser(string user) + { + var account = new NTAccount(user); + var sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); + AddSecurityIdentifier(sid); + } + + public void AddSecurityIdentifier(SecurityIdentifier sid) + { + securityIdentifiers.Add(sid); + } + + public void ClearUsers() + { + securityIdentifiers.Clear(); + } + + public void Create() + { + Create(this); + } + + public void Delete() + { + Delete(this); + } + + public int Port { get; private set; } + public string HostName { get; private set; } + public string VirtualDirectory { get; private set; } + public bool HTTPS { get; private set; } + + public static ReadOnlyCollection GetAll() + { + var reservations = new List(); + + var retVal = HttpApi.HttpInitialize(HttpApiConstants.Version1, HttpApiConstants.InitializeConfig, IntPtr.Zero); + + if (retVal == ErrorCode.Success) + { + var inputConfigInfoSet = new HttpServiceConfigUrlAclQuery + { + QueryDesc = HttpServiceConfigQueryType.HttpServiceConfigQueryNext + }; + + var i = 0; + while (retVal == 0) + { + inputConfigInfoSet.Token = (uint)i; + var pInputConfigInfo = Marshal.AllocHGlobal((Marshal.SizeOf(typeof(HttpServiceConfigUrlAclQuery)))); + Marshal.StructureToPtr(inputConfigInfoSet, pInputConfigInfo, false); + + var pOutputConfigInfo = Marshal.AllocHGlobal(0); + var returnLength = 0; + retVal = HttpApi.HttpQueryServiceConfiguration(IntPtr.Zero, + HttpServiceConfigId.HttpServiceConfigUrlAclInfo, + pInputConfigInfo, + Marshal.SizeOf(inputConfigInfoSet), + pOutputConfigInfo, + returnLength, + out returnLength, + IntPtr.Zero); + + if (retVal == ErrorCode.InsufficientBuffer) + { + Marshal.FreeHGlobal(pOutputConfigInfo); + pOutputConfigInfo = Marshal.AllocHGlobal(Convert.ToInt32(returnLength)); + + retVal = HttpApi.HttpQueryServiceConfiguration(IntPtr.Zero, + HttpServiceConfigId.HttpServiceConfigUrlAclInfo, + pInputConfigInfo, + Marshal.SizeOf(inputConfigInfoSet), + pOutputConfigInfo, + returnLength, + out returnLength, + IntPtr.Zero); + } + + if ( retVal == ErrorCode.Success) + { + var outputConfigInfo = (HttpServiceConfigUrlAclSet) Marshal.PtrToStructure(pOutputConfigInfo, typeof(HttpServiceConfigUrlAclSet)); + var rev = new UrlReservation(outputConfigInfo.KeyDesc.UrlPrefix, SecurityIdentifiersFromSecurityDescriptor(outputConfigInfo.ParamDesc.StringSecurityDescriptor).ToArray()); + reservations.Add(rev); + } + Marshal.FreeHGlobal(pOutputConfigInfo); + Marshal.FreeHGlobal(pInputConfigInfo); + i++; + } + + retVal = HttpApi.HttpTerminate(HttpApiConstants.InitializeConfig, IntPtr.Zero); + + } + + if (retVal != ErrorCode.Success) + { + throw new Win32Exception(Convert.ToInt32(retVal)); + } + return new ReadOnlyCollection(reservations); + } + + public static void Create(UrlReservation urlReservation) + { + if (urlReservation.securityIdentifiers.Count == 0) + { + throw new Exception("No SecurityIdentifiers have been assigned to the URLACL"); + } + + var sddl = GenerateSecurityDescriptor(urlReservation.securityIdentifiers); + reserveURL(urlReservation.Url, sddl); + } + + private static void reserveURL(string networkURL, string securityDescriptor) + { + + var retVal = HttpApi.HttpInitialize(HttpApiConstants.Version1, HttpApiConstants.InitializeConfig, IntPtr.Zero); + if (retVal == ErrorCode.Success) + { + var keyDesc = new HttpServiceConfigUrlAclKey(networkURL); + var paramDesc = new HttpServiceConfigUrlAclParam(securityDescriptor); + + var inputConfigInfoSet = new HttpServiceConfigUrlAclSet + { + KeyDesc = keyDesc, + ParamDesc = paramDesc + }; + + var pInputConfigInfo = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(HttpServiceConfigUrlAclSet))); + Marshal.StructureToPtr(inputConfigInfoSet, pInputConfigInfo, false); + + retVal = HttpApi.HttpSetServiceConfiguration(IntPtr.Zero, + HttpServiceConfigId.HttpServiceConfigUrlAclInfo, + pInputConfigInfo, + Marshal.SizeOf(inputConfigInfoSet), + IntPtr.Zero); + + if (ErrorCode.AlreadyExists == retVal) + { + retVal = HttpApi.HttpDeleteServiceConfiguration(IntPtr.Zero,HttpServiceConfigId.HttpServiceConfigUrlAclInfo, pInputConfigInfo,Marshal.SizeOf(inputConfigInfoSet),IntPtr.Zero); + + if (ErrorCode.Success == retVal) + { + retVal = HttpApi.HttpSetServiceConfiguration(IntPtr.Zero,HttpServiceConfigId.HttpServiceConfigUrlAclInfo,pInputConfigInfo,Marshal.SizeOf(inputConfigInfoSet),IntPtr.Zero); + } + } + + Marshal.FreeHGlobal(pInputConfigInfo); + HttpApi.HttpTerminate(HttpApiConstants.InitializeConfig, IntPtr.Zero); + } + + if (retVal != ErrorCode.Success) + { + throw new Win32Exception(Convert.ToInt32(retVal)); + } + } + + public static void Delete(UrlReservation urlReservation) + { + var securityDescriptor = GenerateSecurityDescriptor(urlReservation.securityIdentifiers); + FreeURL(urlReservation.Url, securityDescriptor); + } + + private static void FreeURL(string networkURL, string securityDescriptor) + { + var retVal = HttpApi.HttpInitialize(HttpApiConstants.Version1, HttpApiConstants.InitializeConfig, IntPtr.Zero); + if (ErrorCode.Success == retVal) + { + var urlAclKey = new HttpServiceConfigUrlAclKey(networkURL); + var urlAclParam = new HttpServiceConfigUrlAclParam(securityDescriptor); + + var urlAclSet = new HttpServiceConfigUrlAclSet + { + KeyDesc = urlAclKey, + ParamDesc = urlAclParam + }; + + var configInformation = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(HttpServiceConfigUrlAclSet))); + Marshal.StructureToPtr(urlAclSet, configInformation, false); + var configInformationSize = Marshal.SizeOf(urlAclSet); + retVal = HttpApi.HttpDeleteServiceConfiguration(IntPtr.Zero, + HttpServiceConfigId.HttpServiceConfigUrlAclInfo, + configInformation, + configInformationSize, + IntPtr.Zero); + + Marshal.FreeHGlobal(configInformation); + HttpApi.HttpTerminate(HttpApiConstants.InitializeConfig, IntPtr.Zero); + } + + if (ErrorCode.Success != retVal) + { + throw new Win32Exception(Convert.ToInt32(retVal)); + } + } + + private static IEnumerable SecurityIdentifiersFromSecurityDescriptor(string securityDescriptor) + { + var commonSecurityDescriptor = new CommonSecurityDescriptor(false, false, securityDescriptor); + var discretionaryAcl = commonSecurityDescriptor.DiscretionaryAcl; + return discretionaryAcl.Cast().Select(ace => ace.SecurityIdentifier); + } + + private static DiscretionaryAcl GetDiscretionaryAcl(List securityIdentifiers) + { + var discretionaryAcl = new DiscretionaryAcl(false, false, 16); + foreach (var securityIdentifier in securityIdentifiers) + { + discretionaryAcl.AddAccess(AccessControlType.Allow, securityIdentifier, GENERIC_EXECUTE, InheritanceFlags.None, PropagationFlags.None); + } + + return discretionaryAcl; + } + + private static CommonSecurityDescriptor GetSecurityDescriptor(List securityIdentifiers) + { + var discretionaryAcl = GetDiscretionaryAcl(securityIdentifiers); + var securityDescriptor = new CommonSecurityDescriptor(false, false, + ControlFlags.GroupDefaulted | + ControlFlags.OwnerDefaulted | + ControlFlags.DiscretionaryAclPresent, + null, null, null, discretionaryAcl); + return securityDescriptor; + } + + private static string GenerateSecurityDescriptor(List securityIdentifiers) + { + return GetSecurityDescriptor(securityIdentifiers).GetSddlForm(AccessControlSections.Access); + } + + public byte[] ToDiscretionaryAclBytes() + { + var discretionaryAcl = GetDiscretionaryAcl(securityIdentifiers); + var bytes = new byte[discretionaryAcl.BinaryLength]; + discretionaryAcl.GetBinaryForm(bytes, 0); + return bytes; + } + + public byte[] ToSystemAclBytes() + { + var systemAcl = new SystemAcl(false, false, 0); + var bytes = new byte[systemAcl.BinaryLength]; + systemAcl.GetBinaryForm(bytes, 0); + return bytes; + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Installers/UrlAclInstaller.cs b/src/ServiceControl/Infrastructure/Installers/UrlAclInstaller.cs index df42f90fe3..82e9ad5c11 100644 --- a/src/ServiceControl/Infrastructure/Installers/UrlAclInstaller.cs +++ b/src/ServiceControl/Infrastructure/Installers/UrlAclInstaller.cs @@ -1,8 +1,6 @@ namespace ServiceBus.Management.Infrastructure.Installers { using System; - using System.Diagnostics; - using System.IO; using System.Security.Principal; using NServiceBus; using NServiceBus.Installation; @@ -32,10 +30,12 @@ public void Install(string identity, Configure config) httpcfg set urlacl /u {{http://URL:PORT/[PATH/] | https://URL:PORT/[PATH/]}} /a D:(A;;GX;;;""{0}"")", identity); return; } - StartNetshProcess(identity, Settings.ApiUrl); - } - + Logger.InfoFormat("Granting user '{0}' HttpListener permissions to {1}", identity, Settings.ApiUrl); + var reservation = new UrlReservation(Settings.ApiUrl, accountSid); + reservation.Create(); + } + static bool CurrentUserIsNotAdmin() { // ReSharper disable once AssignNullToNotNullAttribute @@ -43,77 +43,6 @@ static bool CurrentUserIsNotAdmin() return !principal.IsInRole(WindowsBuiltInRole.Administrator); } - static void StartNetshProcess(string identity, string uri, bool deleteExisting = true) - { - var startInfo = GetProcessStartInfo(identity, uri); - - string error; - - if (ExecuteNetshCommand(startInfo, out error)) - { - Logger.InfoFormat("Granted user '{0}' HttpListener permissions for {1}.", identity, uri); - return; - } - - if (deleteExisting && error.Contains(": 183")) - { - startInfo = GetProcessStartInfo(identity, uri, true); - Logger.Info(string.Format(@"Failed to grant to grant user '{0}' HttpListener permissions. The error message from running the above command is: {1} Will try to delete the existing urlacl",identity, error)); - - if (ExecuteNetshCommand(startInfo, out error)) - { - Logger.InfoFormat("Deleted user HttpListener permissions for {0}.", uri); - StartNetshProcess(identity, uri, false); - return; - } - } - - throw new Exception(string.Format( - @"Failed to grant to grant user '{0}' HttpListener permissions. -Try running the following command from an admin console: -netsh http add urlacl url={2} user=""{0}"" - -The error message from running the above command is: -{1}", identity, error, uri)); - } - - static bool ExecuteNetshCommand(ProcessStartInfo startInfo, out string error) - { - error = null; - using (var process = Process.Start(startInfo)) - { - process.WaitForExit(5000); - - if (process.ExitCode == 0) - { - return true; - } - error = process.StandardOutput.ReadToEnd().Trim(); - return false; - } - } - - static ProcessStartInfo GetProcessStartInfo(string identity, string uri, bool delete = false) - { - var arguments = string.Format(@"http {1} urlacl url={0}", uri, delete ? "delete" : "add"); - - if (!delete) - { - arguments += string.Format(" user=\"{0}\"", identity); - } - - var startInfo = new ProcessStartInfo - { - CreateNoWindow = true, - Verb = "runas", - UseShellExecute = false, - RedirectStandardOutput = true, - Arguments = arguments, - FileName = "netsh", - WorkingDirectory = Path.GetTempPath() - }; - return startInfo; - } static readonly ILog Logger = LogManager.GetLogger(typeof(UrlAclInstaller)); } diff --git a/src/ServiceControl/Infrastructure/PeriodicExecutor.cs b/src/ServiceControl/Infrastructure/PeriodicExecutor.cs deleted file mode 100644 index 4a86e558b0..0000000000 --- a/src/ServiceControl/Infrastructure/PeriodicExecutor.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace ServiceControl.Infrastructure -{ - using System; - using System.Threading; - using System.Threading.Tasks; - - public class PeriodicExecutor - { - Action action; - Action onError; - TimeSpan period; - CancellationTokenSource tokenSource; - - public PeriodicExecutor(Action action, TimeSpan period, Action onError = null) - { - this.action = action; - this.period = period; - this.onError = onError ?? (e => { }); - } - - public bool IsCancellationRequested - { - get - { - if (tokenSource == null) - { - return true; - } - return tokenSource.IsCancellationRequested; - } - } - - public void Start(bool delay) - { - if (tokenSource != null) - { - throw new InvalidOperationException("Executor has already been started"); - } - tokenSource = new CancellationTokenSource(); - Task.Run(async () => - { - var cancelToken = tokenSource.Token; - - if (delay) - await Task.Delay(period, cancelToken); - - while (!cancelToken.IsCancellationRequested) - { - var nextTime = DateTime.Now + period; - - try - { - action(this); - } - catch (Exception ex) - { - onError(ex); - } - - var delayPeriod = nextTime - DateTime.Now; - if (delayPeriod > TimeSpan.Zero) - { - await Task.Delay(delayPeriod, cancelToken); - } - } - }, tokenSource.Token); - } - - public void Stop() - { - if (tokenSource != null) - { - tokenSource.Cancel(); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs b/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs index edf3e03755..507263895a 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs +++ b/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleanerBundle.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.Composition; + using System.Threading; using Raven.Abstractions.Logging; using Raven.Database; using Raven.Database.Plugins; @@ -13,7 +14,7 @@ public class ExpiredDocumentsCleanerBundle : IStartupTask, IDisposable { ILog logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleanerBundle)); - PeriodicExecutor timer; + Timer timer; public void Execute(DocumentDatabase database) { @@ -29,15 +30,24 @@ public void Execute(DocumentDatabase database) logger.Info("Deletion batch size set to {0}", deletionBatchSize); logger.Info("Retention period is {0} hours", Settings.HoursToKeepMessagesBeforeExpiring); - timer = new PeriodicExecutor(executor => ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database), TimeSpan.FromSeconds(deleteFrequencyInSeconds)); - timer.Start(true); + var due = TimeSpan.FromSeconds(deleteFrequencyInSeconds); + timer = new Timer(executor => + { + ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database); + + timer.Change(due, Timeout.InfiniteTimeSpan); + }, null, due, Timeout.InfiniteTimeSpan); } public void Dispose() { if (timer != null) { - timer.Stop(); + using (var manualResetEvent = new ManualResetEvent(false)) + { + timer.Dispose(manualResetEvent); + manualResetEvent.WaitOne(); + } } } } diff --git a/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs b/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs index 99c1bbbebc..be18769113 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs +++ b/src/ServiceControl/Infrastructure/RavenDB/RavenBootstrapper.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using NServiceBus; + using NServiceBus.Configuration.AdvanceExtensibility; using NServiceBus.Logging; using NServiceBus.Persistence; using NServiceBus.Pipeline; @@ -31,14 +32,13 @@ public static string ReadLicense() public void Customize(BusConfiguration configuration) { + var documentStore = configuration.GetSettings().Get("ServiceControl.EmbeddableDocumentStore"); + Directory.CreateDirectory(Settings.DbPath); - var documentStore = new EmbeddableDocumentStore - { - DataDirectory = Settings.DbPath, - UseEmbeddedHttpServer = Settings.MaintenanceMode || Settings.ExposeRavenDB, - EnlistInDistributedTransactions = false, - }; + documentStore.DataDirectory = Settings.DbPath; + documentStore.UseEmbeddedHttpServer = Settings.MaintenanceMode || Settings.ExposeRavenDB; + documentStore.EnlistInDistributedTransactions = false; var localRavenLicense = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RavenLicense.xml"); if (File.Exists(localRavenLicense)) @@ -92,8 +92,7 @@ public void Customize(BusConfiguration configuration) PurgeKnownEndpointsWithTemporaryIdsThatAreDuplicate(documentStore); configuration.RegisterComponents(c => - c.RegisterSingleton(documentStore) - .ConfigureComponent(builder => + c.ConfigureComponent(builder => { var context = builder.Build().CurrentContext; diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 908804e149..579030c6d2 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -159,7 +159,7 @@ public static NLog.LogLevel LoggingLevel var level = NLog.LogLevel.Warn; try { - level = NLog.LogLevel.FromString(SettingsReader.Read("LogLevel")); + level = NLog.LogLevel.FromString(SettingsReader.Read("LogLevel", NLog.LogLevel.Warn.Name)); } catch { @@ -169,6 +169,24 @@ public static NLog.LogLevel LoggingLevel } } + public static NLog.LogLevel RavenDBLogLevel + { + get + { + var level = NLog.LogLevel.Warn; + try + { + level = NLog.LogLevel.FromString(SettingsReader.Read("RavenDBLogLevel", NLog.LogLevel.Warn.Name)); + } + catch + { + NLog.Common.InternalLogger.Warn("Failed to parse RavenDBLogLevel setting. Defaulting to Warn"); + } + return level; + } + } + + public static string LogPath { get diff --git a/src/ServiceControl/Infrastructure/ShutdownNotifier.cs b/src/ServiceControl/Infrastructure/ShutdownNotifier.cs new file mode 100644 index 0000000000..c671ac9f65 --- /dev/null +++ b/src/ServiceControl/Infrastructure/ShutdownNotifier.cs @@ -0,0 +1,21 @@ +namespace ServiceControl.Infrastructure +{ + using System; + using System.Threading; + + public class ShutdownNotifier : IDisposable + { + CancellationTokenSource source = new CancellationTokenSource(); + + public void Register(Action callback) + { + source.Token.Register(callback); + } + + public void Dispose() + { + source.Cancel(); + source.Dispose(); + } + } +} diff --git a/src/ServiceControl/Infrastructure/TimeKeeper.cs b/src/ServiceControl/Infrastructure/TimeKeeper.cs new file mode 100644 index 0000000000..059fe1a9cd --- /dev/null +++ b/src/ServiceControl/Infrastructure/TimeKeeper.cs @@ -0,0 +1,68 @@ +namespace ServiceControl.Infrastructure +{ + using System; + using System.Collections.Concurrent; + using System.Threading; + using NServiceBus.Logging; + + public class TimeKeeper : IDisposable + { + ConcurrentDictionary timers = new ConcurrentDictionary(); + private ILog log = LogManager.GetLogger(); + + public Timer New(Action callback, TimeSpan dueTime, TimeSpan period) + { + Timer timer = null; + + timer = new Timer(_ => + { + try + { + callback(); + } + catch (Exception ex) + { + log.Error("Reoccurring timer task failed.", ex); + } + if (timers.ContainsKey(timer)) + { + try + { + timer.Change(period, Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // timer has been disposed already, safe to ignore + } + } + }, null, dueTime, Timeout.InfiniteTimeSpan); + + timers.TryAdd(timer, null); + return timer; + } + + public void Release(Timer timer) + { + object _; + timers.TryRemove(timer, out _); + WaitAndDispose(timer); + } + + public void Dispose() + { + foreach (var pair in timers) + { + WaitAndDispose(pair.Key); + } + } + + private static void WaitAndDispose(Timer timer) + { + using (var manualResetEvent = new ManualResetEvent(false)) + { + timer.Dispose(manualResetEvent); + manualResetEvent.WaitOne(); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Operations/DetectFailedMessageImportsFeature.cs b/src/ServiceControl/Operations/DetectFailedMessageImportsFeature.cs index e99043a61d..352083f6eb 100644 --- a/src/ServiceControl/Operations/DetectFailedMessageImportsFeature.cs +++ b/src/ServiceControl/Operations/DetectFailedMessageImportsFeature.cs @@ -29,7 +29,7 @@ public CheckForFailedErrorMessageImports(IDocumentStore store) protected override void OnStart() { source = new CancellationTokenSource(); - task = Task.Factory.StartNew(() => Run(source.Token), source.Token); + task = Task.Factory.StartNew(() => Run(source.Token)); } protected override void OnStop() diff --git a/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroup.cs b/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroup.cs index cd635817f0..afc1761926 100644 --- a/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroup.cs +++ b/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroup.cs @@ -1,9 +1,11 @@ namespace ServiceControl.Recoverability { + using System; using NServiceBus; public class ArchiveAllInGroup : ICommand { public string GroupId { get; set; } + public DateTime? CutOff { get; set; } } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroupHandler.cs b/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroupHandler.cs index 4a72ef0025..ff918071cc 100644 --- a/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroupHandler.cs +++ b/src/ServiceControl/Recoverability/Grouping/Archiving/ArchiveAllInGroupHandler.cs @@ -1,72 +1,72 @@ namespace ServiceControl.Recoverability { - using System.Collections.Generic; + using System.Globalization; + using System.Linq; using NServiceBus; using Raven.Abstractions.Data; - using Raven.Abstractions.Exceptions; + using Raven.Abstractions.Extensions; using Raven.Client; - using Raven.Client.Linq; + using ServiceControl.Infrastructure; using ServiceControl.MessageFailures; public class ArchiveAllInGroupHandler : IHandleMessages { - public void Handle(ArchiveAllInGroup message) - { - var query = Session.Query() - .Where(m => m.FailureGroupId == message.GroupId && m.Status == FailedMessageStatus.Unresolved) - .ProjectFromIndexFieldsInto(); - - string groupName = null; - var messageIds = new List(); - - using (var stream = Session.Advanced.Stream(query)) - { - while (stream.MoveNext()) - { - if (stream.Current.Document.Status != FailedMessageStatus.Unresolved) - { - continue; - } + private bool abort; - if (groupName == null) - { - groupName = stream.Current.Document.FailureGroupName; - } + public ArchiveAllInGroupHandler(ShutdownNotifier notifier) + { + notifier.Register(() => { abort = true; }); + } - try { - Session.Advanced.DocumentStore.DatabaseCommands.Patch( - stream.Current.Document.Id, + public void Handle(ArchiveAllInGroup message) + { + var result = Session.Advanced.DocumentStore.DatabaseCommands.UpdateByIndex( + new FailedMessages_ByGroup().IndexName, + new IndexQuery + { + Query = string.Format(CultureInfo.InvariantCulture, "FailureGroupId:{0} AND Status:{1}", message.GroupId, (int)FailedMessageStatus.Unresolved), + Cutoff = message.CutOff + }, new[] { new PatchRequest { Type = PatchCommandType.Set, Name = "Status", - Value = (int) FailedMessageStatus.Archived, - PrevVal = (int) FailedMessageStatus.Unresolved + Value = (int) FailedMessageStatus.Archived } - }); + }, true).WaitForCompletion(); - messageIds.Add(stream.Current.Document.MessageId); - } - catch (ConcurrencyException) - { - // Ignore concurrency exceptions - } + var patchedDocumentIds = result.JsonDeserialization(); - - } + if (patchedDocumentIds.Length == 0) + { + return; } - Bus.Publish(m => + var failedMessage = Session.Load(patchedDocumentIds[0].Document); + var failureGroup = failedMessage.FailureGroups.FirstOrDefault(); + var groupName = "Undefined"; + + if (failureGroup != null && failureGroup.Title != null) { - m.GroupId = message.GroupId; - m.GroupName = groupName; - m.MessageIds = messageIds.ToArray(); - }); + groupName = failureGroup.Title; + } + + Bus.Publish(m => + { + m.GroupId = message.GroupId; + m.GroupName = groupName; + m.MessagesCount = patchedDocumentIds.Length; + }); } public IDocumentSession Session { get; set; } public IBus Bus { get; set; } + + class DocumentPatchResult + { + public string Document { get; set; } + } } -} \ No newline at end of file +} diff --git a/src/ServiceControl/Recoverability/Grouping/Archiving/FailedMessageGroupArchived.cs b/src/ServiceControl/Recoverability/Grouping/Archiving/FailedMessageGroupArchived.cs index 1b84a22c5d..06f3c0ddb3 100644 --- a/src/ServiceControl/Recoverability/Grouping/Archiving/FailedMessageGroupArchived.cs +++ b/src/ServiceControl/Recoverability/Grouping/Archiving/FailedMessageGroupArchived.cs @@ -6,6 +6,6 @@ public class FailedMessageGroupArchived : IEvent { public string GroupId { get; set; } public string GroupName { get; set; } - public string[] MessageIds { get; set; } + public int MessagesCount { get; set; } } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Grouping/Archiving/FailureGroupsArchiveApi.cs b/src/ServiceControl/Recoverability/Grouping/Archiving/FailureGroupsArchiveApi.cs index 1642d99c1d..7061ea3ec2 100644 --- a/src/ServiceControl/Recoverability/Grouping/Archiving/FailureGroupsArchiveApi.cs +++ b/src/ServiceControl/Recoverability/Grouping/Archiving/FailureGroupsArchiveApi.cs @@ -20,7 +20,11 @@ dynamic ArchiveGroupErrors(string groupId) return HttpStatusCode.BadRequest; } - Bus.SendLocal(m => m.GroupId = groupId); + Bus.SendLocal(m => + { + m.GroupId = groupId; + m.CutOff = DateTime.UtcNow; + }); return HttpStatusCode.Accepted; } diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs b/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs index a05953888f..e19f2335c5 100644 --- a/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs +++ b/src/ServiceControl/Recoverability/Grouping/Groupers/ReclassifyErrorsHandler.cs @@ -25,82 +25,99 @@ class ReclassifyErrorsHandler : IHandleMessages readonly IEnumerable classifiers; const int BatchSize = 1000; int failedMessagesReclassified; + private bool abort; + private static int executing; ILog logger = LogManager.GetLogger(); - public ReclassifyErrorsHandler(IBus bus, IDocumentStore store, IEnumerable classifiers) + public ReclassifyErrorsHandler(IBus bus, IDocumentStore store, ShutdownNotifier notifier, IEnumerable classifiers) { this.bus = bus; this.store = store; this.classifiers = classifiers; + + notifier.Register(() => { abort = true; }); } public void Handle(ReclassifyErrors message) { - using (var session = store.OpenSession()) + if (Interlocked.Exchange(ref executing, 1) != 0) { - ReclassifyErrorSettings settings = null; + // Prevent more then one execution at a time + return; + } - if (!message.Force) + try + { + using (var session = store.OpenSession()) { - settings = session.Load(ReclassifyErrorSettings.IdentifierCase); + ReclassifyErrorSettings settings = null; - if (settings != null && settings.ReclassificationDone) + if (!message.Force) { - logger.Info("Skipping reclassification of failures as classification has already been done."); - return; + settings = session.Load(ReclassifyErrorSettings.IdentifierCase); + + if (settings != null && settings.ReclassificationDone) + { + logger.Info("Skipping reclassification of failures as classification has already been done."); + return; + } } - } - logger.Info("Reclassification of failures started."); + logger.Info("Reclassification of failures started."); - var query = session.Query() - .Where(f => f.Status == FailedMessageStatus.Unresolved); + var query = session.Query() + .Where(f => f.Status == FailedMessageStatus.Unresolved); - var currentBatch = new List>(); + var currentBatch = new List>(); - using (var stream = session.Advanced.Stream(query.As())) - { - while (stream.MoveNext()) + using (var stream = session.Advanced.Stream(query.As())) { - if (stream.Current.Document.FailureGroups.Count > 0) + while (!abort && stream.MoveNext()) { - continue; - } + if (stream.Current.Document.FailureGroups.Count > 0) + { + continue; + } - currentBatch.Add(Tuple.Create(stream.Current.Document.Id, stream.Current.Document.ProcessingAttempts.Last().FailureDetails)); + currentBatch.Add(Tuple.Create(stream.Current.Document.Id, stream.Current.Document.ProcessingAttempts.Last().FailureDetails)); - if (currentBatch.Count == BatchSize) - { - ReclassifyBatch(currentBatch); - currentBatch.Clear(); + if (currentBatch.Count == BatchSize) + { + ReclassifyBatch(currentBatch); + currentBatch.Clear(); + } } } - } - if (currentBatch.Any()) - { - ReclassifyBatch(currentBatch); - } + if (currentBatch.Any()) + { + ReclassifyBatch(currentBatch); + } - logger.Info("Reclassification of failures ended."); + logger.Info("Reclassification of failures ended."); - if (settings == null) - { - settings = new ReclassifyErrorSettings(); + if (settings == null) + { + settings = new ReclassifyErrorSettings(); + } + + settings.ReclassificationDone = true; + session.Store(settings); + session.SaveChanges(); } - settings.ReclassificationDone = true; - session.Store(settings); - session.SaveChanges(); + if (failedMessagesReclassified > 0) + { + bus.Publish(new ReclassificationOfErrorMessageComplete + { + NumberofMessageReclassified = failedMessagesReclassified + }); + } } - - if (failedMessagesReclassified > 0) + finally { - bus.Publish(new ReclassificationOfErrorMessageComplete - { - NumberofMessageReclassified = failedMessagesReclassified - }); + Interlocked.Exchange(ref executing, 0); } } diff --git a/src/ServiceControl/Recoverability/Retries/FailedMessageRetries.cs b/src/ServiceControl/Recoverability/Retries/FailedMessageRetries.cs index 6ef00aa756..0d6057ec20 100644 --- a/src/ServiceControl/Recoverability/Retries/FailedMessageRetries.cs +++ b/src/ServiceControl/Recoverability/Retries/FailedMessageRetries.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Recoverability { using System; + using System.Threading; using NServiceBus; using NServiceBus.Features; using NServiceBus.Logging; @@ -27,22 +28,22 @@ protected override void Setup(FeatureConfigurationContext context) class BulkRetryBatchCreation : FeatureStartupTask { readonly RetriesGateway retries; - PeriodicExecutor retriesGatewayExecutor; + private readonly TimeKeeper timeKeeper; + private Timer timer; + private bool abortProcessing; - public BulkRetryBatchCreation(RetriesGateway retries) + public BulkRetryBatchCreation(RetriesGateway retries, TimeKeeper timeKeeper) { this.retries = retries; - - retriesGatewayExecutor = new PeriodicExecutor( - ProcessRequestedBulkRetryOperations, - TimeSpan.FromSeconds(5)); + this.timeKeeper = timeKeeper; } protected override void OnStart() { if (retries != null) { - retriesGatewayExecutor.Start(true); + var due = TimeSpan.FromSeconds(5); + timer = timeKeeper.New(ProcessRequestedBulkRetryOperations, due, due); } } @@ -50,92 +51,102 @@ protected override void OnStop() { if (retries != null) { - retriesGatewayExecutor.Stop(); + abortProcessing = true; + timeKeeper.Release(timer); } } - void ProcessRequestedBulkRetryOperations(PeriodicExecutor obj) + void ProcessRequestedBulkRetryOperations() { bool processedRequests; do { processedRequests = retries.ProcessNextBulkRetry(); - } while (processedRequests && !obj.IsCancellationRequested); + } while (processedRequests && !abortProcessing); } } class AdoptOrphanBatchesFromPreviousSession : FeatureStartupTask { - public AdoptOrphanBatchesFromPreviousSession(RetryDocumentManager retryDocumentManager) + private Timer timer; + + public AdoptOrphanBatchesFromPreviousSession(RetryDocumentManager retryDocumentManager, TimeKeeper timeKeeper) { this.retryDocumentManager = retryDocumentManager; - executor = new PeriodicExecutor( - AdoptOrphanedBatches, - TimeSpan.FromMinutes(2) - ); + this.timeKeeper = timeKeeper; } - private void AdoptOrphanedBatches(PeriodicExecutor ex) + private void AdoptOrphanedBatches() { var allDone = retryDocumentManager.AdoptOrphanedBatches(); if (allDone) { - executor.Stop(); + //Disable timeout + timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } } protected override void OnStart() { - executor.Start(false); + timer = timeKeeper.New(AdoptOrphanedBatches, TimeSpan.Zero, TimeSpan.FromMinutes(2)); } protected override void OnStop() { - executor.Stop(); + timeKeeper.Release(timer); } - PeriodicExecutor executor; RetryDocumentManager retryDocumentManager; + private readonly TimeKeeper timeKeeper; } class ProcessRetryBatches : FeatureStartupTask { static ILog log = LogManager.GetLogger(typeof(ProcessRetryBatches)); - - public ProcessRetryBatches(IDocumentStore store, RetryProcessor processor) + private Timer timer; + public ProcessRetryBatches(IDocumentStore store, RetryProcessor processor, TimeKeeper timeKeeper) { - executor = new PeriodicExecutor(Process, TimeSpan.FromSeconds(30), ex => log.Error("Error during retry batch processing", ex)); this.processor = processor; + this.timeKeeper = timeKeeper; this.store = store; } protected override void OnStart() { - executor.Start(false); + timer = timeKeeper.New(Process, TimeSpan.Zero, TimeSpan.FromSeconds(30)); } protected override void OnStop() { - executor.Stop(); + abortProcessing = true; + timeKeeper.Release(timer); } - void Process(PeriodicExecutor e) + void Process() { - bool batchesProcessed; - do + try { - using (var session = store.OpenSession()) + bool batchesProcessed; + do { - batchesProcessed = processor.ProcessBatches(session); - session.SaveChanges(); - } - } while (batchesProcessed && !e.IsCancellationRequested); + using (var session = store.OpenSession()) + { + batchesProcessed = processor.ProcessBatches(session); + session.SaveChanges(); + } + } while (batchesProcessed && !abortProcessing); + } + catch (Exception ex) + { + log.Error("Error during retry batch processing", ex); + } } - PeriodicExecutor executor; IDocumentStore store; RetryProcessor processor; + private readonly TimeKeeper timeKeeper; + private bool abortProcessing; } } } diff --git a/src/ServiceControl/Recoverability/Retries/RetryDocumentManager.cs b/src/ServiceControl/Recoverability/Retries/RetryDocumentManager.cs index 67792a1ccf..c55bb2272d 100644 --- a/src/ServiceControl/Recoverability/Retries/RetryDocumentManager.cs +++ b/src/ServiceControl/Recoverability/Retries/RetryDocumentManager.cs @@ -10,6 +10,7 @@ namespace ServiceControl.Recoverability using Raven.Client; using Raven.Client.Linq; using Raven.Json.Linq; + using ServiceControl.Infrastructure; using ServiceControl.MessageFailures; public class RetryDocumentManager @@ -18,6 +19,13 @@ public class RetryDocumentManager static string RetrySessionId = Guid.NewGuid().ToString(); + private bool abort; + + public RetryDocumentManager(ShutdownNotifier notifier) + { + notifier.Register(() => { abort = true; }); + } + public string CreateBatchDocument(string context = null) { var batchDocumentId = RetryBatch.MakeDocumentId(Guid.NewGuid().ToString()); @@ -113,7 +121,7 @@ internal bool AdoptOrphanedBatches() AdoptBatches(session, orphanedBatchIds); var moreToDo = stats.IsStale || orphanedBatchIds.Any(); - return !moreToDo; + return abort || !moreToDo; } } @@ -131,13 +139,16 @@ void AdoptBatch(IDocumentSession session, string batchId) using (var stream = session.Advanced.Stream(query)) { - while (stream.MoveNext()) + while (!abort && stream.MoveNext()) { messageIds.Add(stream.Current.Document.Id); } } - MoveBatchToStaging(batchId, messageIds.ToArray()); + if (!abort) + { + MoveBatchToStaging(batchId, messageIds.ToArray()); + } } } } \ No newline at end of file diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 15c9dc77de..d960ef49d3 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -287,6 +287,24 @@ + + + + + + + + + + + + + + + + + + @@ -294,7 +312,7 @@ - + diff --git a/src/ServiceControlInstaller.Engine/Configuration/ConfigurationWriter.cs b/src/ServiceControlInstaller.Engine/Configuration/ConfigurationWriter.cs index 1302d992cb..d4930de8f6 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ConfigurationWriter.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ConfigurationWriter.cs @@ -4,6 +4,7 @@ using System.Configuration; using System.IO; using System.Linq; + using System.Xml.Linq; using ServiceControlInstaller.Engine.Instances; internal class ConfigurationWriter @@ -40,9 +41,37 @@ public void Save() settings.Set("ServiceBus/AuditQueue", details.AuditQueue); settings.Set("ServiceBus/ErrorQueue", details.ErrorQueue); settings.Set("ServiceBus/ErrorLogQueue", details.ErrorLogQueue); - settings.Remove("ServiceBus/AuditLogQueue"); settings.Set("ServiceBus/AuditLogQueue", details.AuditLogQueue); + + // Add Settings for performance tuning + // See https://github.com/Particular/ServiceControl/issues/655 + if (!settings.AllKeys.Contains("Raven/Esent/MaxVerPages")) + { + settings.Add("Raven/Esent/MaxVerPages", "2048"); + } + UpdateRuntimeSection(); + configuration.Save(); } + + void UpdateRuntimeSection() + { + + var runtimesection = configuration.GetSection("runtime"); + var runtimeXml = XDocument.Parse(runtimesection.SectionInformation.GetRawXml() ?? ""); + + // Set gcServer Value if it does not exist + var gcServer = runtimeXml.Descendants("gcServer").SingleOrDefault(); + if (gcServer == null) //So no config so we can set + { + gcServer = new XElement("gcServer"); + gcServer.SetAttributeValue("enabled", "true"); + if (runtimeXml.Root != null) + { + runtimeXml.Root.Add(gcServer); + runtimesection.SectionInformation.SetRawXml(runtimeXml.Root.ToString()); + } + } + } } }