diff --git a/.gitignore b/.gitignore index 1a2e20de58..86bdc17724 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,13 @@ nugets +deploy build32 binaries -obj -bin *.vshost.* .nu -_ReSharper.* _UpgradeReport.* -*.csproj.user -*.resharper.user -*.resharper -*.suo *.cache *~ *.swp -*.user -TestResults -TestResult.xml results CommonAssemblyInfo.cs lib/sqlite/System.Data.SQLite.dll @@ -35,3 +26,47 @@ _NCrunch_NServiceBus/* logs run-git.cmd src/Chocolatey/Build/* + +# Created by https://www.gitignore.io + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user diff --git a/installer/ServiceControl.aip b/installer/ServiceControl.aip index d6a8bc28b6..b373c811df 100644 --- a/installer/ServiceControl.aip +++ b/installer/ServiceControl.aip @@ -19,10 +19,13 @@ + + + @@ -52,147 +55,19 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -243,6 +118,9 @@ + + + @@ -278,20 +156,22 @@ - + - + - - + + - + - + + + @@ -384,6 +264,12 @@ + + + + + + @@ -395,20 +281,10 @@ - - - - - - - - - - - - - - + + + + @@ -420,6 +296,8 @@ + + @@ -443,14 +321,20 @@ + + + + - + + - + + @@ -466,13 +350,13 @@ - + - + - + @@ -485,6 +369,12 @@ + + + + + + diff --git a/src/Chocolatey/chocolateyInstall.ps1 b/src/Chocolatey/chocolateyInstall.ps1 index 4a1502f876..249b7acb57 100644 --- a/src/Chocolatey/chocolateyInstall.ps1 +++ b/src/Chocolatey/chocolateyInstall.ps1 @@ -10,7 +10,6 @@ else{ $url = "https://github.com/Particular/$packageName/releases/download/{{ReleaseName}}/Particular.$packageName-{{FileVersion}}.exe" } - try { $existngService = Get-Service -Name "Particular.Management" -ErrorAction SilentlyContinue @@ -29,7 +28,7 @@ try { } Get-ChocolateyWebFile $packageName $file $url - $msiArguments ="/quiet /L*V `"$logFile`"" + $msiArguments ="/quiet PlatformInstaller=true /L*V `"$logFile`"" Write-Host "Starting installer with arguments: $msiArguments"; Start-ChocolateyProcessAsAdmin "$msiArguments" $file -validExitCodes 0 Write-ChocolateySuccess $packageName diff --git a/src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSytemMessage_Tests.cs b/src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSystemMessage_Tests.cs similarity index 97% rename from src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSytemMessage_Tests.cs rename to src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSystemMessage_Tests.cs index 948e5781e3..6663f85199 100644 --- a/src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSytemMessage_Tests.cs +++ b/src/ServiceControl.AcceptanceTests/Audit/Audit_Messages_Have_Proper_IsSystemMessage_Tests.cs @@ -4,13 +4,11 @@ using Contexts; using NServiceBus; using NServiceBus.AcceptanceTesting; - using NServiceBus.Features; using NServiceBus.Transports; using NUnit.Framework; using ServiceControl.CompositeViews.Messages; - using ServiceControl.MessageFailures.Api; - class Audit_Messages_Have_Proper_IsSytemMessage_Tests: AcceptanceTest + class Audit_Messages_Have_Proper_IsSystemMessage_Tests: AcceptanceTest { [Test] public void Should_set_the_IsSystemMessage_when_message_type_is_not_a_scheduled_task() diff --git a/src/ServiceControl.AcceptanceTests/ExternalIntegrations/When_a_message_has_custom_checks.cs b/src/ServiceControl.AcceptanceTests/ExternalIntegrations/When_a_message_has_custom_checks.cs new file mode 100644 index 0000000000..6c002f08a2 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests/ExternalIntegrations/When_a_message_has_custom_checks.cs @@ -0,0 +1,150 @@ +namespace ServiceBus.Management.AcceptanceTests.ExternalIntegrations +{ + using System; + using Contexts; + using NServiceBus; + using NServiceBus.AcceptanceTesting; + using NServiceBus.Config; + using NServiceBus.Config.ConfigurationSource; + using NServiceBus.Features; + using NServiceBus.Unicast.Subscriptions; + using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; + using NUnit.Framework; + using ServiceControl.Contracts; + using ServiceControl.Contracts.Operations; + + [TestFixture] + public class When_a_message_has_custom_checks : AcceptanceTest + { + [Test] + public void Notification_should_be_published_on_the_bus() + { + var context = new MyContext(); + + Scenario.Define(context) + .WithEndpoint(b => b.Given((bus, c) => Subscriptions.OnEndpointSubscribed(s => + { + if (s.SubscriberReturnAddress.Queue.Contains("ExternalProcessor")) + { + c.ExternalProcessorSubscribed = true; + } + }, () => c.ExternalProcessorSubscribed = true)) + .When(c => c.ExternalProcessorSubscribed, bus => + { + bus.Publish(new ServiceControl.Contracts.CustomChecks.CustomCheckSucceeded + { + Category = "Testing", + CustomCheckId = "Success custom check", + OriginatingEndpoint = new EndpointDetails + { + Host = "MyHost", + HostId = Guid.Empty, + Name = "Testing" + }, + SucceededAt = DateTime.Now, + }); + bus.Publish(new ServiceControl.Contracts.CustomChecks.CustomCheckFailed + { + Category = "Testing", + CustomCheckId = "Fail custom check", + OriginatingEndpoint = new EndpointDetails + { + Host = "MyHost", + HostId = Guid.Empty, + Name = "Testing" + }, + FailedAt = DateTime.Now, + FailureReason = "Because I can", + }); + }).AppConfig(PathToAppConfig)) + .WithEndpoint(b => b.Given((bus, c) => + { + bus.Subscribe(); + bus.Subscribe(); + })) + .Done(c => c.CustomCheckFailedReceived && c.CustomCheckFailedReceived) + .Run(); + + Assert.IsTrue(context.CustomCheckFailedReceived); + Assert.IsTrue(context.CustomCheckSucceededReceived); + } + + [Serializable] + public class Subscriptions + { + public static Action, Action> OnEndpointSubscribed = (actionToPerformIfMessageDrivenSubscriptions, actionToPerformIfMessageDrivenSubscriptionsNotRequired) => + { + if (Feature.IsEnabled()) + { + Configure.Instance.Builder.Build().ClientSubscribed += + (sender, args) => + { + actionToPerformIfMessageDrivenSubscriptions(args); + }; + + return; + } + + actionToPerformIfMessageDrivenSubscriptionsNotRequired(); + }; + } + + public class ExternalIntegrationsManagementEndpoint : EndpointConfigurationBuilder + { + public ExternalIntegrationsManagementEndpoint() + { + EndpointSetup(); + } + } + + public class ExternalProcessor : EndpointConfigurationBuilder + { + public ExternalProcessor() + { + EndpointSetup(); + } + + public class CustomCheckSucceededHandler : IHandleMessages + { + public MyContext Context { get; set; } + + public void Handle(CustomCheckSucceeded message) + { + Context.CustomCheckSucceededReceived = true; + } + } + + public class CustomCheckFailedHandler : IHandleMessages + { + public MyContext Context { get; set; } + + public void Handle(CustomCheckFailed message) + { + Context.CustomCheckFailedReceived = true; + } + } + + public class UnicastOverride : IProvideConfiguration + { + public UnicastBusConfig GetConfiguration() + { + var config = new UnicastBusConfig(); + var serviceControlMapping = new MessageEndpointMapping + { + Messages = "ServiceControl.Contracts", + Endpoint = "Particular.ServiceControl" + }; + config.MessageEndpointMappings.Add(serviceControlMapping); + return config; + } + } + } + + public class MyContext : ScenarioContext + { + public bool CustomCheckSucceededReceived { get; set; } + public bool CustomCheckFailedReceived { get; set; } + public bool ExternalProcessorSubscribed { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj b/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj index 5c218c0aa3..60b7e852fe 100644 --- a/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj +++ b/src/ServiceControl.AcceptanceTests/ServiceControl.AcceptanceTests.csproj @@ -114,7 +114,7 @@ False - ..\packages\ServiceControl.Contracts.1.0.0\lib\net45\ServiceControl.Contracts.dll + ..\packages\ServiceControl.Contracts.1.1.0\lib\net45\ServiceControl.Contracts.dll False @@ -167,7 +167,7 @@ - + @@ -180,7 +180,8 @@ - + + diff --git a/src/ServiceControl.AcceptanceTests/packages.config b/src/ServiceControl.AcceptanceTests/packages.config index 20e9bb9490..f6614b1ca0 100644 --- a/src/ServiceControl.AcceptanceTests/packages.config +++ b/src/ServiceControl.AcceptanceTests/packages.config @@ -18,7 +18,7 @@ - + diff --git a/src/ServiceControl.Install.CustomActions/CustomAction.cs b/src/ServiceControl.Install.CustomActions/CustomAction.cs index d555b4f252..b162b9f295 100644 --- a/src/ServiceControl.Install.CustomActions/CustomAction.cs +++ b/src/ServiceControl.Install.CustomActions/CustomAction.cs @@ -1,47 +1,115 @@ namespace ServiceControl.Install.CustomActions { using System; + using System.IO; + using System.Linq; + using System.Xml.Linq; + using System.Xml.XPath; using Microsoft.Deployment.WindowsInstaller; + using Microsoft.Win32; public class CustomActions { /// /// This custom action will test if a port number is in the range 0 to 49152. Sets VALID_PORT to TRUE/FALSE /// - [CustomAction()] + [CustomAction] public static ActionResult CheckValidPort(Session session) + { + if (!session.CustomActionData.ContainsKey("PORT")) + { + Log(session, "CheckValidPort custom action requires a port variable be passed to it in the from PORT=xxxx"); + return ActionResult.Failure; + } + var port = session.CustomActionData["PORT"]; + UInt16 portNumber; + if (UInt16.TryParse(port, out portNumber)) + { + // Port number 49152 and above should not be used http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + if (portNumber < 49152) + { + session.Set("VALID_PORT", "TRUE"); + return ActionResult.Success; + } + } + session.Set("VALID_PORT", "FALSE"); + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult ReadForwardAuditMessagesFromConfig(Session session) { try { - Log(session, "Start custom action CheckValidPort"); - if (!session.CustomActionData.ContainsKey("PORT")) + if (session.CustomActionData.Keys.Count != 1) { - Log(session, "CheckValidPort custom action requires a port variable be passed to it in the from PORT=xxxx"); + Log(session, "ReadForwardAuditMessagesFromConfig custom action requires a single property name to be passed in the CustomActionData. The result will passed to that property"); return ActionResult.Failure; } - var port = session.CustomActionData["PORT"]; - UInt16 portNumber; - if (UInt16.TryParse(port, out portNumber)) + + var outputProperty = session.CustomActionData.Keys.First(); + + const string ServiceControlRegKey = @"SOFTWARE\ParticularSoftware\ServiceControl"; + var targetPath = session.Get("APPDIR"); + var configPath = Path.Combine(targetPath, @"ServiceControl.exe.config"); + var entryValue = "null"; + + // Try to get value from existing config + if (File.Exists(configPath)) { - // Port numbder 49152 and above should not be used http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml - if (portNumber < 49152) + var configDoc = XDocument.Load(configPath); + var entry = configDoc.XPathSelectElement(@"/configuration/appSettings/add[@key='ServiceControl/ForwardAuditMessages']"); + entryValue = (entry != null) ? entry.Attribute("value").Value : "null"; + } + + // Fallback to getting value from registry. + if (!String.IsNullOrWhiteSpace(entryValue)) + { + var key = Registry.LocalMachine.OpenSubKey(ServiceControlRegKey, RegistryKeyPermissionCheck.Default); + if (key != null) { - session.Set("VALID_PORT", "TRUE"); - return ActionResult.Success; + entryValue = (string) key.GetValue("ForwardAuditMessages", "null"); } } - session.Set("VALID_PORT", "FALSE"); + + entryValue = entryValue.ToLower(); + switch (entryValue) + { + case "true" : + case "false" : + session.Set(outputProperty, entryValue); + break; + default: + session.Set(outputProperty,"null"); + break; + } return ActionResult.Success; } - finally + catch (Exception ex) { - Log(session, "End custom action CheckValidPort"); + Log(session, "ReadForwardAuditMessagesFromConfig failed - {0}", ex); + return ActionResult.Failure; } } - static void Log(Session session, string message) + [CustomAction] + public static ActionResult ValidateForwardAuditMessages(Session session) + { + var forwardAuditMessages = session.Get("FORWARDAUDITMESSAGES"); + switch (forwardAuditMessages) + { + case "true": + case "false": + return ActionResult.Success; + } + Log(session, "A required settings has not been provided. ForwardAuditMessages must be explicitly set to true or false when installing via unattended mode. e.g. 'Particular.ServiceControl.exe /quiet ForwardAuditMessages=false'"); + return ActionResult.Failure; + + } + + static void Log(Session session, string message, params object[] args) { - LogAction(session, message); + LogAction(session, string.Format(message, args)); } public static Action LogAction = (s, m) => s.Log(m); diff --git a/src/ServiceControl.IntegrationDemo/ServiceControl.IntegrationDemo.csproj b/src/ServiceControl.IntegrationDemo/ServiceControl.IntegrationDemo.csproj index 34545b9be7..03ba1559e3 100644 --- a/src/ServiceControl.IntegrationDemo/ServiceControl.IntegrationDemo.csproj +++ b/src/ServiceControl.IntegrationDemo/ServiceControl.IntegrationDemo.csproj @@ -47,7 +47,7 @@ False - ..\packages\ServiceControl.Contracts.1.0.0\lib\net45\ServiceControl.Contracts.dll + ..\packages\ServiceControl.Contracts.1.1.0\lib\net45\ServiceControl.Contracts.dll diff --git a/src/ServiceControl.IntegrationDemo/packages.config b/src/ServiceControl.IntegrationDemo/packages.config index 802e6629d0..4be01563f7 100644 --- a/src/ServiceControl.IntegrationDemo/packages.config +++ b/src/ServiceControl.IntegrationDemo/packages.config @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/AuditImport/AuditImportTests.cs b/src/ServiceControl.UnitTests/AuditImport/AuditImportTests.cs new file mode 100644 index 0000000000..8741a5a204 --- /dev/null +++ b/src/ServiceControl.UnitTests/AuditImport/AuditImportTests.cs @@ -0,0 +1,26 @@ +namespace ServiceControl.UnitTests.AuditImport +{ + using System.Messaging; + using NServiceBus; + using NServiceBus.Transports.Msmq; + using NUnit.Framework; + + [TestFixture] + public class AuditImportTests + { + [Test, Explicit] + public void SendAMessageWithInvalidHeadersToTheAuditQueue() + { + // Generate a bad msg to test MSMQ Audit Queue Importer + // This message should fail to parse to a transport message because of the bad header and so should end up in the Particular.ServiceControl.Errors queue + var q = new MessageQueue(MsmqUtilities.GetFullPath(Address.Parse("audit")), false, true, QueueAccessMode.Send); + using (var tx = new MessageQueueTransaction()) + { + tx.Begin(); + var message = new Message("Message with invalid headers"){Extension = new byte[]{1}}; + q.Send(message, tx); + tx.Commit(); + } + } + } +} diff --git a/src/ServiceControl.UnitTests/CompositeViews/FailedMessageTest.cs b/src/ServiceControl.UnitTests/CompositeViews/FailedMessageTest.cs index e6b93a35da..f795a7e195 100644 --- a/src/ServiceControl.UnitTests/CompositeViews/FailedMessageTest.cs +++ b/src/ServiceControl.UnitTests/CompositeViews/FailedMessageTest.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading; - using Infrastructure.RavenDB; using MessageFailures; using NUnit.Framework; using Raven.Client; using ServiceControl.MessageFailures.Api; [TestFixture] - public class FailedMessagesTests : TestWithRavenDB + public class FailedMessagesTests { [Test] public void Should_allow_errors_with_no_metadata() diff --git a/src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs b/src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs index 09aa78d95e..664f488bdb 100644 --- a/src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs +++ b/src/ServiceControl.UnitTests/CompositeViews/MessagesViewTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using Contracts.Operations; - using Infrastructure.RavenDB; using MessageAuditing; using MessageFailures; using NUnit.Framework; @@ -13,7 +12,7 @@ using ServiceControl.CompositeViews.Messages; [TestFixture] - public class MessagesViewTests : TestWithRavenDB + public class MessagesViewTests { [Test] public void Filter_out_system_messages() @@ -82,7 +81,7 @@ public void Order_by_critical_time() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); using (var session = documentStore.OpenSession()) { @@ -93,12 +92,12 @@ public void Order_by_critical_time() .First(); Assert.AreEqual("1", firstByCriticalTime.Id); - var firstByCriticalTimeDesc = session.Query() + var firstByCriticalTimeDescription = session.Query() .OrderByDescending(x => x.CriticalTime) .Where(x => x.CriticalTime != null) .AsProjection() .First(); - Assert.AreEqual("2", firstByCriticalTimeDesc.Id); + Assert.AreEqual("2", firstByCriticalTimeDescription.Id); } } [Test] @@ -125,7 +124,7 @@ public void Order_by_time_sent() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); using (var session = documentStore.OpenSession()) { @@ -135,11 +134,11 @@ public void Order_by_time_sent() .First(); Assert.AreEqual("3", firstByTimeSent.Id); - var firstByTimeSentDesc = session.Query() + var firstByTimeSentDescription = session.Query() .OrderByDescending(x => x.TimeSent) .OfType() .First(); - Assert.AreEqual("1", firstByTimeSentDesc.Id); + Assert.AreEqual("1", firstByTimeSentDescription.Id); } } @@ -161,7 +160,7 @@ public void Correct_status_for_repeated_errors() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); using (var session = documentStore.OpenSession()) { diff --git a/src/ServiceControl.UnitTests/Expiration/CustomExpirationBundleTests.cs b/src/ServiceControl.UnitTests/Expiration/CustomExpirationBundleTests.cs index 0525e4717b..3b9d9f07b4 100644 --- a/src/ServiceControl.UnitTests/Expiration/CustomExpirationBundleTests.cs +++ b/src/ServiceControl.UnitTests/Expiration/CustomExpirationBundleTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; - using Infrastructure.RavenDB; using MessageAuditing; using MessageFailures; using NUnit.Framework; @@ -13,7 +12,7 @@ using ServiceControl.Infrastructure.RavenDB.Expiration; [TestFixture] - public class CustomExpirationBundleTests : TestWithRavenDB + public class CustomExpirationBundleTests { [Test] public void Processed_messages_are_being_expired() @@ -38,7 +37,7 @@ public void Processed_messages_are_being_expired() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); Thread.Sleep(Settings.ExpirationProcessTimerInSeconds * 1000 * 2); using (var session = documentStore.OpenSession()) @@ -85,7 +84,7 @@ public void Many_processed_messages_are_being_expired() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); Thread.Sleep(Settings.ExpirationProcessTimerInSeconds * 1000 * 10); using (var session = documentStore.OpenSession()) { @@ -125,7 +124,7 @@ public void Only_processed_messages_are_being_expired() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); Thread.Sleep(Settings.ExpirationProcessTimerInSeconds * 1000 * 2); using (var session = documentStore.OpenSession()) @@ -155,7 +154,7 @@ public void Recent_processed_messages_are_not_being_expired() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); Thread.Sleep(Settings.ExpirationProcessTimerInSeconds * 1000 * 2); using (var session = documentStore.OpenSession()) @@ -191,7 +190,7 @@ public void Errors_are_not_being_expired() session.SaveChanges(); } - WaitForIndexing(documentStore); + documentStore.WaitForIndexing(); Thread.Sleep(Settings.ExpirationProcessTimerInSeconds * 1000 * 2); using (var session = documentStore.OpenSession()) diff --git a/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs b/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs new file mode 100644 index 0000000000..bc52829596 --- /dev/null +++ b/src/ServiceControl.UnitTests/Expiration/PeriodicExecutorTests.cs @@ -0,0 +1,76 @@ +namespace ServiceControl.UnitTests.Expiration +{ + using System; + using System.Threading; + using NUnit.Framework; + using ServiceControl.Infrastructure.RavenDB.Expiration; + + [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(() => + { + 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(CancellationToken.None); + 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(() => + { + if (first) + { + first = false; + throw new Exception(); + } + success = true; + @event.Set(); + }, TimeSpan.FromSeconds(1)); + executor.Start(true); + @event.Wait(); + executor.Stop(CancellationToken.None); + Assert.IsTrue(success); + } + + [Test] + public void Can_shutdown_while_waiting() + { + var @event = new ManualResetEventSlim(false); + var executor = new PeriodicExecutor(@event.Set, TimeSpan.FromSeconds(10000)); + executor.Start(false); + @event.Wait(); + Thread.Sleep(1000); + executor.Stop(CancellationToken.None); + Assert.Pass(); + } + } +} diff --git a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/TestWithRavenDB.cs b/src/ServiceControl.UnitTests/Infrastructure/RavenDB/TestWithRavenDB.cs deleted file mode 100644 index 4af228b1eb..0000000000 --- a/src/ServiceControl.UnitTests/Infrastructure/RavenDB/TestWithRavenDB.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace ServiceControl.UnitTests.Infrastructure.RavenDB -{ - using System.Diagnostics; - using System.Threading; - using NUnit.Framework; - using Raven.Client; - using Raven.Client.Embedded; - using Raven.Database.Server; - using Raven.Json.Linq; - - [TestFixture] - public abstract class TestWithRavenDB - { - public static void WaitForIndexing(IDocumentStore store, string db = null, TimeSpan? timeout = null) - { - var databaseCommands = store.DatabaseCommands; - if (db != null) - databaseCommands = databaseCommands.ForDatabase(db); - Assert.True(SpinWait.SpinUntil(() => databaseCommands.GetStatistics().StaleIndexes.Length == 0, timeout ?? TimeSpan.FromSeconds(10))); - } - - public static void WaitForUserToContinueTheTest(EmbeddableDocumentStore documentStore, bool debug = true) - { - if (debug && Debugger.IsAttached == false) - return; - - documentStore.SetStudioConfigToAllowSingleDb(); - - documentStore.DatabaseCommands.Put("Pls Delete Me", null, - - RavenJObject.FromObject(new { StackTrace = new StackTrace(true) }), - new RavenJObject()); - - documentStore.Configuration.AnonymousUserAccessMode = AnonymousUserAccessMode.Admin; - using (var server = new HttpServer(documentStore.Configuration, documentStore.DocumentDatabase)) - { - server.StartListening(); - Process.Start(documentStore.Configuration.ServerUrl); // start the server - - do - { - Thread.Sleep(100); - } while (documentStore.DatabaseCommands.Get("Pls Delete Me") != null && (debug == false || Debugger.IsAttached)); - } - } - } -} diff --git a/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs b/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs new file mode 100644 index 0000000000..37daf1a96c --- /dev/null +++ b/src/ServiceControl.UnitTests/RavenIndexAwaiter.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading; +using NUnit.Framework; +using Raven.Client; + +public static class RavenIndexAwaiter +{ + public static void WaitForIndexing(this IDocumentStore store) + { + var databaseCommands = store.DatabaseCommands; + Assert.True(SpinWait.SpinUntil(() => databaseCommands.GetStatistics().StaleIndexes.Length == 0, TimeSpan.FromSeconds(10))); + } + +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj index d7a7be25fb..eb7cb2246c 100644 --- a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj +++ b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj @@ -74,13 +74,14 @@ False - ..\packages\ServiceControl.Contracts.1.0.0\lib\net45\ServiceControl.Contracts.dll + ..\packages\ServiceControl.Contracts.1.1.0\lib\net45\ServiceControl.Contracts.dll + False ..\packages\System.Spatial.5.6.2\lib\net40\System.Spatial.dll @@ -89,14 +90,16 @@ + + - + diff --git a/src/ServiceControl.UnitTests/packages.config b/src/ServiceControl.UnitTests/packages.config index 8c5257959e..ae64cdeafd 100644 --- a/src/ServiceControl.UnitTests/packages.config +++ b/src/ServiceControl.UnitTests/packages.config @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/src/ServiceControl.sln.DotSettings b/src/ServiceControl.sln.DotSettings index 411e1127cd..7244d3c82c 100644 --- a/src/ServiceControl.sln.DotSettings +++ b/src/ServiceControl.sln.DotSettings @@ -125,6 +125,16 @@ ERROR ERROR ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR <?xml version="1.0" encoding="utf-16"?><Profile name="Format My Code Using &quot;Particular&quot; conventions"><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSUseVar><BehavourStyle>CAN_CHANGE_TO_IMPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_IMPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_IMPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSReorderTypeMembers>True</CSReorderTypeMembers><JsInsertSemicolon>True</JsInsertSemicolon><JsReformatCode>True</JsReformatCode><CssReformatCode>True</CssReformatCode><CSArrangeThisQualifier>True</CSArrangeThisQualifier><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><HtmlReformatCode>True</HtmlReformatCode><CSShortenReferences>True</CSShortenReferences><CSharpFormatDocComments>True</CSharpFormatDocComments><CssAlphabetizeProperties>True</CssAlphabetizeProperties></Profile> Default: Reformat Code Format My Code Using "Particular" conventions @@ -144,6 +154,167 @@ CHOP_ALWAYS True True + <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <HasAttribute Name="System.Runtime.InteropServices.StructLayoutAttribute" /> + </Or> + </TypePattern.Match> + </TypePattern> + + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="true" /> + <HasAttribute Name="NUnit.Framework.TestCaseFixtureAttribute" Inherited="true" /> + </And> + </TypePattern.Match> + + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="true" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="true" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="true" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="true" /> + </Or> + </And> + </Entry.Match> + </Entry> + + <Entry DisplayName="All other members" /> + + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" Inherited="false" /> + </And> + </Entry.Match> + + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + + <TypePattern DisplayName="Default Pattern"> + <Entry DisplayName="Public Delegates" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Delegate" /> + </And> + </Entry.Match> + + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="Public Enums" Priority="100"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Enum" /> + </And> + </Entry.Match> + + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="Static Fields and Constants"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Kind Is="Field" /> + <Static /> + </And> + </Or> + </Entry.Match> + + <Entry.SortBy> + <Kind> + <Kind.Order> + <DeclarationKind>Constant</DeclarationKind> + <DeclarationKind>Field</DeclarationKind> + </Kind.Order> + </Kind> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="Fields"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + + <Entry.SortBy> + <Static/> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="Properties, Indexers"> + <Entry.Match> + <Or> + <Kind Is="Property" /> + <Kind Is="Indexer" /> + </Or> + </Entry.Match> + </Entry> + + <Entry DisplayName="Interface Implementations" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Member" /> + <ImplementsInterface /> + </And> + </Entry.Match> + + <Entry.SortBy> + <ImplementsInterface Immediate="true" /> + </Entry.SortBy> + </Entry> + + <Entry DisplayName="All other members" /> + + <Entry DisplayName="Nested Types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> +</Patterns> + <?xml version="1.0" encoding="utf-8" ?> <!-- @@ -408,7 +579,9 @@ II.2.12 <HandlesEvent /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True True + True True diff --git a/src/ServiceControl/Bootstrapper.cs b/src/ServiceControl/Bootstrapper.cs index 5c4046d630..330f665dce 100644 --- a/src/ServiceControl/Bootstrapper.cs +++ b/src/ServiceControl/Bootstrapper.cs @@ -133,11 +133,12 @@ static void ConfigureLogging() UseDefaultRowHighlightingRules = true, }; - nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Warn, fileTarget)); - nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Warn, consoleTarget) { Final = true }); - nlogConfig.LoggingRules.Add(new LoggingRule("Particular.ServiceControl.Licensing.*", LogLevel.Info, fileTarget)); - nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.Licensing.*", LogLevel.Error, fileTarget)); + nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Error, fileTarget)); + nlogConfig.LoggingRules.Add(new LoggingRule("Raven.*", LogLevel.Error, consoleTarget) { Final = true }); + nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.RavenDB.Persistence.*", LogLevel.Error, fileTarget) { Final = true }); + nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.Licensing.*", LogLevel.Error, fileTarget) { Final = true }); nlogConfig.LoggingRules.Add(new LoggingRule("NServiceBus.Licensing.*", LogLevel.Error, consoleTarget) { Final = true }); + nlogConfig.LoggingRules.Add(new LoggingRule("Particular.ServiceControl.Licensing.*", LogLevel.Info, fileTarget)); nlogConfig.LoggingRules.Add(new LoggingRule("*", LogLevel.Warn, fileTarget)); nlogConfig.LoggingRules.Add(new LoggingRule("*", LogLevel.Info, consoleTarget)); diff --git a/src/ServiceControl/ConfigTransportConfig.cs b/src/ServiceControl/ConfigTransportConfig.cs index f73a11a9aa..d8b3c4c7de 100644 --- a/src/ServiceControl/ConfigTransportConfig.cs +++ b/src/ServiceControl/ConfigTransportConfig.cs @@ -2,6 +2,7 @@ namespace Particular.ServiceControl { using NServiceBus.Config; using NServiceBus.Config.ConfigurationSource; + using ServiceBus.Management.Infrastructure.Settings; class ConfigTransportConfig : IProvideConfiguration { @@ -9,7 +10,7 @@ public TransportConfig GetConfiguration() { return new TransportConfig { - + MaximumMessageThroughputPerSecond = Settings.MaximumMessageThroughputPerSecond, MaximumConcurrencyLevel = 10, MaxRetries = 3, }; diff --git a/src/ServiceControl/ExternalIntegrations/CustomCheckFailedPublisher.cs b/src/ServiceControl/ExternalIntegrations/CustomCheckFailedPublisher.cs new file mode 100644 index 0000000000..97991fdcde --- /dev/null +++ b/src/ServiceControl/ExternalIntegrations/CustomCheckFailedPublisher.cs @@ -0,0 +1,51 @@ +namespace ServiceControl.ExternalIntegrations +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Raven.Client; + using CustomCheckFailed = ServiceControl.Contracts.CustomCheckFailed; + + + public class CustomCheckFailedPublisher : EventPublisher + { + protected override DispatchContext CreateDispatchRequest(Contracts.CustomChecks.CustomCheckFailed @event) + { + return new DispatchContext + { + EndpointHost = @event.OriginatingEndpoint.Host, + EndpointHostId = @event.OriginatingEndpoint.HostId, + EndpointName = @event.OriginatingEndpoint.Name, + FailedAt = @event.FailedAt, + Category = @event.Category, + FailureReason = @event.FailureReason, + CustomCheckId = @event.CustomCheckId, + }; + } + + protected override IEnumerable PublishEvents(IEnumerable contexts, IDocumentSession session) + { + return contexts.Select(r => new CustomCheckFailed + { + FailedAt = r.FailedAt, + Category = r.Category, + FailureReason = r.FailureReason, + CustomCheckId = r.CustomCheckId, + Host = r.EndpointHost, + HostId = r.EndpointHostId, + EndpointName = r.EndpointName + }); + } + + public class DispatchContext + { + public string EndpointName { get; set; } + public Guid EndpointHostId { get; set; } + public string EndpointHost { get; set; } + public string CustomCheckId { get; set; } + public string Category { get; set; } + public string FailureReason { get; set; } + public DateTime FailedAt { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/CustomCheckSucceededPublisher.cs b/src/ServiceControl/ExternalIntegrations/CustomCheckSucceededPublisher.cs new file mode 100644 index 0000000000..cb0ae81f29 --- /dev/null +++ b/src/ServiceControl/ExternalIntegrations/CustomCheckSucceededPublisher.cs @@ -0,0 +1,47 @@ +namespace ServiceControl.ExternalIntegrations +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Raven.Client; + using ServiceControl.Contracts; + + public class CustomCheckSucceededPublisher : EventPublisher + { + protected override DispatchContext CreateDispatchRequest(Contracts.CustomChecks.CustomCheckSucceeded @event) + { + return new DispatchContext + { + EndpointHost = @event.OriginatingEndpoint.Host, + EndpointHostId = @event.OriginatingEndpoint.HostId, + EndpointName = @event.OriginatingEndpoint.Name, + SucceededAt = @event.SucceededAt, + Category = @event.Category, + CustomCheckId = @event.CustomCheckId, + }; + } + + protected override IEnumerable PublishEvents(IEnumerable contexts, IDocumentSession session) + { + return contexts.Select(r => new CustomCheckSucceeded + { + SucceededAt = r.SucceededAt, + Category = r.Category, + CustomCheckId = r.CustomCheckId, + Host = r.EndpointHost, + HostId = r.EndpointHostId, + EndpointName = r.EndpointName + }); + } + + public class DispatchContext + { + public string EndpointName { get; set; } + public Guid EndpointHostId { get; set; } + public string EndpointHost { get; set; } + public string CustomCheckId { get; set; } + public string Category { get; set; } + public DateTime SucceededAt { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/ExternalIntegrationsInitializer.cs b/src/ServiceControl/ExternalIntegrations/ExternalIntegrationsInitializer.cs index dad847607f..bec135c183 100644 --- a/src/ServiceControl/ExternalIntegrations/ExternalIntegrationsInitializer.cs +++ b/src/ServiceControl/ExternalIntegrations/ExternalIntegrationsInitializer.cs @@ -9,6 +9,8 @@ public void Init() Configure.Component(DependencyLifecycle.SingleInstance); Configure.Component(DependencyLifecycle.SingleInstance); Configure.Component(DependencyLifecycle.SingleInstance); + Configure.Component(DependencyLifecycle.SingleInstance); + Configure.Component(DependencyLifecycle.SingleInstance); } } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/CheckMandatoryInstallOptionsCommand.cs b/src/ServiceControl/Hosting/Commands/CheckMandatoryInstallOptionsCommand.cs new file mode 100644 index 0000000000..0208e8da41 --- /dev/null +++ b/src/ServiceControl/Hosting/Commands/CheckMandatoryInstallOptionsCommand.cs @@ -0,0 +1,32 @@ +namespace Particular.ServiceControl.Commands +{ + using System; + using Particular.ServiceControl.Hosting; + using ServiceBus.Management.Infrastructure.Settings; + + internal class CheckMandatoryInstallOptionsCommand : AbstractCommand + { + public override void Execute(HostArguments args) + { + if (!Settings.ForwardAuditMessages.HasValue) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Installation Aborted!"); + Console.ResetColor(); + Console.WriteLine(@" +This installation requires addition information: + +ForwardAuditMessages must be explicitly set to true or false + +e.g. + + ServiceControl.exe -install -d=""ServiceControl/ForwardAuditMessages==true"" + +For more information go to the documentation site - http://docs.particular.net +and search for 'ServiceControl ForwardAuditMessages' +"); + Environment.Exit(1); + } + } + } +} diff --git a/src/ServiceControl/Hosting/Help.txt b/src/ServiceControl/Hosting/Help.txt index b4e3a487b3..95c98f88fb 100644 --- a/src/ServiceControl/Hosting/Help.txt +++ b/src/ServiceControl/Hosting/Help.txt @@ -4,7 +4,7 @@ USAGE: ServiceControl.exe --start [options] ServiceControl.exe --stop [options] ServiceControl.exe --restart [options] - ServiceControl.exe --install [options] + ServiceControl.exe --install -d="ForwardAuditQueue==[true|false]" [options] ServiceControl.exe --uninstall [options] ServiceControl.exe --maintenance [options] @@ -29,6 +29,10 @@ This mode is only supported when run interactively. CONFIGURATION OPTIONS: +ServiceControl/ForwardAuditMessages bool (true/false) +Use this setting to configure whether processed audit messages are forwarded to another queue or not. +This is a mandatory setting and does not have a default value. + ServiceControl/LogPath string The path for the ServiceControl logs. Default: %LOCALAPPDATA%\Particular\ServiceControl\logs @@ -44,11 +48,8 @@ The virtual directory to bind the embedded http server to, modify if you want to ServiceControl/HeartbeatGracePeriod timespan The period that defines whether an endpoint is considered alive or not. Default: 00:00:40 (40 secs) -ServiceControl/ForwardAuditMessages bool (true/false) -Use this setting to configure whether processed audit messages are forwarded to anothe queue or not. Default false - ServiceControl/ExpirationProcessTimerInSeconds int -The number of seconds to wait between checking for expired messages, Default 60 (1 minute) +The number of seconds to wait between checking for expired messages, Default 600 (10 minutes) ServiceControl/HoursToKeepMessagesBeforeExpiring int The number of hours to keep a message for before it is deleted, Default 720 (30 days) @@ -79,14 +80,17 @@ EXAMPLES: -d="ServiceControl/LogPath==c:\MyLogs Files" -d=ServiceControl/Hostname==sc.myspecialdomain.com -d=ServiceControl/Port==80 - + ServiceControl.exe --install --serviceName="MyServiceControl" --displayName="My ServiceControl" --description="Service for monitoring" --username="corp\serviceuser" --password="p@ssw0rd!" + --d=ServiceControl/ForwardAuditMessages==true --d=ServiceControl/Hostname==localhost --d=ServiceControl/Port==33334 ServiceControl.exe --uninstall --serviceName="MyServiceControl" + + diff --git a/src/ServiceControl/Hosting/HostArguments.cs b/src/ServiceControl/Hosting/HostArguments.cs index dfb065d560..881306f84d 100644 --- a/src/ServiceControl/Hosting/HostArguments.cs +++ b/src/ServiceControl/Hosting/HostArguments.cs @@ -15,7 +15,6 @@ public class HostArguments public HostArguments(string[] args) { var executionMode = ExecutionMode.Run; - commands = new List { typeof(RunCommand) }; startMode = StartMode.Automatic; ServiceName = "Particular.ServiceControl"; @@ -83,20 +82,20 @@ public HostArguments(string[] args) }, }; - maintenanceOptions = new OptionSet - { - { - "m|maint|maintenance", - @"Run RavenDB only - use for DB maintenance", - s => { - commands = new List - { - typeof(MaintCommand) - }; - executionMode = ExecutionMode.Maintenance; - } - } - }; + var maintenanceOptions = new OptionSet + { + { + "m|maint|maintenance", + @"Run RavenDB only - use for DB maintenance", + s => { + commands = new List + { + typeof(MaintCommand) + }; + executionMode = ExecutionMode.Maintenance; + } + } + }; uninstallOptions = new OptionSet @@ -135,6 +134,7 @@ public HostArguments(string[] args) commands = new List { typeof(WriteOptionsCommand), + typeof(CheckMandatoryInstallOptionsCommand), typeof(RunBootstrapperAndNServiceBusInstallers), typeof(InstallCommand) }; @@ -299,7 +299,6 @@ public void PrintUsage() readonly OptionSet installOptions; readonly OptionSet uninstallOptions; readonly OptionSet defaultOptions; - readonly OptionSet maintenanceOptions; List commands; Dictionary options = new Dictionary(); StartMode startMode; diff --git a/src/ServiceControl/Infrastructure/Installers/AuditLogQueueInstaller.cs b/src/ServiceControl/Infrastructure/Installers/AuditLogQueueInstaller.cs index a27565c1a3..83a959ce09 100644 --- a/src/ServiceControl/Infrastructure/Installers/AuditLogQueueInstaller.cs +++ b/src/ServiceControl/Infrastructure/Installers/AuditLogQueueInstaller.cs @@ -13,7 +13,7 @@ public Address Address public bool IsDisabled { - get { return !Settings.ForwardAuditMessages; } + get { return Settings.AuditLogQueue == Address.Undefined; } } } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs b/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs index f85446ccca..ca4afd0576 100644 --- a/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs +++ b/src/ServiceControl/Infrastructure/RavenDB/Expiration/ExpiredDocumentsCleaner.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Infrastructure.RavenDB.Expiration { + using System; using System.Collections.Generic; using System.ComponentModel.Composition; @@ -16,38 +17,40 @@ using Raven.Database.Plugins; using ServiceBus.Management.Infrastructure.Settings; + [InheritedExport(typeof(IStartupTask))] [ExportMetadata("Bundle", "customDocumentExpiration")] public class ExpiredDocumentsCleaner : IStartupTask, IDisposable { ILog logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleaner)); - Timer timer; + PeriodicExecutor timer; DocumentDatabase Database { get; set; } string indexName; int deleteFrequencyInSeconds; int deletionBatchSize; - + public void Execute(DocumentDatabase database) { Database = database; indexName = new ExpiryProcessedMessageIndex().IndexName; - + deletionBatchSize = Settings.ExpirationProcessBatchSize; deleteFrequencyInSeconds = Settings.ExpirationProcessTimerInSeconds; - + if (deleteFrequencyInSeconds == 0) { return; } - logger.Info("Expired Documents every {0} seconds",deleteFrequencyInSeconds); + logger.Info("Expired Documents every {0} seconds", deleteFrequencyInSeconds); logger.Info("Deletion Batch Size: {0}", deletionBatchSize); logger.Info("Retention Period: {0}", Settings.HoursToKeepMessagesBeforeExpiring); - timer = new Timer(TimerCallback, null, TimeSpan.FromSeconds(deleteFrequencyInSeconds), Timeout.InfiniteTimeSpan); + timer = new PeriodicExecutor(Delete,TimeSpan.FromSeconds(deleteFrequencyInSeconds)); + timer.Start(true); } - void TimerCallback(object state) + void Delete() { var currentTime = SystemTime.UtcNow; var currentExpiryThresholdTime = currentTime.AddHours(-Settings.HoursToKeepMessagesBeforeExpiring); @@ -87,7 +90,9 @@ void TimerCallback(object state) { var documentWithCurrentThresholdTimeReached = false; var items = new List(deletionBatchSize); - Database.Query(indexName, query, CancellationTokenSource.CreateLinkedTokenSource(Database.WorkContext.CancellationToken, cts.Token).Token, + try + { + Database.Query(indexName, query, CancellationTokenSource.CreateLinkedTokenSource(Database.WorkContext.CancellationToken, cts.Token).Token, null, doc => { @@ -112,10 +117,18 @@ void TimerCallback(object state) }); } }); - docsToExpire += items.Count; - var results = Database.Batch(items.ToArray()); - deletionCount = results.Count(x => x.Deleted == true); - items.Clear(); + } + catch (OperationCanceledException) + { + //Ignore + } + + logger.Debug("Batching deletion of {0} documents.",items.Count); + + docsToExpire += items.Count; + var results = Database.Batch(items.ToArray()); + deletionCount = results.Count(x => x.Deleted == true); + items.Clear(); } if (docsToExpire == 0) @@ -127,25 +140,10 @@ void TimerCallback(object state) logger.Debug("Deleted {0} out of {1} expired documents batch - Execution time:{2}ms", deletionCount, docsToExpire, stopwatch.ElapsedMilliseconds); } } - catch (OperationCanceledException) - { - //Ignore - } catch (Exception e) { logger.ErrorException("Error when trying to find expired documents", e); } - finally - { - try - { - timer.Change(TimeSpan.FromSeconds(deleteFrequencyInSeconds), Timeout.InfiniteTimeSpan); - } - catch (ObjectDisposedException) - { - //Ignore - } - } } /// @@ -156,12 +154,7 @@ public void Dispose() { if (timer != null) { - using (var waitHandle = new ManualResetEvent(false)) - { - timer.Dispose(waitHandle); - - waitHandle.WaitOne(); - } + timer.Stop(CancellationToken.None); } } } diff --git a/src/ServiceControl/Infrastructure/RavenDB/Expiration/PeriodicExecutor.cs b/src/ServiceControl/Infrastructure/RavenDB/Expiration/PeriodicExecutor.cs new file mode 100644 index 0000000000..1e581ee178 --- /dev/null +++ b/src/ServiceControl/Infrastructure/RavenDB/Expiration/PeriodicExecutor.cs @@ -0,0 +1,102 @@ +namespace ServiceControl.Infrastructure.RavenDB.Expiration +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + public class PeriodicExecutor + { + readonly Action action; + readonly TimeSpan period; + DateTime lastStartedAt; + CancellationTokenSource tokenSource; + ManualResetEventSlim resetEvent; + + public PeriodicExecutor(Action action, TimeSpan period) + { + this.action = action; + this.period = period; + } + + public void Start(bool delay) + { + lock (this) + { + if (tokenSource != null) + { + throw new InvalidOperationException("Executor has already been started"); + } + tokenSource = new CancellationTokenSource(); + resetEvent = new ManualResetEventSlim(false); + if (delay) + { + Task.Delay(period, tokenSource.Token).ContinueWith(task => + { + if (task.Status == TaskStatus.RanToCompletion) + { + Trigger(); + } + else + { + resetEvent.Set(); + } + }); + } + else + { + Trigger(); + } + } + } + + public void Stop(CancellationToken token) + { + lock (this) + { + if (tokenSource == null) + { + throw new InvalidOperationException("Executor has not been started"); + } + tokenSource.Cancel(); + resetEvent.Wait(token); + } + } + + void Trigger() + { + Task.Factory.StartNew(() => + { + lastStartedAt = DateTime.Now; + action(); + }) + .ContinueWith(x => + { + if (tokenSource.IsCancellationRequested) + { + resetEvent.Set(); + return; + } + var duration = DateTime.Now - lastStartedAt; + if (duration > period) + { + Trigger(); + } + else + { + Task.Delay(period - duration, tokenSource.Token) + .ContinueWith(task => + { + if (task.Status == TaskStatus.RanToCompletion) + { + Trigger(); + } + else + { + resetEvent.Set(); + } + }); + } + }); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/CheckSettingsOnStartup.cs b/src/ServiceControl/Infrastructure/Settings/CheckSettingsOnStartup.cs new file mode 100644 index 0000000000..9ac1c7c19c --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/CheckSettingsOnStartup.cs @@ -0,0 +1,23 @@ +namespace ServiceControl.Infrastructure.Settings +{ + using NServiceBus; + using NServiceBus.Logging; + + class SettingsCheck : IWantToRunWhenBusStartsAndStops + { + ILog logger = LogManager.GetLogger(typeof(SettingsCheck)); + + public void Start() + { + if (!ServiceBus.Management.Infrastructure.Settings.Settings.ForwardAuditMessages.HasValue) + { + logger.ErrorFormat("The setting ServiceControl/ForwardAuditMessges is not explicitly set. To suppress this error set ServiceControl/ForwardAuditMessges to true or false."); + } + } + + public void Stop() + { + //ignore + } + } +} diff --git a/src/ServiceControl/Infrastructure/Settings/NullableRegistryReader.cs b/src/ServiceControl/Infrastructure/Settings/NullableRegistryReader.cs new file mode 100644 index 0000000000..fb5f29b75b --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/NullableRegistryReader.cs @@ -0,0 +1,78 @@ +namespace ServiceBus.Management.Infrastructure.Settings +{ + using System; + using Microsoft.Win32; + using NServiceBus.Logging; + + /// + /// Wrapper to read registry keys. + /// + /// The type of the key to retrieve + class NullableRegistryReader where T : struct + { + /// + /// Attempts to read the key from the registry. + /// + /// The subkey to target. + /// The name of the value to retrieve. This string is not case-sensitive. + /// The value to return if does not exist. + /// + /// The value associated with , with any embedded environment variables left unexpanded, or + /// if is not found. + /// + public static T? Read(string subKey, string name, T? defaultValue) + { + var regPath = @"SOFTWARE\ParticularSoftware\" + subKey.Replace("/", "\\"); + try + { + if (Environment.Is64BitOperatingSystem) + { + var rootKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + + using (var registryKey = rootKey.OpenSubKey(regPath)) + { + if (registryKey != null) + { + var value = registryKey.GetValue(name); + + if (value != null) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + } + } + + rootKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + + using (var registryKey = rootKey.OpenSubKey(regPath)) + { + if (registryKey != null) + { + return (T)Convert.ChangeType(registryKey.GetValue(name, defaultValue), typeof(T)); + } + } + } + else + { + var rootKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default); + + using (var registryKey = rootKey.OpenSubKey(regPath)) + { + if (registryKey != null) + { + return (T)Convert.ChangeType(registryKey.GetValue(name, defaultValue), typeof(T)); + } + } + } + } + catch (Exception ex) + { + Logger.Warn(string.Format(@"We couldn't read the registry to retrieve the {0}, from '{1}'.", name, regPath), ex); + } + + return defaultValue; + } + + static readonly ILog Logger = LogManager.GetLogger(typeof(NullableRegistryReader)); + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/NullableSettingsReader.cs b/src/ServiceControl/Infrastructure/Settings/NullableSettingsReader.cs new file mode 100644 index 0000000000..7eafa84d70 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/NullableSettingsReader.cs @@ -0,0 +1,25 @@ +namespace ServiceBus.Management.Infrastructure.Settings +{ + using System; + using System.Configuration; + + public class NullableSettingsReader where T : struct + { + public static T? Read(string name) + { + return Read("ServiceControl", name, null); + } + + public static T? Read(string root, string name, T? defaultValue) + { + var fullKey = root + "/" + name; + + if (ConfigurationManager.AppSettings[fullKey] != null) + { + return (T) Convert.ChangeType(ConfigurationManager.AppSettings[fullKey], typeof(T)); + } + + return RegistryReader.Read(root, name, defaultValue); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 6250003147..968f80d3f7 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -146,11 +146,13 @@ public static string LogPath public static int ExternalIntegrationsDispatchingBatchSize = SettingsReader.Read("ExternalIntegrationsDispatchingBatchSize", 100); + public static int MaximumMessageThroughputPerSecond = SettingsReader.Read("MaximumMessageThroughputPerSecond", 350); + public static string DbPath; public static Address ErrorLogQueue; public static Address ErrorQueue; public static Address AuditQueue; - public static bool ForwardAuditMessages = SettingsReader.Read("ForwardAuditMessages"); + public static bool? ForwardAuditMessages = NullableSettingsReader.Read("ForwardAuditMessages"); public static bool CreateIndexSync = SettingsReader.Read("CreateIndexSync"); public static Address AuditLogQueue; diff --git a/src/ServiceControl/Operations/AuditQueueImport.cs b/src/ServiceControl/Operations/AuditQueueImport.cs index d388826889..47fc375e30 100644 --- a/src/ServiceControl/Operations/AuditQueueImport.cs +++ b/src/ServiceControl/Operations/AuditQueueImport.cs @@ -1,7 +1,9 @@ namespace ServiceControl.Operations { using System; + using System.Collections.Generic; using System.IO; + using System.Threading; using Contracts.Operations; using NServiceBus; using NServiceBus.Logging; @@ -57,7 +59,7 @@ void InnerHandle(TransportMessage message) PipelineExecutor.InvokeLogicalMessagePipeline(logicalMessage); } - if (Settings.ForwardAuditMessages) + if (Settings.ForwardAuditMessages == true) { Forwarder.Send(message, Settings.AuditLogQueue); } @@ -65,8 +67,34 @@ void InnerHandle(TransportMessage message) public void Start() { - Logger.InfoFormat("Audit import is now started, feeding audit messages from: {0}", InputAddress); + if (!TerminateIfForwardingIsEnabledButQueueNotWritable()) + { + Logger.InfoFormat("Audit import is now started, feeding audit messages from: {0}", InputAddress); + } + } + + bool TerminateIfForwardingIsEnabledButQueueNotWritable() + { + if (Settings.ForwardAuditMessages != true) + { + return false; + } + + try + { + //Send a message to test the forwarding queue + var testMessage = new TransportMessage(Guid.Empty.ToString("N"), new Dictionary()); + Forwarder.Send(testMessage, Settings.AuditLogQueue); + return false; + } + catch (Exception messageForwardingException) + { + //This call to RaiseCriticalError has to be on a seperate thread otherwise it deadlocks and doesn't stop correctly. + ThreadPool.QueueUserWorkItem(state => Configure.Instance.RaiseCriticalError(string.Format("Audit Import cannot start"), messageForwardingException)); + return true; + } } + public void Stop() { diff --git a/src/ServiceControl/Operations/ErrorQueueImport.cs b/src/ServiceControl/Operations/ErrorQueueImport.cs index f94c64b85d..b6a9742d8e 100644 --- a/src/ServiceControl/Operations/ErrorQueueImport.cs +++ b/src/ServiceControl/Operations/ErrorQueueImport.cs @@ -1,7 +1,9 @@ namespace ServiceControl.Operations { using System; + using System.Collections.Generic; using System.IO; + using System.Threading; using Contracts.Operations; using NServiceBus; using NServiceBus.Logging; @@ -54,6 +56,11 @@ void InnerHandle(TransportMessage message) public void Start() { + + if (TerminateIfForwardingQueueNotWritable()) + { + return; + } Logger.InfoFormat("Error import is now started, feeding error messages from: {0}", InputAddress); } @@ -82,6 +89,24 @@ public Action GetReceiverCustomization() return receiver => { receiver.FailureManager = satelliteImportFailuresHandler; }; } + bool TerminateIfForwardingQueueNotWritable() + { + try + { + //Send a message to test the forwarding queue + var testMessage = new TransportMessage(Guid.Empty.ToString("N"), new Dictionary()); + Forwarder.Send(testMessage, Settings.ErrorLogQueue); + return false; + } + catch (Exception messageForwardingException) + { + //This call to RaiseCriticalError has to be on a seperate thread otherwise it deadlocks and doesn't stop correctly. + ThreadPool.QueueUserWorkItem(state => Configure.Instance.RaiseCriticalError(string.Format("Error Import cannot start"), messageForwardingException)); + return true; + } + } + + public void Dispose() { if (satelliteImportFailuresHandler != null) diff --git a/src/ServiceControl/Operations/Msmq/MsmqAuditQueueImporter.cs b/src/ServiceControl/Operations/Msmq/MsmqAuditQueueImporter.cs index 7a55366167..14a3a9d5ed 100644 --- a/src/ServiceControl/Operations/Msmq/MsmqAuditQueueImporter.cs +++ b/src/ServiceControl/Operations/Msmq/MsmqAuditQueueImporter.cs @@ -42,6 +42,10 @@ public MsmqAuditQueueImporter(IDocumentStore store, IBuilder builder, IDequeueMe public void Start() { + // Any messages that fail conversion to a transportmessage is sent to the particular.servicecontrol.errors queue using low level Api + // The actual queue name is based on service name to support mulitple instances on same host (particular.servicecontrol.errors is the default) + var serviceControlErrorQueueAddress = Address.Parse(string.Format("{0}.errors", Settings.ServiceName)); + serviceControlErrorQueue = new MessageQueue(MsmqUtilities.GetFullPath(serviceControlErrorQueueAddress), false, true, QueueAccessMode.Send); if (!enabled) { @@ -54,6 +58,11 @@ public void Start() return; } + if (TerminateIfForwardingIsEnabledButQueueNotWritable()) + { + return; + } + performanceCounters.Initialize(); queuePeeker = new MessageQueue(MsmqUtilities.GetFullPath(Settings.AuditQueue), QueueAccessMode.Peek); @@ -90,6 +99,28 @@ public void Stop() stopResetEvent.Dispose(); } + bool TerminateIfForwardingIsEnabledButQueueNotWritable() + { + if (Settings.ForwardAuditMessages != true) + { + return false; + } + + try + { + //Send a message to test the forwarding queue + var testMessage = new TransportMessage(Guid.Empty.ToString("N"), new Dictionary()); + Forwarder.Send(testMessage, Settings.AuditLogQueue); + return false; + } + catch (Exception messageForwardingException) + { + //This call to RaiseCriticalError has to be on a seperate thread otherwise it deadlocks and doesn't stop correctly. + ThreadPool.QueueUserWorkItem(state => Configure.Instance.RaiseCriticalError(string.Format("Audit Import cannot start"), messageForwardingException)); + return true; + } + } + static MessageQueue CreateReceiver() { var queue = new MessageQueue(MsmqUtilities.GetFullPath(Settings.AuditQueue), QueueAccessMode.Receive); @@ -233,7 +264,7 @@ void BatchImporter() bulkInsert.Store(auditMessage); performanceCounters.MessageProcessed(); - if (Settings.ForwardAuditMessages) + if (Settings.ForwardAuditMessages == true) { Forwarder.Send(transportMessage, Settings.AuditLogQueue); } @@ -308,6 +339,7 @@ void RetryMessageImportById(string messageID) catch (Exception convertException) { importFailuresHandler.FailedToReceive(convertException); //logs and increments circuit breaker + serviceControlErrorQueue.Send(message, msmqTransaction); // Send unconvertable message to SC's ErrorQueue so it's not lost commitTransaction = true; // Can't convert the messsage, so commit to get message out of the queue return; } @@ -328,7 +360,7 @@ void RetryMessageImportById(string messageID) } performanceCounters.MessageProcessed(); - if (Settings.ForwardAuditMessages) + if (Settings.ForwardAuditMessages == true) { Forwarder.Send(transportMessage, Settings.AuditLogQueue); } @@ -379,6 +411,8 @@ void RetryMessageImportById(string messageID) BatchTaskTracker batchTaskTracker = new BatchTaskTracker(); List enrichers; MessageQueue queuePeeker; + MessageQueue serviceControlErrorQueue; + volatile bool stopping; class BatchTaskTracker diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index e1cdf96b1b..7ec18b6608 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -12,6 +12,21 @@ v4.5 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true true @@ -137,7 +152,7 @@ False - ..\packages\ServiceControl.Contracts.1.0.0\lib\net45\ServiceControl.Contracts.dll + ..\packages\ServiceControl.Contracts.1.1.0\lib\net45\ServiceControl.Contracts.dll @@ -235,11 +250,13 @@ + + @@ -252,6 +269,7 @@ + @@ -271,8 +289,12 @@ Component + + + + @@ -441,11 +463,27 @@ - PreserveNewest Designer + + + False + Microsoft .NET Framework 4.5 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + \ No newline at end of file diff --git a/src/ServiceControl/packages.config b/src/ServiceControl/packages.config index 11f9218c2d..ce6d4f2a21 100644 --- a/src/ServiceControl/packages.config +++ b/src/ServiceControl/packages.config @@ -31,7 +31,7 @@ - +