diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs index c0ec329d4b..6c747a5677 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -1,23 +1,26 @@ namespace ServiceControl.AcceptanceTests { - using System.Collections.Generic; + using System; + using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Threading.Tasks; using Persistence.RavenDb; + using ServiceBus.Management.Infrastructure.Settings; class AcceptanceTestStorageConfiguration { public string PersistenceType { get; protected set; } - public Task> CustomizeSettings() + public void CustomizeSettings(Settings settings) { - return Task.FromResult>(new Dictionary + settings.PersisterSpecificSettings = new RavenDBPersisterSettings { - { "RavenDB35/RunInMemory", bool.TrueString}, - { "DatabaseMaintenancePort", FindAvailablePort(33334).ToString()}, - { "HostName", "localhost" } - }); + RunInMemory = true, + DatabaseMaintenancePort = FindAvailablePort(settings.Port + 1), + DatabasePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), + ErrorRetentionPeriod = TimeSpan.FromDays(10), + }; } public Task Configure() diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/App.config b/src/ServiceControl.AcceptanceTests.RavenDB/App.config index c2987b05e6..4149eda6a7 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/App.config +++ b/src/ServiceControl.AcceptanceTests.RavenDB/App.config @@ -1,10 +1,7 @@  - - - diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs index 84979bf317..0297c2ccaa 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Legacy/AuditRetention.cs @@ -14,7 +14,6 @@ using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.AcceptanceTests; using ServiceControl.AcceptanceTests.TestSupport.EndpointTemplates; - using ServiceControl.Persistence; using ServiceControl.Persistence.RavenDb.SagaAudit; using ServiceControl.SagaAudit; @@ -34,7 +33,7 @@ public async Task //Override the configuration of the check in the container in order to make it run more frequently for testing purposes. CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices((ctx, services) => - services.AddTransient(provider => new AuditRetentionCustomCheck(provider.GetRequiredService(), provider.GetRequiredService(), TimeSpan.FromSeconds(10)))); + services.AddTransient(provider => new AuditRetentionCustomCheck(provider.GetRequiredService(), provider.GetRequiredService(), TimeSpan.FromSeconds(10)))); SingleResult customCheckEventEntry = default; bool sagaAudiDataInMainInstanceIsAvailableForQuery = false; diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs b/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs new file mode 100644 index 0000000000..83b9c668da --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs @@ -0,0 +1,69 @@ +namespace ServiceControl.AcceptanceTests.RavenDB +{ + using System; + using System.Threading.Tasks; + using Hosting.Commands; + using NServiceBus; + using NUnit.Framework; + using Particular.ServiceControl; + using Particular.ServiceControl.Hosting; + using Persistence.RavenDb; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.AcceptanceTesting.InfrastructureConfig; + + class StartupModeTests : AcceptanceTest + { + Settings settings; + + [SetUp] + public void InitializeSettings() + { + var transportIntegration = new ConfigureEndpointLearningTransport(); + settings = new Settings( + forwardErrorMessages: false, + errorRetentionPeriod: TimeSpan.FromDays(1), + persisterType: typeof(RavenDbPersistenceConfiguration).AssemblyQualifiedName) + { + PersisterSpecificSettings = new RavenDBPersisterSettings + { + ErrorRetentionPeriod = TimeSpan.FromDays(1), + RunInMemory = true + }, + TransportType = transportIntegration.TypeName, + TransportConnectionString = transportIntegration.ConnectionString + }; + } + + [Test] + public async Task CanRunMaintenanceMode() + { + var bootstrapper = new MaintenanceBootstrapper(settings); + + var host = bootstrapper.HostBuilder.Build(); + + await host.StartAsync(); + await host.StopAsync(); + } + + [Test] + public async Task CanRunImportFailedMessagesMode() + { + await new TestableImportFailedErrorsCommand().Execute(new HostArguments(Array.Empty()), settings); + } + + class TestableImportFailedErrorsCommand : ImportFailedErrorsCommand + { + protected override EndpointConfiguration CreateEndpointConfiguration(Settings settings) + { + var configuration = base.CreateEndpointConfiguration(settings); + + //HINT: we want to exclude this assembly to prevent loading features that are part of the acceptance testing framework + var thisAssembly = new[] { typeof(StartupModeTests).Assembly.GetName().Name }; + + configuration.AssemblyScanner().ExcludeAssemblies(thisAssembly); + + return configuration; + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs index 77c357fb30..4acd4bdce5 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs @@ -2,16 +2,12 @@ { using System; using System.Linq; - using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using MessageFailures.Api; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.AcceptanceTesting; using NUnit.Framework; - using Persistence; using ServiceBus.Management.Infrastructure.Settings; using TestSupport.EndpointTemplates; @@ -28,27 +24,11 @@ public void SetupIngestion() }; } - class PersisterSettingsRetriever : IHostedService - { - static PersistenceSettings _persistenceSettings; - - public PersisterSettingsRetriever(PersistenceSettings persistenceSettings, int value) - { - _persistenceSettings = persistenceSettings; - SetMinimumStorageLeftForIngestion(value); - } - - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public static void SetMinimumStorageLeftForIngestion(int value) => _persistenceSettings.PersisterSpecificSettings["MinimumStorageLeftRequiredForIngestion"] = value.ToString(); - } + RavenDBPersisterSettings PersisterSettings => (RavenDBPersisterSettings)Settings.PersisterSpecificSettings; [Test] public async Task Should_stop_ingestion() { - CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices(services => services.AddHostedService(b => new PersisterSettingsRetriever(b.GetRequiredService(), 0))); - await Define() .WithEndpoint(b => b .When(context => @@ -56,7 +36,7 @@ await Define() return context.Logs.ToArray().Any(i => i.Message.StartsWith("Ensure started. Infrastructure started")); }, (_, __) => { - PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(100); + PersisterSettings.MinimumStorageLeftRequiredForIngestion = 100; return Task.CompletedTask; }) .When(context => @@ -67,14 +47,13 @@ await Define() }, (bus, c) => bus.SendLocal(new MyMessage()) ) .DoNotFailOnErrorMessages()) - //.Done(async c => await this.TryGetSingle("/api/errors") == false) + .Done(async c => await this.TryGetSingle("/api/errors") == false) .Run(); } [Test] public async Task Should_stop_ingestion_and_resume_when_more_space_is_available() { - CustomizeHostBuilder = hostBuilder => hostBuilder.ConfigureServices(services => services.AddHostedService(b => new PersisterSettingsRetriever(b.GetRequiredService(), 0))); var ingestionShutdown = false; await Define() @@ -86,7 +65,7 @@ await Define() "Ensure started. Infrastructure started")); }, (session, context) => { - PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(100); + PersisterSettings.MinimumStorageLeftRequiredForIngestion = 100; return Task.CompletedTask; }) .When(context => @@ -103,7 +82,7 @@ await Define() }) .When(c => ingestionShutdown, (session, context) => { - PersisterSettingsRetriever.SetMinimumStorageLeftForIngestion(0); + PersisterSettings.MinimumStorageLeftRequiredForIngestion = 0; return Task.CompletedTask; }) .DoNotFailOnErrorMessages()) diff --git a/src/ServiceControl.AcceptanceTests/RootControllerTests.cs b/src/ServiceControl.AcceptanceTests/RootControllerTests.cs index 72d1ddfad6..b370736363 100644 --- a/src/ServiceControl.AcceptanceTests/RootControllerTests.cs +++ b/src/ServiceControl.AcceptanceTests/RootControllerTests.cs @@ -24,7 +24,7 @@ public async Task Should_gather_remote_data() { hostBuilder.ConfigureServices((hostBuilderContext, services) => { - services.AddSingleton(new Settings(serviceName) + services.AddSingleton(new Settings(serviceName, forwardErrorMessages: false, errorRetentionPeriod: TimeSpan.FromDays(10)) { RemoteInstances = new[] { diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs index 30d7dfbfbd..2410c9ab75 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs @@ -72,7 +72,16 @@ public async Task Setup() await StorageConfiguration.Configure(); - serviceControlRunnerBehavior = new ServiceControlComponentBehavior(TransportIntegration, StorageConfiguration, s => SetSettings(s), s => CustomConfiguration(s), hb => CustomizeHostBuilder(hb)); + serviceControlRunnerBehavior = new ServiceControlComponentBehavior( + TransportIntegration, + StorageConfiguration, + s => + { + SetSettings(s); + }, + s => CustomConfiguration(s), + hb => CustomizeHostBuilder(hb) + ); } [TearDown] diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 97795026d1..7202c5fdeb 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -70,22 +70,13 @@ static int FindAvailablePort(int startPort) async Task InitializeServiceControl(ScenarioContext context) { var instancePort = FindAvailablePort(33333); - var maintenancePort = FindAvailablePort(instancePort + 1); ConfigurationManager.AppSettings.Set("ServiceControl/TransportType", transportToUse.TypeName); - // TODO: ⬇️ This is required for the PERSISTER implementation to actually retrieve the DBPath settings are persister isn't using this type-safe settings class - var dbPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - ConfigurationManager.AppSettings.Set("ServiceControl/DBPath", dbPath); // TODO: Tests should not use static ConfigurationManager.AppSettings - ConfigurationManager.AppSettings.Set("ServiceControl/DatabaseMaintenancePort", "33434"); - ConfigurationManager.AppSettings.Set("ServiceControl/ExposeRavenDB", "False"); - // TODO: ⬆️ - - var settings = new Settings(instanceName, transportToUse.TypeName, persistenceToUse.PersistenceType) + var settings = new Settings(instanceName, transportToUse.TypeName, persistenceToUse.PersistenceType, forwardErrorMessages: false, errorRetentionPeriod: TimeSpan.FromDays(10)) { + AllowMessageEditing = true, Port = instancePort, - DatabaseMaintenancePort = maintenancePort, - DbPath = dbPath, ForwardErrorMessages = false, TransportConnectionString = transportToUse.ConnectionString, ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(2), @@ -124,19 +115,14 @@ async Task InitializeServiceControl(ScenarioContext context) } return false; - } + }, }; + persistenceToUse.CustomizeSettings(settings); + setSettings(settings); Settings = settings; - var persisterSpecificSettings = await persistenceToUse.CustomizeSettings(); - - foreach (var persisterSpecificSetting in persisterSpecificSettings) - { - ConfigurationManager.AppSettings.Set($"ServiceControl/{persisterSpecificSetting.Key}", persisterSpecificSetting.Value); - } - using (new DiagnosticTimer($"Creating infrastructure for {instanceName}")) { @@ -223,7 +209,7 @@ public override async Task Stop() await host.StopAsync(); HttpClient.Dispose(); Handler.Dispose(); - DirectoryDeleter.Delete(Settings.DbPath); + DirectoryDeleter.Delete(Settings.PersisterSpecificSettings.DatabasePath); } bootstrapper = null; diff --git a/src/ServiceControl.Audit/App.config b/src/ServiceControl.Audit/App.config index f489462dec..5d596184ef 100644 --- a/src/ServiceControl.Audit/App.config +++ b/src/ServiceControl.Audit/App.config @@ -9,7 +9,7 @@ These settings are only here so that we can debug ServiceControl while developin - + - + - + - + - + @@ -63,8 +63,7 @@ These settings are only here so that we can debug ServiceControl while developin - - + diff --git a/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs b/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs index f0a9da3ad4..9d9a5fc7a5 100644 --- a/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs +++ b/src/ServiceControl.Configuration/ConfigFileSettingsReader.cs @@ -3,32 +3,30 @@ namespace ServiceBus.Management.Infrastructure.Settings using System; using System.Configuration; - static class ConfigFileSettingsReader + class ConfigFileSettingsReader : ISettingsReader { - public static T Read(string name, T defaultValue = default) + public object Read(string root, string name, Type type, object defaultValue = default) { - return Read("ServiceControl", name, defaultValue); + return TryRead(root, name, type, out var value) + ? value + : defaultValue; } - public static T Read(string root, string name, T defaultValue = default) - { - if (TryRead(root, name, out var value)) - { - return value; - } - - return defaultValue; - } - - public static bool TryRead(string root, string name, out T value) + public bool TryRead(string root, string name, Type type, out object value) { var fullKey = $"{root}/{name}"; var appSettingValue = ConfigurationManager.AppSettings[fullKey]; if (appSettingValue != null) { - appSettingValue = Environment.ExpandEnvironmentVariables(appSettingValue); // TODO: Just added this to have expansing on appsettings to not have hardcoded "temp" paths which are different for everyone. - value = (T)Convert.ChangeType(appSettingValue, typeof(T)); + appSettingValue = Environment.ExpandEnvironmentVariables(appSettingValue); + + var underlyingType = Nullable.GetUnderlyingType(type); + + var destinationType = underlyingType ?? type; + + value = SettingsReader.ConvertFrom(appSettingValue, destinationType); + return true; } diff --git a/src/ServiceControl.Configuration/EnvironmentVariableSettingsReader.cs b/src/ServiceControl.Configuration/EnvironmentVariableSettingsReader.cs index da1e2b53ab..22824a6270 100644 --- a/src/ServiceControl.Configuration/EnvironmentVariableSettingsReader.cs +++ b/src/ServiceControl.Configuration/EnvironmentVariableSettingsReader.cs @@ -2,31 +2,28 @@ namespace ServiceBus.Management.Infrastructure.Settings { using System; - static class EnvironmentVariableSettingsReader + class EnvironmentVariableSettingsReader : ISettingsReader { - public static T Read(string name, T defaultValue = default) + public object Read(string root, string name, Type type, object defaultValue = default) { - return Read("ServiceControl", name, defaultValue); + return TryRead(root, name, type, out var value) + ? value + : defaultValue; } - public static T Read(string root, string name, T defaultValue = default) + public bool TryRead(string root, string name, Type type, out object value) { - if (TryRead(root, name, out var value)) + if (TryReadVariable(type, out value, $"{root}/{name}")) { - return value; + return true; } - - return defaultValue; - } - - public static bool TryRead(string root, string name, out T value) - { - if (TryReadVariable(out value, $"{root}/{name}")) + // Azure container instance compatibility: + if (TryReadVariable(type, out value, $"{root}_{name}".Replace('.', '_'))) { return true; } - // Azure container instance compatibility: - if (TryReadVariable(out value, $"{root}_{name}".Replace('.', '_'))) + // container images and env files compatibility: + if (TryReadVariable(type, out value, $"{root}_{name}".Replace('.', '_').Replace('/', '_'))) { return true; } @@ -35,14 +32,14 @@ public static bool TryRead(string root, string name, out T value) return false; } - static bool TryReadVariable(out T value, string fullKey) + static bool TryReadVariable(Type type, out object value, string fullKey) { var environmentValue = Environment.GetEnvironmentVariable(fullKey); if (environmentValue != null) { environmentValue = Environment.ExpandEnvironmentVariables(environmentValue); - value = (T)Convert.ChangeType(environmentValue, typeof(T)); + value = SettingsReader.ConvertFrom(environmentValue, type); return true; } diff --git a/src/ServiceControl.Configuration/ISettingsReader.cs b/src/ServiceControl.Configuration/ISettingsReader.cs new file mode 100644 index 0000000000..7f9f2f77c1 --- /dev/null +++ b/src/ServiceControl.Configuration/ISettingsReader.cs @@ -0,0 +1,10 @@ +namespace ServiceBus.Management.Infrastructure.Settings +{ + using System; + + interface ISettingsReader + { + object Read(string root, string name, Type type, object defaultValue = null); + bool TryRead(string root, string name, Type type, out object value); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/NullableRegistryReader.cs b/src/ServiceControl.Configuration/NullableRegistryReader.cs deleted file mode 100644 index 67583e8888..0000000000 --- a/src/ServiceControl.Configuration/NullableRegistryReader.cs +++ /dev/null @@ -1,75 +0,0 @@ -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)) - { - 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($@"We couldn't read the registry to retrieve the {name}, from '{regPath}'.", ex); - } - - return defaultValue; - } - - static readonly ILog Logger = LogManager.GetLogger(typeof(NullableRegistryReader)); - } -} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/NullableSettingsReader.cs b/src/ServiceControl.Configuration/NullableSettingsReader.cs deleted file mode 100644 index 4bd4d0420d..0000000000 --- a/src/ServiceControl.Configuration/NullableSettingsReader.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace ServiceBus.Management.Infrastructure.Settings -{ - using System; - using System.Configuration; - - 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 (EnvironmentVariableSettingsReader.TryRead(root, name, out var environmentVariable)) - { - return environmentVariable; - } - - 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.Configuration/RegistryReader.cs b/src/ServiceControl.Configuration/RegistryReader.cs index 1f5d02b7ac..50b03178f0 100644 --- a/src/ServiceControl.Configuration/RegistryReader.cs +++ b/src/ServiceControl.Configuration/RegistryReader.cs @@ -7,8 +7,7 @@ namespace ServiceBus.Management.Infrastructure.Settings /// /// Wrapper to read registry keys. /// - /// The type of the key to retrieve - class RegistryReader + class RegistryReader : ISettingsReader { /// /// Attempts to read the key from the registry. @@ -20,7 +19,7 @@ class RegistryReader /// 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 = default) + public object Read(string subKey, string name, Type type, object defaultValue = default) { var regPath = @"SOFTWARE\ParticularSoftware\" + subKey.Replace("/", "\\"); try @@ -35,7 +34,7 @@ public static T Read(string subKey, string name, T defaultValue = default) if (value != null) { - return (T)Convert.ChangeType(value, typeof(T)); + return SettingsReader.ConvertFrom(value, type); } } @@ -45,7 +44,7 @@ public static T Read(string subKey, string name, T defaultValue = default) { if (registryKey != null) { - return (T)Convert.ChangeType(registryKey.GetValue(name, defaultValue), typeof(T)); + return SettingsReader.ConvertFrom(registryKey.GetValue(name, defaultValue), type); } } } @@ -57,20 +56,25 @@ public static T Read(string subKey, string name, T defaultValue = default) { if (registryKey != null) { - return (T)Convert.ChangeType(registryKey.GetValue(name, defaultValue), typeof(T)); + return SettingsReader.ConvertFrom(registryKey.GetValue(name, defaultValue), type); } } } } catch (Exception ex) { - Logger.Warn( - $@"We couldn't read the registry to retrieve the {name}, from '{regPath}'.", ex); + Logger.Warn($"Couldn't read the registry to retrieve the {name}, from '{regPath}'.", ex); } return defaultValue; } - static readonly ILog Logger = LogManager.GetLogger(typeof(RegistryReader)); + public bool TryRead(string root, string name, Type type, out object value) + { + value = Read(root, name, type); + return value != null; + } + + static readonly ILog Logger = LogManager.GetLogger(typeof(RegistryReader)); } } \ No newline at end of file diff --git a/src/ServiceControl.Configuration/SettingsReader.cs b/src/ServiceControl.Configuration/SettingsReader.cs index b8fa78c8f7..58a94aa6cb 100644 --- a/src/ServiceControl.Configuration/SettingsReader.cs +++ b/src/ServiceControl.Configuration/SettingsReader.cs @@ -1,25 +1,77 @@ namespace ServiceBus.Management.Infrastructure.Settings { - class SettingsReader + using System; + using System.ComponentModel; + + static class SettingsReader { - public static T Read(string name, T defaultValue = default) + const string Namespace = "ServiceControl"; + static readonly ISettingsReader EnvironmentVariable = new EnvironmentVariableSettingsReader(); + static readonly ISettingsReader Registry = new RegistryReader(); + public static readonly ISettingsReader ConfigFile = new ConfigFileSettingsReader(); + + public static T Read(string name, T defaultValue = default) { - return Read("ServiceControl", name, defaultValue); + return Read(Namespace, name, defaultValue); } - public static T Read(string root, string name, T defaultValue = default) + public static T Read(string root, string name, T defaultValue = default) { - if (EnvironmentVariableSettingsReader.TryRead(root, name, out var envValue)) + return (T)Read(root, name, typeof(T), defaultValue); + } + + static object Read(string root, string name, Type type, object defaultValue = default) + { + if (EnvironmentVariable.TryRead(root, name, type, out var envValue)) { return envValue; } - if (ConfigFileSettingsReader.TryRead(root, name, out var value)) + if (ConfigFile.TryRead(root, name, type, out var value)) { return value; } - return RegistryReader.Read(root, name, defaultValue); + return Registry.Read(root, name, type, defaultValue); } + + public static bool TryRead(string name, Type type, out object value) + { + var root = Namespace; + + if (EnvironmentVariable.TryRead(root, name, type, out var envValue)) + { + value = envValue; + return true; + } + + if (ConfigFile.TryRead(root, name, type, out var configValue)) + { + value = configValue; + return true; + } + + if (Registry.TryRead(root, name, type, out var regValue)) + { + value = regValue; + return true; + } + + value = null; + return false; + } + + public static T Read(this ISettingsReader instance, string name, T defaultValue = default) + { + return (T)instance.Read(Namespace, name, typeof(T), defaultValue); + } + + public static object ConvertFrom(object sourceValue, Type destinationType) + { + var converter = TypeDescriptor.GetConverter(destinationType); + object value = converter.ConvertFrom(sourceValue); + return value; + } + } } \ No newline at end of file diff --git a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj index 877509b573..ed0859a57a 100644 --- a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj +++ b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj @@ -9,7 +9,6 @@ - @@ -17,12 +16,4 @@ - - - - - - - - \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/App.config b/src/ServiceControl.Monitoring/App.config index eb0ea335e8..1ed00b3cf4 100644 --- a/src/ServiceControl.Monitoring/App.config +++ b/src/ServiceControl.Monitoring/App.config @@ -4,7 +4,7 @@ - + + --> - + - - + + @@ -36,7 +36,7 @@ - + diff --git a/src/ServiceControl.Monitoring/Infrastructure/Settings/EnvironmentVariableSettingsReader.cs b/src/ServiceControl.Monitoring/Infrastructure/Settings/EnvironmentVariableSettingsReader.cs index 94802ea930..c63249f83e 100644 --- a/src/ServiceControl.Monitoring/Infrastructure/Settings/EnvironmentVariableSettingsReader.cs +++ b/src/ServiceControl.Monitoring/Infrastructure/Settings/EnvironmentVariableSettingsReader.cs @@ -30,6 +30,11 @@ public static bool TryRead(string root, string name, out T value) { return true; } + // container images and env files compatibility: + if (TryReadVariable(out value, $"{root}_{name}".Replace('.', '_').Replace('/', '_'))) + { + return true; + } value = default; return false; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs index c83cffbe63..0a3e140f3d 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs @@ -71,7 +71,14 @@ public void Setup() DataStoreTypeName = "RavenDB35" }; - serviceControlRunnerBehavior = new ServiceControlComponentBehavior(TransportIntegration, DataStoreConfiguration, c => CustomEndpointConfiguration(c), c => CustomAuditEndpointConfiguration(c), s => CustomServiceControlSettings(s), s => CustomServiceControlAuditSettings(s)); + serviceControlRunnerBehavior = new ServiceControlComponentBehavior( + TransportIntegration, + DataStoreConfiguration, + c => CustomEndpointConfiguration(c), + c => CustomAuditEndpointConfiguration(c), + s => CustomServiceControlSettings(s), + s => CustomServiceControlAuditSettings(s) + ); } [TearDown] diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 993bb7da20..44c1cce4dc 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -84,11 +84,10 @@ async Task InitializeServiceControl(ScenarioContext context, int instancePort, i var settings = new Settings(instanceName, transportToUse.TypeName, dataStoreConfiguration.DataStoreTypeName) { Port = instancePort, - PersisterSpecificSettings = new Dictionary() + PersisterSpecificSettings = new RavenDBPersisterSettings { - { "HostName", "localhost" }, - { "RavenDB35/RunInMemory", "true" }, - { "DatabaseMaintenancePort", maintenancePort.ToString() } + RunInMemory = true, + DatabaseMaintenancePort = maintenancePort }, ForwardErrorMessages = false, TransportType = transportToUse.TypeName, diff --git a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs index da9de3e68d..14fb9ae8d9 100644 --- a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckFreeDiskSpace.cs @@ -5,15 +5,14 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence; using Persistence.RavenDb; class CheckFreeDiskSpace : CustomCheck { - public CheckFreeDiskSpace(PersistenceSettings settings) : base("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) + public CheckFreeDiskSpace(RavenDBPersisterSettings settings) : base("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) { - dataPath = settings.PersisterSpecificSettings[RavenDbPersistenceConfiguration.DbPathKey]; - percentageThreshold = GetDataSpaceRemainingThreshold(settings) / 100m; + dataPath = settings.DatabasePath; + percentageThreshold = settings.DataSpaceRemainingThreshold; Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); } @@ -35,7 +34,7 @@ public override Task PerformCheck() if (Logger.IsDebugEnabled) { - Logger.Debug($"Free space: {availableFreeSpace} | Total: {totalSpace} | Percent remaining {percentRemaining:P0}"); + Logger.Debug($"Free space: {availableFreeSpace:N0}B | Total: {totalSpace:N0}B | Percent remaining {percentRemaining:P1}"); } return percentRemaining > percentageThreshold @@ -43,14 +42,10 @@ public override Task PerformCheck() : CheckResult.Failed($"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."); } - int GetDataSpaceRemainingThreshold(PersistenceSettings settings) + public static void Validate(RavenDBPersisterSettings settings) { - var threshold = DataSpaceRemainingThresholdDefault; + var threshold = settings.DataSpaceRemainingThreshold; - if (settings.PersisterSpecificSettings.TryGetValue(RavenDbPersistenceConfiguration.DataSpaceRemainingThresholdKey, out var thresholdValue)) - { - threshold = int.Parse(thresholdValue); - } string message; if (threshold < 0) @@ -66,14 +61,12 @@ int GetDataSpaceRemainingThreshold(PersistenceSettings settings) Logger.Fatal(message); throw new Exception(message); } - - return threshold; } readonly string dataPath; readonly decimal percentageThreshold; - const int DataSpaceRemainingThresholdDefault = 20; + public const int DataSpaceRemainingThresholdDefault = 20; static readonly ILog Logger = LogManager.GetLogger(typeof(CheckFreeDiskSpace)); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs index c87cf2066b..a696a1d45d 100644 --- a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -10,23 +10,25 @@ class CheckMinimumStorageRequiredForIngestion : CustomCheck { - public CheckMinimumStorageRequiredForIngestion(MinimumRequiredStorageState stateHolder, PersistenceSettings settings) + public CheckMinimumStorageRequiredForIngestion( + MinimumRequiredStorageState stateHolder, + RavenDBPersisterSettings settings) : base("Message Ingestion Process", "ServiceControl Health", TimeSpan.FromSeconds(5)) { this.stateHolder = stateHolder; this.settings = settings; - dataPathRoot = Path.GetPathRoot(settings.PersisterSpecificSettings[RavenDbPersistenceConfiguration.DbPathKey]); + dataPathRoot = Path.GetPathRoot(settings.DatabasePath); } public override Task PerformCheck() { - percentageThreshold = GetMinimumStorageLeftRequiredForIngestion(settings) / 100m; + percentageThreshold = settings.MinimumStorageLeftRequiredForIngestion / 100m; if (dataPathRoot == null) { stateHolder.CanIngestMore = true; - return successResult; + return SuccessResult; } Logger.Debug($"Check ServiceControl data drive space starting. Threshold {percentageThreshold:P0}"); @@ -39,13 +41,13 @@ public override Task PerformCheck() if (Logger.IsDebugEnabled) { - Logger.Debug($"Free space: {availableFreeSpace} | Total: {totalSpace} | Percent remaining {percentRemaining:P0}"); + Logger.Debug($"Free space: {availableFreeSpace:N0}B | Total: {totalSpace:N0}B | Percent remaining {percentRemaining:P1}"); } if (percentRemaining > percentageThreshold) { stateHolder.CanIngestMore = true; - return successResult; + return SuccessResult; } var message = $"Error message ingestion stopped! {percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'. This is less than {percentageThreshold}% - the minimal required space configured. The threshold can be set using the {RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} configuration setting."; @@ -54,14 +56,9 @@ public override Task PerformCheck() return CheckResult.Failed(message); } - int GetMinimumStorageLeftRequiredForIngestion(PersistenceSettings settings) + public static void Validate(RavenDBPersisterSettings settings) { - int threshold = MinimumStorageLeftRequiredForIngestionDefault; - - if (settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, out var thresholdValue)) - { - threshold = int.Parse(thresholdValue); - } + int threshold = settings.MinimumStorageLeftRequiredForIngestion; string message; if (threshold < 0) @@ -77,19 +74,17 @@ int GetMinimumStorageLeftRequiredForIngestion(PersistenceSettings settings) Logger.Fatal(message); throw new Exception(message); } - - return threshold; } - const int MinimumStorageLeftRequiredForIngestionDefault = 5; + public const int MinimumStorageLeftRequiredForIngestionDefault = 5; readonly MinimumRequiredStorageState stateHolder; - readonly PersistenceSettings settings; + readonly RavenDBPersisterSettings settings; readonly string dataPathRoot; decimal percentageThreshold; - static Task successResult = Task.FromResult(CheckResult.Pass); + static readonly Task SuccessResult = Task.FromResult(CheckResult.Pass); static readonly ILog Logger = LogManager.GetLogger(typeof(CheckMinimumStorageRequiredForIngestion)); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs index ebec2d73e6..f138e0efa0 100644 --- a/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs +++ b/src/ServiceControl.Persistence.RavenDb/CustomChecks/CheckRavenDBIndexLag.cs @@ -6,20 +6,16 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence; - using Persistence.RavenDb; using Raven.Client; using Raven.Abstractions.Data; - using Raven.Abstractions.Extensions; using CustomCheck = NServiceBus.CustomChecks.CustomCheck; class CheckRavenDBIndexLag : CustomCheck { - public CheckRavenDBIndexLag(IDocumentStore store, PersistenceSettings settings) + public CheckRavenDBIndexLag(IDocumentStore store) : base("Error Database Index Lag", "ServiceControl Health", TimeSpan.FromMinutes(5)) { this.store = store; - this.settings = settings; } public override Task PerformCheck() @@ -33,9 +29,7 @@ public override Task PerformCheck() if (indexCountWithTooMuchLag > 0) { - var logPath = settings.PersisterSpecificSettings.GetOrDefault(RavenDbPersistenceConfiguration.LogPathKey); - - return CheckResult.Failed($"At least one index significantly stale. Please run maintenance mode if this custom check persists to ensure index(es) can recover. See log file in `{logPath}` for more details. Visit https://docs.particular.net/search?q=servicecontrol+troubleshooting for more information."); + return CheckResult.Failed($"At least one index significantly stale. Please run maintenance mode if this custom check persists to ensure index(es) can recover. See log file for more details. Visit https://docs.particular.net/search?q=servicecontrol+troubleshooting for more information."); } return CheckResult.Pass; @@ -54,12 +48,12 @@ static int CheckAndReportIndexesWithTooMuchIndexLag(IndexStats[] indexes) if (indexLag > IndexLagThresholdError) { indexCountWithTooMuchLag++; - _log.Error($"Index [{indexStats.Name}] IndexingLag {indexLag:n0} is above error threshold ({IndexLagThresholdError:n0}). Launch in maintenance mode to let indexes catch up."); + Log.Error($"Index [{indexStats.Name}] IndexingLag {indexLag:n0} is above error threshold ({IndexLagThresholdError:n0}). Launch in maintenance mode to let indexes catch up."); } else if (indexLag > IndexLagThresholdWarning) { indexCountWithTooMuchLag++; - _log.Warn($"Index [{indexStats.Name}] IndexingLag {indexLag:n0} is above warning threshold ({IndexLagThresholdWarning:n0}). Launch in maintenance mode to let indexes catch up."); + Log.Warn($"Index [{indexStats.Name}] IndexingLag {indexLag:n0} is above warning threshold ({IndexLagThresholdWarning:n0}). Launch in maintenance mode to let indexes catch up."); } } @@ -68,7 +62,7 @@ static int CheckAndReportIndexesWithTooMuchIndexLag(IndexStats[] indexes) static void CreateDiagnosticsLogEntry(DatabaseStatistics statistics, IndexStats[] indexes) { - if (!_log.IsDebugEnabled) + if (!Log.IsDebugEnabled) { return; } @@ -83,14 +77,13 @@ static void CreateDiagnosticsLogEntry(DatabaseStatistics statistics, IndexStats[ indexLag = Math.Abs(indexLag); report.AppendLine($"- Index [{indexStats.Name,-44}] Stale: {statistics.StaleIndexes.Contains(indexStats.Name),-5}, Lag: {indexLag,9:n0}, Valid: {indexStats.IsInvalidIndex,-5}, LastIndexed: {indexStats.LastIndexedTimestamp:u}, LastIndexing: {indexStats.LastIndexingTime:u}"); } - _log.Debug(report.ToString()); + Log.Debug(report.ToString()); } const int IndexLagThresholdWarning = 10000; const int IndexLagThresholdError = 100000; - static ILog _log = LogManager.GetLogger(); + static readonly ILog Log = LogManager.GetLogger(); - IDocumentStore store; - PersistenceSettings settings; + readonly IDocumentStore store; } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs index ea0b50f3e9..ec610a1bc1 100644 --- a/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleaner.cs @@ -8,11 +8,10 @@ using Raven.Abstractions; using Raven.Database; using SagaAudit; - using ServiceControl.Persistence; class ExpiredDocumentsCleaner { - public static Task RunCleanup(int deletionBatchSize, DocumentDatabase database, PersistenceSettings settings, CancellationToken cancellationToken = default) + public static Task RunCleanup(int deletionBatchSize, DocumentDatabase database, RavenDBPersisterSettings settings, CancellationToken cancellationToken = default) { var threshold = SystemTime.UtcNow.Add(settings.ErrorRetentionPeriod); diff --git a/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs index cb0ccb016a..bcaf23c33b 100644 --- a/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs +++ b/src/ServiceControl.Persistence.RavenDb/Expiration/ExpiredDocumentsCleanerBundle.cs @@ -31,11 +31,11 @@ public void Dispose() var finishedTask = composite.GetAwaiter().GetResult(); if (finishedTask == delayTask) { - logger.Error("Cleanup process did not finish on time. Forcing shutdown."); + Logger.Error("Cleanup process did not finish on time. Forcing shutdown."); } else { - logger.Info("Expired documents cleanup process stopped."); + Logger.Info("Expired documents cleanup process stopped."); } timer = null; @@ -54,41 +54,36 @@ public void Execute(DocumentDatabase database) var due = TimeSpan.FromSeconds(ExpirationProcessTimerInSeconds); var deletionBatchSize = ExpirationProcessBatchSize; - logger.Info($"Running deletion of expired documents every {ExpirationProcessTimerInSeconds} seconds"); - logger.Info($"Deletion batch size set to {deletionBatchSize}"); - logger.Info($"Retention period for errors is {persistenceSettings.ErrorRetentionPeriod}"); + Logger.Info($"Running deletion of expired documents every {ExpirationProcessTimerInSeconds} seconds"); + Logger.Info($"Deletion batch size set to {deletionBatchSize}"); + Logger.Info($"Retention period for errors is {persistenceSettings.ErrorRetentionPeriod}"); var auditRetention = persistenceSettings.AuditRetentionPeriod; if (auditRetention.HasValue) { - logger.InfoFormat("Retention period for audits and saga history is {0}", persistenceSettings.AuditRetentionPeriod); + Logger.InfoFormat("Retention period for audits and saga history is {0}", persistenceSettings.AuditRetentionPeriod); } timer = new TimerJob( - token => ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database, persistenceSettings, token), due, due, e => { logger.Error("Error when trying to find expired documents", e); }); + token => ExpiredDocumentsCleaner.RunCleanup(deletionBatchSize, database, persistenceSettings, token), due, due, e => { Logger.Error("Error when trying to find expired documents", e); }); } int ExpirationProcessTimerInSeconds { get { - var expirationProcessTimerInSeconds = ExpirationProcessTimerInSecondsDefault; - - if (RavenBootstrapper.Settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.ExpirationProcessTimerInSecondsKey, out var expirationProcessTimerInSecondsString)) - { - expirationProcessTimerInSeconds = int.Parse(expirationProcessTimerInSecondsString); - } + var expirationProcessTimerInSeconds = settings.ExpirationProcessTimerInSeconds; if (expirationProcessTimerInSeconds < 0) { - logger.Error($"ExpirationProcessTimerInSeconds cannot be negative. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); + Logger.Error($"ExpirationProcessTimerInSeconds cannot be negative. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); return ExpirationProcessTimerInSecondsDefault; } if (expirationProcessTimerInSeconds > TimeSpan.FromHours(3).TotalSeconds) { - logger.Error($"ExpirationProcessTimerInSeconds cannot be larger than {TimeSpan.FromHours(3).TotalSeconds}. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); + Logger.Error($"ExpirationProcessTimerInSeconds cannot be larger than {TimeSpan.FromHours(3).TotalSeconds}. Defaulting to {ExpirationProcessTimerInSecondsDefault}"); return ExpirationProcessTimerInSecondsDefault; } @@ -100,21 +95,17 @@ public int ExpirationProcessBatchSize { get { - var expirationProcessBatchSize = ExpirationProcessBatchSizeDefault; + var expirationProcessBatchSize = settings.ExpirationProcessBatchSize; - if (RavenBootstrapper.Settings.PersisterSpecificSettings.TryGetValue(RavenBootstrapper.ExpirationProcessBatchSizeKey, out var expirationProcessBatchSizeString)) - { - expirationProcessBatchSize = int.Parse(expirationProcessBatchSizeString); - } if (expirationProcessBatchSize < 1) { - logger.Error($"ExpirationProcessBatchSize cannot be less than 1. Defaulting to {ExpirationProcessBatchSizeDefault}"); + Logger.Error($"ExpirationProcessBatchSize cannot be less than 1. Defaulting to {ExpirationProcessBatchSizeDefault}"); return ExpirationProcessBatchSizeDefault; } if (expirationProcessBatchSize < ExpirationProcessBatchSizeMinimum) { - logger.Error($"ExpirationProcessBatchSize cannot be less than {ExpirationProcessBatchSizeMinimum}. Defaulting to {ExpirationProcessBatchSizeDefault}"); + Logger.Error($"ExpirationProcessBatchSize cannot be less than {ExpirationProcessBatchSizeMinimum}. Defaulting to {ExpirationProcessBatchSizeDefault}"); return ExpirationProcessBatchSizeDefault; } @@ -122,12 +113,13 @@ public int ExpirationProcessBatchSize } } - const int ExpirationProcessTimerInSecondsDefault = 600; - const int ExpirationProcessBatchSizeDefault = 65512; + public const int ExpirationProcessTimerInSecondsDefault = 600; + public const int ExpirationProcessBatchSizeDefault = 65512; const int ExpirationProcessBatchSizeMinimum = 10240; + readonly RavenDBPersisterSettings settings = RavenBootstrapper.Settings; - ILog logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleanerBundle)); + static readonly ILog Logger = LogManager.GetLogger(typeof(ExpiredDocumentsCleanerBundle)); TimerJob timer; } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs b/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs index 10288ff84e..ebb0db036d 100644 --- a/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDb/ExternalIntegrationRequestsDataStore.cs @@ -20,7 +20,7 @@ class ExternalIntegrationRequestsDataStore , IHostedService , IAsyncDisposable { - public ExternalIntegrationRequestsDataStore(PersistenceSettings settings, IDocumentStore documentStore, CriticalError criticalError) + public ExternalIntegrationRequestsDataStore(RavenDBPersisterSettings settings, IDocumentStore documentStore, CriticalError criticalError) { this.settings = settings; this.documentStore = documentStore; @@ -202,7 +202,7 @@ public async ValueTask DisposeAsync() circuitBreaker?.Dispose(); } - readonly PersistenceSettings settings; + readonly RavenDBPersisterSettings settings; readonly IDocumentStore documentStore; readonly CancellationTokenSource tokenSource = new CancellationTokenSource(); readonly RepeatedFailuresOverTimeCircuitBreaker circuitBreaker; diff --git a/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs b/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs index f923d470ad..cbd15879aa 100644 --- a/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs +++ b/src/ServiceControl.Persistence.RavenDb/InternalsVisibleTo.cs @@ -4,3 +4,4 @@ [assembly: InternalsVisibleTo("ServiceControl.PersistenceTests")] [assembly: InternalsVisibleTo("ServiceControl.Persistence.Tests.RavenDb")] [assembly: InternalsVisibleTo("ServiceControl.AcceptanceTests.RavenDB")] +[assembly: InternalsVisibleTo("ServiceControl.MultiInstance.AcceptanceTests")] diff --git a/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs b/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs index ea8058d164..16863103e5 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenBootstrapper.cs @@ -2,8 +2,10 @@ { using System; using System.ComponentModel.Composition.Hosting; + using System.Globalization; using System.IO; using System.Runtime.Serialization; + using ByteSizeLib; using NServiceBus.Logging; using Raven.Abstractions.Data; using Raven.Client.Document; @@ -24,17 +26,13 @@ static class RavenBootstrapper public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; - public static PersistenceSettings Settings { get; private set; } + public static RavenDBPersisterSettings Settings { get; private set; } - public static void Configure(EmbeddableDocumentStore documentStore, PersistenceSettings settings) + public static void Configure(EmbeddableDocumentStore documentStore, RavenDBPersisterSettings settings) { Settings = settings; - var runInMemory = false; - if (settings.PersisterSpecificSettings.TryGetValue(RunInMemoryKey, out var runInMemoryString)) - { - runInMemory = bool.Parse(runInMemoryString); - } + var runInMemory = settings.RunInMemory; if (runInMemory) { @@ -42,7 +40,9 @@ public static void Configure(EmbeddableDocumentStore documentStore, PersistenceS } else { - if (!settings.PersisterSpecificSettings.TryGetValue(DatabasePathKey, out string dbPath)) + var dbPath = settings.DatabasePath; + + if (string.IsNullOrEmpty(dbPath)) { throw new InvalidOperationException($"{DatabasePathKey} is mandatory"); } @@ -54,11 +54,7 @@ public static void Configure(EmbeddableDocumentStore documentStore, PersistenceS documentStore.Listeners.RegisterListener(new SubscriptionsLegacyAddressConverter()); } - var exposeRavenDB = false; - if (settings.PersisterSpecificSettings.TryGetValue(ExposeRavenDBKey, out var exposeRavenDBString)) - { - exposeRavenDB = bool.Parse(exposeRavenDBString); - } + var exposeRavenDB = settings.ExposeRavenDB; documentStore.UseEmbeddedHttpServer = settings.MaintenanceMode || exposeRavenDB; documentStore.EnlistInDistributedTransactions = false; @@ -79,11 +75,7 @@ public static void Configure(EmbeddableDocumentStore documentStore, PersistenceS documentStore.Configuration.Settings["Raven/AnonymousAccess"] = "Admin"; documentStore.Configuration.Settings["Raven/Licensing/AllowAdminAnonymousAccessForCommercialUse"] = "true"; - var runCleanupBundle = true; - if (settings.PersisterSpecificSettings.TryGetValue(RunCleanupBundleKey, out var runCleanupBundleString)) - { - runCleanupBundle = bool.Parse(runCleanupBundleString); - } + var runCleanupBundle = settings.RunCleanupBundle; if (runCleanupBundle) { @@ -93,18 +85,20 @@ public static void Configure(EmbeddableDocumentStore documentStore, PersistenceS documentStore.Configuration.DisableClusterDiscovery = true; documentStore.Configuration.ResetIndexOnUncleanShutdown = true; - if (!settings.PersisterSpecificSettings.TryGetValue(DatabaseMaintenancePortKey, out var databaseMaintenancePort)) + if (settings.DatabaseMaintenancePort == 0) { throw new Exception($"{DatabaseMaintenancePortKey} is mandatory."); } - documentStore.Configuration.Port = int.Parse(databaseMaintenancePort); + documentStore.Configuration.Port = settings.DatabaseMaintenancePort; - if (!settings.PersisterSpecificSettings.TryGetValue(HostNameKey, out var hostName)) + if (string.IsNullOrEmpty(settings.HostName)) { throw new Exception($"{HostNameKey} is mandatory."); } + var hostName = settings.HostName; + documentStore.Configuration.HostName = hostName == "*" || hostName == "+" ? "localhost" : hostName; @@ -132,6 +126,30 @@ public static void Configure(EmbeddableDocumentStore documentStore, PersistenceS return clrtype; }; + + if (settings.MaintenanceMode) + { + Logger.InfoFormat($"RavenDB is now accepting requests on {settings.DatabaseMaintenanceUrl}"); + } + + if (settings.RunInMemory == false) + { + RecordStartup(); + } + } + + static void RecordStartup() + { + var dataSize = DataSize(); + var folderSize = FolderSize(); + + var startupMessage = $@" +------------------------------------------------------------- +Database Size: {ByteSize.FromBytes(dataSize).ToString("#.##", CultureInfo.InvariantCulture)} +Database Folder Size: {ByteSize.FromBytes(folderSize).ToString("#.##", CultureInfo.InvariantCulture)} +-------------------------------------------------------------"; + + Logger.Info(startupMessage); } public static string ReadLicense() @@ -152,7 +170,61 @@ static string ReadAllTextWithoutLocking(string path) } } - static SerializationBinder MigratedTypeAwareBinder = new MigratedTypeAwareBinder(); + static long DataSize() + { + var datafilePath = Path.Combine(Settings.DatabasePath, "data"); + + try + { + var info = new FileInfo(datafilePath); + if (!info.Exists) + { + return -1; + } + return info.Length; + } + catch + { + return -1; + } + } + + static long FolderSize() + { + try + { + var dir = new DirectoryInfo(Settings.DatabasePath); + var dirSize = DirSize(dir); + return dirSize; + } + catch + { + return -1; + } + } + + static long DirSize(DirectoryInfo d) + { + long size = 0; + if (d.Exists) + { + FileInfo[] fis = d.GetFiles(); + foreach (FileInfo fi in fis) + { + size += fi.Length; + } + + DirectoryInfo[] dis = d.GetDirectories(); + foreach (DirectoryInfo di in dis) + { + size += DirSize(di); + } + } + + return size; + } + + static readonly SerializationBinder MigratedTypeAwareBinder = new MigratedTypeAwareBinder(); static readonly ILog Logger = LogManager.GetLogger(typeof(RavenBootstrapper)); diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDBPersisterSettings.cs b/src/ServiceControl.Persistence.RavenDb/RavenDBPersisterSettings.cs new file mode 100644 index 0000000000..a15e1e4e3f --- /dev/null +++ b/src/ServiceControl.Persistence.RavenDb/RavenDBPersisterSettings.cs @@ -0,0 +1,24 @@ +using System; +using ServiceControl.Infrastructure.RavenDB.Expiration; +using ServiceControl.Operations; +using ServiceControl.Persistence; + +class RavenDBPersisterSettings : PersistenceSettings +{ + public string HostName { get; set; } = "localhost"; + public int DatabaseMaintenancePort { get; set; } = DatabaseMaintenancePortDefault; + public string DatabaseMaintenanceUrl => $"http://{HostName}:{DatabaseMaintenancePort}"; + public bool ExposeRavenDB { get; set; } + public int ExpirationProcessTimerInSeconds { get; set; } = ExpiredDocumentsCleanerBundle.ExpirationProcessTimerInSecondsDefault; + public int ExpirationProcessBatchSize { get; set; } = ExpiredDocumentsCleanerBundle.ExpirationProcessBatchSizeDefault; + public bool RunCleanupBundle { get; set; } + public bool RunInMemory { get; set; } + public int MinimumStorageLeftRequiredForIngestion { get; set; } = CheckMinimumStorageRequiredForIngestion.MinimumStorageLeftRequiredForIngestionDefault; + public int DataSpaceRemainingThreshold { get; set; } = CheckFreeDiskSpace.DataSpaceRemainingThresholdDefault; + public TimeSpan ErrorRetentionPeriod { get; set; } + public TimeSpan EventsRetentionPeriod { get; set; } + public TimeSpan? AuditRetentionPeriod { get; set; } + public int ExternalIntegrationsDispatchingBatchSize { get; set; } = 100; + + public const int DatabaseMaintenancePortDefault = 33334; +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs index 8be713ddf6..bab32ac5bd 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistence.cs @@ -19,7 +19,7 @@ class RavenDbPersistence : IPersistence { - public RavenDbPersistence(PersistenceSettings settings, EmbeddableDocumentStore documentStore, RavenStartup ravenStartup) + public RavenDbPersistence(RavenDBPersisterSettings settings, EmbeddableDocumentStore documentStore, RavenStartup ravenStartup) { this.settings = settings; this.documentStore = documentStore; @@ -28,6 +28,11 @@ public RavenDbPersistence(PersistenceSettings settings, EmbeddableDocumentStore public void Configure(IServiceCollection serviceCollection) { + if (settings.MaintenanceMode) + { + return; + } + serviceCollection.AddSingleton(settings); serviceCollection.AddSingleton(documentStore); @@ -84,7 +89,7 @@ public IPersistenceInstaller CreateInstaller() } readonly RavenStartup ravenStartup; - readonly PersistenceSettings settings; + readonly RavenDBPersisterSettings settings; readonly EmbeddableDocumentStore documentStore; } } diff --git a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs index c457809fa3..640e732654 100644 --- a/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence.RavenDb/RavenDbPersistenceConfiguration.cs @@ -1,41 +1,80 @@ namespace ServiceControl.Persistence.RavenDb { - using System.Collections.Generic; + using System; using Raven.Client.Embedded; + using ServiceControl.Infrastructure.RavenDB.Expiration; + using ServiceControl.Operations; class RavenDbPersistenceConfiguration : IPersistenceConfiguration { - //TODO: figure out what can be strongly typed - public const string LogPathKey = "LogPath"; - public const string DbPathKey = "DbPath"; public const string DataSpaceRemainingThresholdKey = "DataSpaceRemainingThreshold"; - public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; - public const string AuditRetentionPeriodKey = "AuditRetentionPeriod"; + const string AuditRetentionPeriodKey = "AuditRetentionPeriod"; + const string ErrorRetentionPeriodKey = "ErrorRetentionPeriod"; + const string EventsRetentionPeriodKey = "EventsRetentionPeriod"; + const string ExternalIntegrationsDispatchingBatchSizeKey = "ExternalIntegrationsDispatchingBatchSize"; + const string MaintenanceModeKey = "MaintenanceMode"; + public PersistenceSettings CreateSettings(Func tryReadSetting) + { + T GetRequiredSetting(string key) + { + var (exists, value) = tryReadSetting(key, typeof(T)); + + if (exists) + { + return (T)value; + } + + throw new Exception($"Setting {key} of type {typeof(T)} is required"); + } + + T GetSetting(string key, T defaultValue) + { + var (exists, value) = tryReadSetting(key, typeof(T)); - public string Name => "RavenDB35"; + if (exists) + { + return (T)value; + } + else + { + return defaultValue; + } + } - public IEnumerable ConfigurationKeys => new[]{ - RavenBootstrapper.DatabasePathKey, - RavenBootstrapper.HostNameKey, - RavenBootstrapper.DatabaseMaintenancePortKey, - RavenBootstrapper.ExposeRavenDBKey, - RavenBootstrapper.ExpirationProcessTimerInSecondsKey, - RavenBootstrapper.ExpirationProcessBatchSizeKey, - RavenBootstrapper.RunCleanupBundleKey, - RavenBootstrapper.RunInMemoryKey, - RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, - DataSpaceRemainingThresholdKey - }; + var settings = new RavenDBPersisterSettings + { + DatabasePath = GetSetting(RavenBootstrapper.DatabasePathKey, default), + HostName = GetSetting(RavenBootstrapper.HostNameKey, "localhost"), + DatabaseMaintenancePort = GetSetting(RavenBootstrapper.DatabaseMaintenancePortKey, RavenDBPersisterSettings.DatabaseMaintenancePortDefault), + ExposeRavenDB = GetSetting(RavenBootstrapper.ExposeRavenDBKey, false), + ExpirationProcessTimerInSeconds = GetSetting(RavenBootstrapper.ExpirationProcessTimerInSecondsKey, ExpiredDocumentsCleanerBundle.ExpirationProcessTimerInSecondsDefault), + ExpirationProcessBatchSize = GetSetting(RavenBootstrapper.ExpirationProcessBatchSizeKey, ExpiredDocumentsCleanerBundle.ExpirationProcessBatchSizeDefault), + RunCleanupBundle = GetSetting(RavenBootstrapper.RunCleanupBundleKey, true), + RunInMemory = GetSetting(RavenBootstrapper.RunInMemoryKey, false), + MinimumStorageLeftRequiredForIngestion = GetSetting(RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, CheckMinimumStorageRequiredForIngestion.MinimumStorageLeftRequiredForIngestionDefault), + DataSpaceRemainingThreshold = GetSetting(DataSpaceRemainingThresholdKey, CheckFreeDiskSpace.DataSpaceRemainingThresholdDefault), + ErrorRetentionPeriod = GetRequiredSetting(ErrorRetentionPeriodKey), + EventsRetentionPeriod = GetSetting(EventsRetentionPeriodKey, TimeSpan.FromDays(14)), + AuditRetentionPeriod = GetSetting(AuditRetentionPeriodKey, TimeSpan.Zero), + ExternalIntegrationsDispatchingBatchSize = GetSetting(ExternalIntegrationsDispatchingBatchSizeKey, 100), + MaintenanceMode = GetSetting(MaintenanceModeKey, false), + }; + + CheckFreeDiskSpace.Validate(settings); + CheckMinimumStorageRequiredForIngestion.Validate(settings); + return settings; + } public IPersistence Create(PersistenceSettings settings) { + var specificSettings = (RavenDBPersisterSettings)settings; + var documentStore = new EmbeddableDocumentStore(); - RavenBootstrapper.Configure(documentStore, settings); + RavenBootstrapper.Configure(documentStore, specificSettings); var ravenStartup = new RavenStartup(); - - return new RavenDbPersistence(settings, documentStore, ravenStartup); + return new RavenDbPersistence(specificSettings, documentStore, ravenStartup); } } -} +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs b/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs index 6204aa696d..e671a462df 100644 --- a/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs +++ b/src/ServiceControl.Persistence.RavenDb/SagaAudit/AuditRetentionCustomCheck.cs @@ -4,28 +4,29 @@ namespace ServiceControl.Persistence.RavenDb.SagaAudit using System; using System.Threading.Tasks; using NServiceBus.CustomChecks; - using Persistence; - using Persistence.RavenDb; using Raven.Client; using ServiceControl.SagaAudit; // This custom check stays in the Raven3.5 persister becuase the Raven5 persister will not store audit data class AuditRetentionCustomCheck : CustomCheck - { - readonly IDocumentStore _documentStore; - readonly bool _auditRetentionPeriodIsSet; + readonly IDocumentStore documentStore; + readonly bool auditRetentionPeriodIsSet; - public AuditRetentionCustomCheck(IDocumentStore documentStore, PersistenceSettings settings, TimeSpan? repeatAfter = null) + public AuditRetentionCustomCheck( + IDocumentStore documentStore, + RavenDBPersisterSettings settings, + TimeSpan? repeatAfter = null + ) : base("Saga Audit Data Retention", "ServiceControl Health", repeatAfter.HasValue ? repeatAfter : TimeSpan.FromHours(1)) { - _documentStore = documentStore; - _auditRetentionPeriodIsSet = settings.PersisterSpecificSettings.ContainsKey(RavenDbPersistenceConfiguration.AuditRetentionPeriodKey); + this.documentStore = documentStore; + auditRetentionPeriodIsSet = settings.AuditRetentionPeriod != default; } public override async Task PerformCheck() { - if (_auditRetentionPeriodIsSet) + if (auditRetentionPeriodIsSet) { return CheckResult.Pass; } @@ -40,7 +41,7 @@ public override async Task PerformCheck() async Task DetectSagaAuditData() { - using (var session = _documentStore.OpenAsyncSession()) + using (var session = documentStore.OpenAsyncSession()) { return await session.Query().AnyAsync(); } diff --git a/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj b/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj index fc725447dd..35addd01a2 100644 --- a/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj +++ b/src/ServiceControl.Persistence.RavenDb/ServiceControl.Persistence.RavenDb.csproj @@ -15,6 +15,7 @@ + diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs b/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs index 33c836cf72..2b8ca6fe9b 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/API/APIApprovals.cs @@ -7,7 +7,6 @@ using NServiceBus.CustomChecks; using NUnit.Framework; using Particular.Approvals; - using Persistence; using Persistence.RavenDb; using ServiceBus.Management.Infrastructure.Settings; @@ -36,18 +35,18 @@ static IEnumerable GetCustomChecks() var customCheckTypes = serviceControlTypes.Where(t => typeof(ICustomCheck).IsAssignableFrom(t)); - var objects = new List() + var supportedConstructorArguments = new List() { new Settings(), - new PersistenceSettings(TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, 1, false) + new RavenDBPersisterSettings { - PersisterSpecificSettings = { [RavenDbPersistenceConfiguration.DbPathKey] = "c:/" } + DatabasePath = "%TEMP%" } }; - object MapParam(ParameterInfo pi) + object MapConstructorParameter(ParameterInfo pi) { - foreach (var obj in objects) + foreach (var obj in supportedConstructorArguments) { if (obj.GetType() == pi.ParameterType) { @@ -63,7 +62,7 @@ object MapParam(ParameterInfo pi) { var constructor = customCheckType.GetConstructors().Single(); var constructorParameters = constructor.GetParameters() - .Select(MapParam) + .Select(MapConstructorParameter) .ToArray(); var instance = (ICustomCheck)constructor.Invoke(constructorParameters); yield return instance; diff --git a/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs b/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs index c803a50d9f..20abc166c5 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDb/TestPersistenceImpl.cs @@ -10,33 +10,30 @@ using NUnit.Framework; using Persistence; using Raven.Client; - using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence.RavenDb; sealed class TestPersistenceImpl : TestPersistence { + readonly RavenDBPersisterSettings settings = CreateSettings(); IDocumentStore documentStore; - public const string DatabaseMaintenancePort = "55554"; - - static PersistenceSettings CreateSettings() + static RavenDBPersisterSettings CreateSettings() { var retentionPeriod = TimeSpan.FromMinutes(1); - var settings = new PersistenceSettings(retentionPeriod, retentionPeriod, retentionPeriod, 100, false) + + var settings = new RavenDBPersisterSettings { - PersisterSpecificSettings = - { - [RavenBootstrapper.RunInMemoryKey] = bool.TrueString, - [RavenBootstrapper.HostNameKey] = "localhost", - [RavenBootstrapper.DatabaseMaintenancePortKey] = DatabaseMaintenancePort - } + AuditRetentionPeriod = retentionPeriod, + ErrorRetentionPeriod = retentionPeriod, + EventsRetentionPeriod = retentionPeriod, + RunInMemory = true, }; if (Debugger.IsAttached) { Console.WriteLine("If you get 'Access is denied' exception while debugging, comment out this line or create a URLACL reservervation:"); Console.WriteLine("> netsh http add urlacl http://+:55554/ user=Everyone"); - settings.PersisterSpecificSettings[RavenBootstrapper.ExposeRavenDBKey] = bool.TrueString; + settings.ExposeRavenDB = true; } return settings; @@ -44,12 +41,9 @@ static PersistenceSettings CreateSettings() public override void Configure(IServiceCollection services) { - var config = PersistenceConfigurationFactory.LoadPersistenceConfiguration(DataStoreConfig.RavenDB35PersistenceTypeFullyQualifiedName); - var settings = CreateSettings(); - - var instance = config.Create(settings); - PersistenceHostBuilderExtensions.CreatePersisterLifecyle(services, instance); + var persistence = new RavenDbPersistenceConfiguration().Create(CreateSettings()); + PersistenceHostBuilderExtensions.CreatePersisterLifecyle(services, persistence); services.AddHostedService(p => new Wrapper(this, p.GetRequiredService())); } @@ -79,7 +73,7 @@ public override void BlockToInspectDatabase() return; } - var url = $"http://localhost:{DatabaseMaintenancePort}/studio/index.html#databases/documents?&database=%3Csystem%3E"; + var url = $"http://localhost:{settings.DatabaseMaintenancePort}/studio/index.html#databases/documents?&database=%3Csystem%3E"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs index e7794196c8..2f5645fbcd 100644 --- a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs @@ -1,11 +1,10 @@ namespace ServiceControl.Persistence { - using System.Collections.Generic; + using System; public interface IPersistenceConfiguration { - string Name { get; } - IEnumerable ConfigurationKeys { get; } + PersistenceSettings CreateSettings(Func tryReadSetting); IPersistence Create(PersistenceSettings settings); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/PersistenceSettings.cs b/src/ServiceControl.Persistence/PersistenceSettings.cs index f037c991e3..6ee4345f73 100644 --- a/src/ServiceControl.Persistence/PersistenceSettings.cs +++ b/src/ServiceControl.Persistence/PersistenceSettings.cs @@ -1,31 +1,12 @@ namespace ServiceControl.Persistence { - using System; - using System.Collections.Generic; - - public class PersistenceSettings + /// + /// Marker interface used to serialize persister settings in REST API + /// + public abstract class PersistenceSettings { - public PersistenceSettings( - TimeSpan errorRetentionPeriod, - TimeSpan eventsRetentionPeriod, - TimeSpan? auditRetentionPeriod, - int externalIntegrationsDispatchingBatchSize, - bool maintenanceMode - ) - { - ErrorRetentionPeriod = errorRetentionPeriod; - EventsRetentionPeriod = eventsRetentionPeriod; - AuditRetentionPeriod = auditRetentionPeriod; - MaintenanceMode = maintenanceMode; - ExternalIntegrationsDispatchingBatchSize = externalIntegrationsDispatchingBatchSize; - } - - public IDictionary PersisterSpecificSettings { get; } = new Dictionary(); - - public bool MaintenanceMode { get; } - public TimeSpan ErrorRetentionPeriod { get; } - public TimeSpan EventsRetentionPeriod { get; } - public TimeSpan? AuditRetentionPeriod { get; } - public int ExternalIntegrationsDispatchingBatchSize { get; } + public bool MaintenanceMode { get; set; } + //HINT: This needs to be here so that ServerControl instance can add an instance specific metadata to tweak the DatabasePath value + public string DatabasePath { get; set; } } } \ No newline at end of file diff --git a/src/ServiceControl.Transports.RabbitMQ/RabbitMQConventionalRoutingTransportCustomization.cs b/src/ServiceControl.Transports.RabbitMQ/RabbitMQConventionalRoutingTransportCustomization.cs index 0fb42bd93b..e2d7518a8c 100644 --- a/src/ServiceControl.Transports.RabbitMQ/RabbitMQConventionalRoutingTransportCustomization.cs +++ b/src/ServiceControl.Transports.RabbitMQ/RabbitMQConventionalRoutingTransportCustomization.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Transports.RabbitMQ { + using System; using NServiceBus; using NServiceBus.Raw; @@ -61,6 +62,10 @@ static void CustomizeRawEndpoint(RawEndpointConfiguration endpointConfig, Transp static void ConfigureTransport(TransportExtensions transport, TransportSettings transportSettings, QueueType queueType) { + if (transportSettings.ConnectionString == null) + { + throw new InvalidOperationException("Connection string not configured"); + } transport.UseConventionalRoutingTopology(queueType); transport.Transactions(TransportTransactionMode.ReceiveOnly); transport.ApplyConnectionString(transportSettings.ConnectionString); diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs index d85d09c760..ba104d11f9 100644 --- a/src/ServiceControl.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs @@ -17,7 +17,6 @@ using PublicApiGenerator; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure.WebApi; - using ServiceControl.Persistence; using ServiceControlInstaller.Engine.Instances; [TestFixture] @@ -29,8 +28,12 @@ public void RootPathValue() var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost"); request.Properties.Add(HttpPropertyKeys.RequestContextKey, new HttpRequestContext { VirtualPathRoot = "/" }); - var persistenceSettings = new PersistenceSettings(TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, 1, false); - var controller = new RootController(new ActiveLicense { IsValid = true }, new LoggingSettings("testEndpoint"), new Settings(), persistenceSettings, httpClientFactory: null) + var controller = new RootController( + new ActiveLicense { IsValid = true }, + new LoggingSettings("testEndpoint"), + new Settings(), + httpClientFactory: null + ) { Url = new UrlHelper(request) }; @@ -132,7 +135,7 @@ public void TransportNames() Approver.Verify(publicTransportNames); } - [Test, Ignore("TODO: Deal with this once persister settings are properly managed")] + [Test] public void PlatformSampleSettings() { //HINT: Particular.PlatformSample includes a parameterized version of the ServiceControl.exe.config file. diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index 136ca67478..918b30ef20 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -2,7 +2,6 @@ "NotificationsFilter": null, "AllowMessageEditing": false, "MessageFilter": null, - "RunInMemory": false, "EmailDropFolder": null, "ValidateConfiguration": true, "ExternalIntegrationsDispatchingBatchSize": 100, @@ -10,30 +9,25 @@ "SkipQueueCreation": false, "RunCleanupBundle": false, "RootUrl": "http://localhost:8888/", - "DatabaseMaintenanceUrl": "http://localhost:33334", "ApiUrl": "http://localhost:8888/api", "StorageUrl": "http://localhost:8888/storage", "StagingQueue": "Particular.ServiceControl.staging", "Port": 8888, - "DatabaseMaintenancePort": 33334, "LicenseFileText": null, - "ExposeRavenDB": false, + "PersisterSpecificSettings": null, "PrintMetrics": false, "Hostname": "localhost", "VirtualDirectory": "", "HeartbeatGracePeriod": "00:00:40", "TransportType": "ServiceControl.Transports.Learning.LearningTransportCustomization, ServiceControl.Transports.Learning", - "DbPath": "C:\\DB", "ErrorLogQueue": "error.log", "ErrorQueue": "error", "ForwardErrorMessages": false, "IngestErrorMessages": true, "RunRetryProcessor": true, - "ExpirationProcessTimerInSeconds": 600, "AuditRetentionPeriod": null, "ErrorRetentionPeriod": "10.00:00:00", "EventsRetentionPeriod": "14.00:00:00", - "ExpirationProcessBatchSize": 65512, "ServiceName": "Particular.ServiceControl", "HttpDefaultConnectionLimit": 100, "TransportConnectionString": null, @@ -43,8 +37,7 @@ "RetryHistoryDepth": 10, "RemoteInstances": [], "DataSpaceRemainingThreshold": 20, - "MinimumStorageLeftRequiredForIngestion": 5, "EnableFullTextSearchOnBodies": true, "DisableHealthChecks": false, - "ExposeApi": true, + "ExposeApi": true } \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/app.config b/src/ServiceControl.UnitTests/app.config index 03d8dcadf7..6b43ce87f0 100644 --- a/src/ServiceControl.UnitTests/app.config +++ b/src/ServiceControl.UnitTests/app.config @@ -1,8 +1,9 @@ - + + - + diff --git a/src/ServiceControl/App.config b/src/ServiceControl/App.config index fada510b70..0f471c0fbc 100644 --- a/src/ServiceControl/App.config +++ b/src/ServiceControl/App.config @@ -5,71 +5,71 @@ These settings are only here so that we can debug ServiceControl while developin --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ServiceControl/Bootstrapper.cs b/src/ServiceControl/Bootstrapper.cs index 87cb5ab53b..ef32019750 100644 --- a/src/ServiceControl/Bootstrapper.cs +++ b/src/ServiceControl/Bootstrapper.cs @@ -3,13 +3,10 @@ namespace Particular.ServiceControl using System; using System.Collections.Generic; using System.Diagnostics; - using System.Globalization; - using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; - using ByteSizeLib; using global::ServiceControl.CustomChecks; using global::ServiceControl.ExternalIntegrations; using global::ServiceControl.Infrastructure.BackgroundTasks; @@ -70,9 +67,6 @@ public Bootstrapper(Settings settings, EndpointConfiguration configuration, Logg void CreateHost() { - var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); - RecordStartup(loggingSettings, configuration); if (!string.IsNullOrWhiteSpace(settings.LicenseFileText)) @@ -115,7 +109,7 @@ void CreateHost() services.AddSingleton(sp => HttpClientFactory); }) .UseLicenseCheck() - .SetupPersistence(persistenceSettings, persistenceConfiguration) + .SetupPersistence(settings) .UseMetrics(settings.PrintMetrics) .UseNServiceBus(context => { @@ -144,65 +138,10 @@ TransportSettings MapSettings(Settings settings) return transportSettings; } - long DataSize() - { - var datafilePath = Path.Combine(settings.DbPath, "data"); - - try - { - var info = new FileInfo(datafilePath); - if (!info.Exists) - { - return -1; - } - return info.Length; - } - catch - { - return -1; - } - } - - long FolderSize() - { - try - { - var dir = new DirectoryInfo(settings.DbPath); - var dirSize = DirSize(dir); - return dirSize; - } - catch - { - return -1; - } - } - - static long DirSize(DirectoryInfo d) - { - long size = 0; - if (d.Exists) - { - FileInfo[] fis = d.GetFiles(); - foreach (FileInfo fi in fis) - { - size += fi.Length; - } - - DirectoryInfo[] dis = d.GetDirectories(); - foreach (DirectoryInfo di in dis) - { - size += DirSize(di); - } - } - - return size; - } - void RecordStartup(LoggingSettings loggingSettings, EndpointConfiguration endpointConfiguration) { var version = FileVersionInfo.GetVersionInfo(typeof(Bootstrapper).Assembly.Location).ProductVersion; - var dataSize = DataSize(); - var folderSize = FolderSize(); + var startupMessage = $@" ------------------------------------------------------------- ServiceControl Version: {version} @@ -210,8 +149,6 @@ Audit Retention Period (optional): {settings.AuditRetentionPeriod} Error Retention Period: {settings.ErrorRetentionPeriod} Ingest Error Messages: {settings.IngestErrorMessages} Forwarding Error Messages: {settings.ForwardErrorMessages} -Database Size: {ByteSize.FromBytes(dataSize).ToString("#.##", CultureInfo.InvariantCulture)} -Database Folder Size: {ByteSize.FromBytes(folderSize).ToString("#.##", CultureInfo.InvariantCulture)} ServiceControl Logging Level: {loggingSettings.LoggingLevel} RavenDB Logging Level: {loggingSettings.RavenDBLogLevel} Selected Transport Customization: {settings.TransportType} @@ -224,10 +161,8 @@ Audit Retention Period (optional): {settings.AuditRetentionPeriod} Settings = new { settings.ApiUrl, - settings.DatabaseMaintenancePort, settings.ErrorLogQueue, settings.DataSpaceRemainingThreshold, - settings.DbPath, settings.ErrorQueue, settings.ForwardErrorMessages, settings.HttpDefaultConnectionLimit, @@ -242,7 +177,7 @@ Audit Retention Period (optional): {settings.AuditRetentionPeriod} settings.SkipQueueCreation, settings.EnableFullTextSearchOnBodies, settings.TransportType, - settings.AllowMessageEditing + settings.AllowMessageEditing, }, LoggingSettings = loggingSettings }); diff --git a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs index 53ba63a40c..a0e69e2709 100644 --- a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs +++ b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs @@ -20,10 +20,7 @@ public override async Task Execute(HostArguments args, Settings settings) settings.RunRetryProcessor = false; settings.DisableHealthChecks = true; - var busConfiguration = new EndpointConfiguration(settings.ServiceName); - var assemblyScanner = busConfiguration.AssemblyScanner(); - assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); - var tokenSource = new CancellationTokenSource(); + EndpointConfiguration busConfiguration = CreateEndpointConfiguration(settings); var loggingSettings = new LoggingSettings(settings.ServiceName, LogLevel.Info, LogLevel.Info); var bootstrapper = new Bootstrapper(settings, busConfiguration, loggingSettings); @@ -32,20 +29,32 @@ public override async Task Execute(HostArguments args, Settings settings) var importFailedErrors = host.Services.GetRequiredService(); - Console.CancelKeyPress += (sender, eventArgs) => { tokenSource.Cancel(); }; - - try - { - await importFailedErrors.Run(tokenSource.Token); - } - catch (OperationCanceledException) - { - // no-op - } - finally + using (var tokenSource = new CancellationTokenSource()) { - await host.StopAsync(CancellationToken.None); + Console.CancelKeyPress += (sender, eventArgs) => { tokenSource.Cancel(); }; + + try + { + await importFailedErrors.Run(tokenSource.Token); + } + catch (OperationCanceledException) + { + // no-op + } + finally + { + await host.StopAsync(CancellationToken.None); + } } } + + protected virtual EndpointConfiguration CreateEndpointConfiguration(Settings settings) + { + var busConfiguration = new EndpointConfiguration(settings.ServiceName); + var assemblyScanner = busConfiguration.AssemblyScanner(); + assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); + + return busConfiguration; + } } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/MaintCommand.cs b/src/ServiceControl/Hosting/Commands/MaintCommand.cs deleted file mode 100644 index 5b4648765d..0000000000 --- a/src/ServiceControl/Hosting/Commands/MaintCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ServiceControl.Hosting.Commands -{ - using System.Threading.Tasks; - using Particular.ServiceControl; - using Particular.ServiceControl.Commands; - using Particular.ServiceControl.Hosting; - using ServiceBus.Management.Infrastructure.Settings; - - class MaintCommand : AbstractCommand - { - public override Task Execute(HostArguments args, Settings settings) => MaintenanceBootstrapper.Run(args, settings); - } -} \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs new file mode 100644 index 0000000000..3b7438ea0c --- /dev/null +++ b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs @@ -0,0 +1,33 @@ +namespace ServiceControl.Hosting.Commands +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Particular.ServiceControl; + using Particular.ServiceControl.Commands; + using Particular.ServiceControl.Hosting; + using ServiceBus.Management.Infrastructure.Settings; + + class MaintenanceModeCommand : AbstractCommand + { + public override async Task Execute(HostArguments args, Settings settings) + { + var bootstrapper = new MaintenanceBootstrapper(settings); + var hostBuilder = bootstrapper.HostBuilder; + + if (args.RunAsWindowsService) + { + hostBuilder.UseWindowsService(); + } + else + { + await Console.Out.WriteLineAsync("RavenDB Maintenance Mode - Press CTRL+C to exit"); + + hostBuilder.UseConsoleLifetime(); + } + + await hostBuilder.Build().RunAsync(); + + } + } +} \ No newline at end of file diff --git a/src/ServiceControl/Hosting/HostArguments.cs b/src/ServiceControl/Hosting/HostArguments.cs index 0e58cc5fbb..4cfb3615e5 100644 --- a/src/ServiceControl/Hosting/HostArguments.cs +++ b/src/ServiceControl/Hosting/HostArguments.cs @@ -13,7 +13,7 @@ class HostArguments { public HostArguments(string[] args) { - if (ConfigFileSettingsReader.Read("MaintenanceMode")) + if (SettingsReader.ConfigFile.Read("MaintenanceMode")) { args = args.Concat(new[] { @@ -41,7 +41,7 @@ public HostArguments(string[] args) { Commands = new List { - typeof(MaintCommand) + typeof(MaintenanceModeCommand) }; executionMode = ExecutionMode.Maintenance; } diff --git a/src/ServiceControl/Infrastructure/Settings/LoggingSettings.cs b/src/ServiceControl/Infrastructure/Settings/LoggingSettings.cs index 970d0759b0..986468da2d 100644 --- a/src/ServiceControl/Infrastructure/Settings/LoggingSettings.cs +++ b/src/ServiceControl/Infrastructure/Settings/LoggingSettings.cs @@ -11,7 +11,7 @@ public LoggingSettings(string serviceName, LogLevel defaultLevel = null, LogLeve { LoggingLevel = InitializeLevel("LogLevel", defaultLevel ?? LogLevel.Info); RavenDBLogLevel = InitializeLevel("RavenDBLogLevel", defaultRavenDBLevel ?? LogLevel.Warn); - LogPath = Environment.ExpandEnvironmentVariables(SettingsReader.Read("LogPath", logPath ?? DefaultLogPathForInstance(serviceName))); + LogPath = Environment.ExpandEnvironmentVariables(SettingsReader.Read("LogPath", logPath ?? DefaultLogPathForInstance(serviceName))); LogToConsole = logToConsole; } @@ -25,7 +25,7 @@ public LoggingSettings(string serviceName, LogLevel defaultLevel = null, LogLeve LogLevel InitializeLevel(string key, LogLevel defaultLevel) { - var levelText = SettingsReader.Read(key); + var levelText = SettingsReader.Read(key); if (string.IsNullOrWhiteSpace(levelText)) { return defaultLevel; diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index ca48c5e0be..aa2f5e46a1 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -10,11 +10,18 @@ namespace ServiceBus.Management.Infrastructure.Settings using NServiceBus.Logging; using NServiceBus.Transport; using ServiceControl.Infrastructure.WebApi; + using ServiceControl.Persistence; using ServiceControl.Transports; public class Settings { - public Settings(string serviceName = null, string transportType = null, string persisterType = null) + public Settings( + string serviceName = null, + string transportType = null, + string persisterType = null, + bool? forwardErrorMessages = default, + TimeSpan? errorRetentionPeriod = default + ) { ServiceName = serviceName; @@ -24,7 +31,7 @@ public Settings(string serviceName = null, string transportType = null, string p } // Overwrite the service name if it is specified in ENVVAR, reg, or config file - ServiceName = SettingsReader.Read("InternalQueueName", ServiceName); + ServiceName = SettingsReader.Read("InternalQueueName", ServiceName); ErrorQueue = GetErrorQueue(); ErrorLogQueue = GetErrorLogQueue(ErrorQueue); @@ -32,26 +39,24 @@ public Settings(string serviceName = null, string transportType = null, string p TryLoadLicenseFromConfig(); TransportConnectionString = GetConnectionString(); - TransportType = transportType ?? SettingsReader.Read("TransportType"); - PersistenceType = persisterType ?? SettingsReader.Read("PersistenceType"); + TransportType = transportType ?? SettingsReader.Read("TransportType"); + PersistenceType = persisterType ?? SettingsReader.Read("PersistenceType"); AuditRetentionPeriod = GetAuditRetentionPeriod(); - ForwardErrorMessages = GetForwardErrorMessages(); - ErrorRetentionPeriod = GetErrorRetentionPeriod(); + ForwardErrorMessages = forwardErrorMessages ?? GetForwardErrorMessages(); + ErrorRetentionPeriod = errorRetentionPeriod ?? GetErrorRetentionPeriod(); EventsRetentionPeriod = GetEventRetentionPeriod(); - Port = SettingsReader.Read("Port", 33333); - DatabaseMaintenancePort = SettingsReader.Read("DatabaseMaintenancePort", 33334); // TODO: Should not be in Core but in the persister implementation + Port = SettingsReader.Read("Port", 33333); ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(30); - MaximumConcurrencyLevel = SettingsReader.Read("MaximumConcurrencyLevel", 10); - RetryHistoryDepth = SettingsReader.Read("RetryHistoryDepth", 10); - HttpDefaultConnectionLimit = SettingsReader.Read("HttpDefaultConnectionLimit", 100); - AllowMessageEditing = SettingsReader.Read("AllowMessageEditing"); - NotificationsFilter = SettingsReader.Read("NotificationsFilter"); + MaximumConcurrencyLevel = SettingsReader.Read("MaximumConcurrencyLevel", 10); + RetryHistoryDepth = SettingsReader.Read("RetryHistoryDepth", 10); + HttpDefaultConnectionLimit = SettingsReader.Read("HttpDefaultConnectionLimit", 100); + AllowMessageEditing = SettingsReader.Read("AllowMessageEditing"); + NotificationsFilter = SettingsReader.Read("NotificationsFilter"); RemoteInstances = GetRemoteInstances().ToArray(); DataSpaceRemainingThreshold = GetDataSpaceRemainingThreshold(); - DbPath = GetDbPath(); TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); - DisableExternalIntegrationsPublishing = SettingsReader.Read("DisableExternalIntegrationsPublishing", false); - EnableFullTextSearchOnBodies = SettingsReader.Read("EnableFullTextSearchOnBodies", true); + DisableExternalIntegrationsPublishing = SettingsReader.Read("DisableExternalIntegrationsPublishing", false); + EnableFullTextSearchOnBodies = SettingsReader.Read("EnableFullTextSearchOnBodies", true); } public string NotificationsFilter { get; set; } @@ -64,9 +69,9 @@ public Settings(string serviceName = null, string transportType = null, string p //HINT: acceptance tests only public string EmailDropFolder { get; set; } - public bool ValidateConfiguration => SettingsReader.Read("ValidateConfig", true); + public bool ValidateConfiguration => SettingsReader.Read("ValidateConfig", true); - public int ExternalIntegrationsDispatchingBatchSize => SettingsReader.Read("ExternalIntegrationsDispatchingBatchSize", 100); + public int ExternalIntegrationsDispatchingBatchSize => SettingsReader.Read("ExternalIntegrationsDispatchingBatchSize", 100); public bool DisableExternalIntegrationsPublishing { get; set; } @@ -89,8 +94,6 @@ public string RootUrl } } - public string DatabaseMaintenanceUrl => $"http://{Hostname}:{DatabaseMaintenancePort}"; - public string ApiUrl => $"{RootUrl}api"; public string StorageUrl => $"{RootUrl}storage"; @@ -98,16 +101,14 @@ public string RootUrl public string StagingQueue => $"{ServiceName}.staging"; public int Port { get; set; } - public int DatabaseMaintenancePort { get; set; } // TODO: Should not be in Core but in the persister implementation public string LicenseFileText { get; set; } - public Dictionary PersisterSpecificSettings { get; set; } = new Dictionary(); + public PersistenceSettings PersisterSpecificSettings { get; set; } - public bool ExposeRavenDB => SettingsReader.Read("ExposeRavenDB"); // TODO: Should not be in Core but in the persister implementation - public bool PrintMetrics => SettingsReader.Read("PrintMetrics"); - public string Hostname => SettingsReader.Read("Hostname", "localhost"); - public string VirtualDirectory => SettingsReader.Read("VirtualDirectory", string.Empty); + public bool PrintMetrics => SettingsReader.Read("PrintMetrics"); + public string Hostname => SettingsReader.Read("Hostname", "localhost"); + public string VirtualDirectory => SettingsReader.Read("VirtualDirectory", string.Empty); public TimeSpan HeartbeatGracePeriod { @@ -115,7 +116,7 @@ public TimeSpan HeartbeatGracePeriod { try { - return TimeSpan.Parse(SettingsReader.Read("HeartbeatGracePeriod", "00:00:40")); + return TimeSpan.Parse(SettingsReader.Read("HeartbeatGracePeriod", "00:00:40")); } catch (Exception ex) { @@ -127,7 +128,6 @@ public TimeSpan HeartbeatGracePeriod public string TransportType { get; set; } public string PersistenceType { get; private set; } - public string DbPath { get; set; } // TODO: Should not be in Core but in the persister implementation public string ErrorLogQueue { get; set; } public string ErrorQueue { get; set; } @@ -178,7 +178,7 @@ public TransportCustomization LoadTransportCustomization() public string GetConnectionString() { - var settingsValue = SettingsReader.Read("ConnectionString"); + var settingsValue = SettingsReader.Read("ConnectionString"); if (settingsValue != null) { return settingsValue; @@ -190,7 +190,7 @@ public string GetConnectionString() string GetErrorQueue() { - var value = SettingsReader.Read("ServiceBus", "ErrorQueue", "error"); + var value = SettingsReader.Read("ServiceBus", "ErrorQueue", "error"); if (value == null) { @@ -216,7 +216,7 @@ string GetErrorLogQueue(string errorQueue) return null; } - var value = SettingsReader.Read("ServiceBus", "ErrorLogQueue", null); + var value = SettingsReader.Read("ServiceBus", "ErrorLogQueue", null); if (value == null) { @@ -227,29 +227,9 @@ string GetErrorLogQueue(string errorQueue) return value; } - string GetDbPath() - { - var host = Hostname; - if (host == "*") - { - host = "%"; - } - - var dbFolder = $"{host}-{Port}"; - - if (!string.IsNullOrEmpty(VirtualDirectory)) - { - dbFolder += $"-{SanitiseFolderName(VirtualDirectory)}"; - } - - var defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Particular", "ServiceControl", dbFolder); - - return SettingsReader.Read("DbPath", defaultPath); - } - static bool GetForwardErrorMessages() { - var forwardErrorMessages = NullableSettingsReader.Read("ForwardErrorMessages"); + var forwardErrorMessages = SettingsReader.Read("ForwardErrorMessages"); if (forwardErrorMessages.HasValue) { return forwardErrorMessages.Value; @@ -258,14 +238,9 @@ static bool GetForwardErrorMessages() throw new Exception("ForwardErrorMessages settings is missing, please make sure it is included."); } - static string SanitiseFolderName(string folderName) - { - return Path.GetInvalidPathChars().Aggregate(folderName, (current, c) => current.Replace(c, '-')); - } - TimeSpan GetEventRetentionPeriod() { - var valueRead = SettingsReader.Read("EventRetentionPeriod"); + var valueRead = SettingsReader.Read("EventRetentionPeriod"); if (valueRead != null) { if (TimeSpan.TryParse(valueRead, out var result)) @@ -295,7 +270,7 @@ TimeSpan GetEventRetentionPeriod() TimeSpan GetErrorRetentionPeriod() { string message; - var valueRead = SettingsReader.Read("ErrorRetentionPeriod"); + var valueRead = SettingsReader.Read("ErrorRetentionPeriod"); if (valueRead == null) { message = "ErrorRetentionPeriod settings is missing, please make sure it is included."; @@ -332,7 +307,7 @@ TimeSpan GetErrorRetentionPeriod() TimeSpan? GetAuditRetentionPeriod() { string message; - var valueRead = SettingsReader.Read("AuditRetentionPeriod"); + var valueRead = SettingsReader.Read("AuditRetentionPeriod"); if (valueRead == null) { return null; @@ -367,7 +342,7 @@ TimeSpan GetErrorRetentionPeriod() TimeSpan GetTimeToRestartErrorIngestionAfterFailure() { string message; - var valueRead = SettingsReader.Read("TimeToRestartErrorIngestionAfterFailure"); + var valueRead = SettingsReader.Read("TimeToRestartErrorIngestionAfterFailure"); if (valueRead == null) { return TimeSpan.FromSeconds(60); @@ -401,7 +376,7 @@ TimeSpan GetTimeToRestartErrorIngestionAfterFailure() static IList GetRemoteInstances() { - var valueRead = SettingsReader.Read("RemoteInstances"); + var valueRead = SettingsReader.Read("RemoteInstances"); if (!string.IsNullOrEmpty(valueRead)) { return ParseRemoteInstances(valueRead); @@ -436,7 +411,7 @@ static string Subscope(string address) int GetDataSpaceRemainingThreshold() { string message; - var threshold = SettingsReader.Read("DataSpaceRemainingThreshold", DataSpaceRemainingThresholdDefault); + var threshold = SettingsReader.Read("DataSpaceRemainingThreshold", DataSpaceRemainingThresholdDefault); if (threshold < 0) { message = $"{nameof(DataSpaceRemainingThreshold)} is invalid, minimum value is 0."; @@ -456,7 +431,7 @@ int GetDataSpaceRemainingThreshold() void TryLoadLicenseFromConfig() { - LicenseFileText = SettingsReader.Read("LicenseText"); + LicenseFileText = SettingsReader.Read("LicenseText"); } ILog logger = LogManager.GetLogger(typeof(Settings)); diff --git a/src/ServiceControl/Infrastructure/WebApi/RootController.cs b/src/ServiceControl/Infrastructure/WebApi/RootController.cs index 0356938396..02573ffd22 100644 --- a/src/ServiceControl/Infrastructure/WebApi/RootController.cs +++ b/src/ServiceControl/Infrastructure/WebApi/RootController.cs @@ -10,15 +10,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Particular.ServiceControl.Licensing; - using Persistence; using ServiceBus.Management.Infrastructure.Settings; class RootController : ApiController { - public RootController(ActiveLicense license, LoggingSettings loggingSettings, Settings settings, PersistenceSettings persistenceSettings, Func httpClientFactory) + public RootController(ActiveLicense license, LoggingSettings loggingSettings, Settings settings, Func httpClientFactory) { this.settings = settings; - this.persistenceSettings = persistenceSettings; this.license = license; this.loggingSettings = loggingSettings; this.httpClientFactory = httpClientFactory; @@ -45,8 +43,8 @@ public OkNegotiatedContentResult Urls() EndpointsMessagesUrl = baseUrl + "endpoints/{name}/messages/{?page,per_page,direction,sort}", AuditCountUrl = baseUrl + "endpoints/{name}/audit-count", - Name = SettingsReader.Read("Name", "ServiceControl"), - Description = SettingsReader.Read("Description", "The management backend for the Particular Service Platform"), + Name = SettingsReader.Read("Name", "ServiceControl"), + Description = SettingsReader.Read("Description", "The management backend for the Particular Service Platform"), LicenseStatus = license.IsValid ? "valid" : "invalid", LicenseDetails = baseUrl + "license", Configuration = baseUrl + "configuration", @@ -69,7 +67,6 @@ public OkNegotiatedContentResult Config() Host = new { settings.ServiceName, - RavenDBPath = settings.DbPath, Logging = new { loggingSettings.LogPath, @@ -87,7 +84,7 @@ public OkNegotiatedContentResult Config() settings.HttpDefaultConnectionLimit, settings.ExternalIntegrationsDispatchingBatchSize }, - PersistenceSettings = persistenceSettings, + PersistenceSettings = settings.PersisterSpecificSettings, Transport = new { settings.TransportType, @@ -157,7 +154,6 @@ public async Task RemoteConfig() readonly LoggingSettings loggingSettings; readonly ActiveLicense license; readonly Settings settings; - readonly PersistenceSettings persistenceSettings; readonly Func httpClientFactory; static readonly JsonSerializer jsonSerializer = JsonSerializer.Create(JsonNetSerializerSettings.CreateDefault()); diff --git a/src/ServiceControl/MaintenanceBootstrapper.cs b/src/ServiceControl/MaintenanceBootstrapper.cs index 805539ea9b..c1062f14a7 100644 --- a/src/ServiceControl/MaintenanceBootstrapper.cs +++ b/src/ServiceControl/MaintenanceBootstrapper.cs @@ -1,37 +1,15 @@ namespace Particular.ServiceControl { - using System; - using System.Threading.Tasks; using global::ServiceControl.Persistence; - using Hosting; using Microsoft.Extensions.Hosting; using ServiceBus.Management.Infrastructure.Settings; - static class MaintenanceBootstrapper + class MaintenanceBootstrapper { - public static async Task Run(HostArguments args, Settings settings) - { - var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings, maintenanceMode: true); + public IHostBuilder HostBuilder { get; set; } - var hostBuilder = new HostBuilder() - .SetupPersistence(persistenceSettings, persistenceConfiguration); - - if (args.RunAsWindowsService) - { - hostBuilder.UseWindowsService(); - - await hostBuilder.Build().RunAsync(); - } - else - { - await Console.Out.WriteLineAsync($"RavenDB is now accepting requests on {settings.DatabaseMaintenanceUrl}"); - await Console.Out.WriteLineAsync("RavenDB Maintenance Mode - Press CTRL+C to exit"); - - hostBuilder.UseConsoleLifetime(); - - await hostBuilder.Build().RunAsync(); - } - } + public MaintenanceBootstrapper(Settings settings) => + HostBuilder = new HostBuilder() + .SetupPersistence(settings, maintenanceMode: true); } } \ No newline at end of file diff --git a/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs b/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs deleted file mode 100644 index ef50d5995a..0000000000 --- a/src/ServiceControl/Persistence/PersistenceConfigurationFactory.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace ServiceControl.Persistence -{ - using System; - using ServiceBus.Management.Infrastructure.Settings; - - // Added recently by David - - static class PersistenceConfigurationFactory - { - public static IPersistenceConfiguration LoadPersistenceConfiguration(string persistenceType) - { - try - { - var foundPersistenceType = PersistenceManifestLibrary.Find(persistenceType); - - var customizationType = Type.GetType(foundPersistenceType, true); - - return (IPersistenceConfiguration)Activator.CreateInstance(customizationType); - } - catch (Exception e) - { - throw new Exception($"Could not load persistence customization type {persistenceType}.", e); - } - } - - public static PersistenceSettings BuildPersistenceSettings(this IPersistenceConfiguration persistenceConfiguration, Settings settings, bool maintenanceMode = false) - { - // TODO: Audit instance passed settings.AuditRetentionPeriod, settings.EnableFullTextSearchOnBodies, settings.MaxBodySizeToStore - are those needed? - // And then remove settings parameter if not needed (but it probably is for something) - var persistenceSettings = new PersistenceSettings( - settings.ErrorRetentionPeriod, - settings.EventsRetentionPeriod, - settings.AuditRetentionPeriod, - settings.ExternalIntegrationsDispatchingBatchSize, - maintenanceMode - ); - - foreach (var keyPair in settings.PersisterSpecificSettings) - { - persistenceSettings.PersisterSpecificSettings[keyPair.Key] = keyPair.Value; - } - - foreach (var key in persistenceConfiguration.ConfigurationKeys) - { - var value = SettingsReader.Read("ServiceControl", key, null); - if (!string.IsNullOrWhiteSpace(value)) - { - persistenceSettings.PersisterSpecificSettings[key] = value; - } - } - - return persistenceSettings; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl/Persistence/PersistenceFactory.cs b/src/ServiceControl/Persistence/PersistenceFactory.cs new file mode 100644 index 0000000000..9345e97b56 --- /dev/null +++ b/src/ServiceControl/Persistence/PersistenceFactory.cs @@ -0,0 +1,75 @@ +namespace ServiceControl.Persistence +{ + using System; + using System.IO; + using System.Linq; + using System.Web.Hosting; + using ServiceBus.Management.Infrastructure.Settings; + + static class PersistenceFactory + { + public static IPersistence Create(Settings settings, bool maintenanceMode = false) + { + var persistenceConfiguration = CreatePersistenceConfiguration(settings.PersistenceType); + + //HINT: This is false when executed from acceptance tests + if (settings.PersisterSpecificSettings == null) + { + (bool, object) TryRead(string name, Type type) + { + var exists = SettingsReader.TryRead(name, type: type, out object value); + return (exists, value); + }; + + settings.PersisterSpecificSettings = persistenceConfiguration.CreateSettings(TryRead); + } + + settings.PersisterSpecificSettings.MaintenanceMode = maintenanceMode; + settings.PersisterSpecificSettings.DatabasePath = BuildDataBasePath(settings); + + var persistence = persistenceConfiguration.Create(settings.PersisterSpecificSettings); + return persistence; + } + + static IPersistenceConfiguration CreatePersistenceConfiguration(string persistenceType) + { + try + { + var foundPersistenceType = PersistenceManifestLibrary.Find(persistenceType); + var customizationType = Type.GetType(foundPersistenceType, true); + var persistenceConfiguration = (IPersistenceConfiguration)Activator.CreateInstance(customizationType); + return persistenceConfiguration; + } + catch (Exception e) + { + throw new Exception($"Could not load persistence customization type {persistenceType}.", e); + } + } + + static string BuildDataBasePath(Settings settings) + { + var host = settings.Hostname; + if (host == "*") + { + host = "%"; + } + + var dbFolder = $"{host}-{settings.Port}"; + + if (!string.IsNullOrEmpty(settings.VirtualDirectory)) + { + dbFolder += $"-{SanitiseFolderName(settings.VirtualDirectory)}"; + } + + var defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Particular", "ServiceControl", dbFolder); + + return SettingsReader.Read("DbPath", defaultPath); + } + + static string SanitiseFolderName(string folderName) + { + return Path.GetInvalidPathChars().Aggregate(folderName, (current, c) => current.Replace(c, '-')); + } + + } +} \ No newline at end of file diff --git a/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs b/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs index 0c8942d57a..ef34ba16d2 100644 --- a/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs +++ b/src/ServiceControl/Persistence/PersistenceHostBuilderExtensions.cs @@ -2,12 +2,13 @@ namespace ServiceControl.Persistence { using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using ServiceBus.Management.Infrastructure.Settings; static class PersistenceHostBuilderExtensions { - public static IHostBuilder SetupPersistence(this IHostBuilder hostBuilder, PersistenceSettings persistenceSettings, IPersistenceConfiguration persistenceConfiguration) + public static IHostBuilder SetupPersistence(this IHostBuilder hostBuilder, Settings settings, bool maintenanceMode = false) { - var persistence = persistenceConfiguration.Create(persistenceSettings); + var persistence = PersistenceFactory.Create(settings, maintenanceMode); hostBuilder.ConfigureServices(serviceCollection => { diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index a9be8ce4b8..20b548d915 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -30,7 +30,6 @@ - diff --git a/src/ServiceControl/SetupBootstrapper.cs b/src/ServiceControl/SetupBootstrapper.cs index d5bdaeadef..dffadfa0aa 100644 --- a/src/ServiceControl/SetupBootstrapper.cs +++ b/src/ServiceControl/SetupBootstrapper.cs @@ -35,9 +35,8 @@ public async Task Run(string username) await installationTask(); } - var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings.PersistenceType); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); - var persistence = persistenceConfiguration.Create(persistenceSettings); + var persistence = PersistenceFactory.Create(settings); + var installer = persistence.CreateInstaller(); await installer.Install(); diff --git a/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs b/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs index 3024ef0aa4..56f972e49e 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs @@ -48,7 +48,7 @@ public async Task CreateInstanceMSMQ() { var installer = new UnattendServiceControlInstaller(new TestLogger(), DeploymentCache); var instanceName = "Test.ServiceControl.Msmq"; - var root = Path.Combine(@"c:\Test", instanceName); + var root = Path.Combine(Path.GetTempPath(), instanceName); var details = ServiceControlNewInstance.CreateWithDefaultPersistence(DeploymentCache); details.DisplayName = instanceName.Replace(".", " "); @@ -108,7 +108,7 @@ public async Task ChangeConfigTests() logger.Info("Changing LogPath"); msmqTestInstance = InstanceFinder.ServiceControlInstances().First(p => p.Name.Equals("Test.ServiceControl.MSMQ", StringComparison.OrdinalIgnoreCase)); - msmqTestInstance.LogPath = @"c:\temp\testloggingchange"; + msmqTestInstance.LogPath = Path.Combine(Path.GetTempPath(), "testloggingchange"); await installer.Update(msmqTestInstance, true).ConfigureAwait(false); Assert.IsTrue(msmqTestInstance.Service.Status == ServiceControllerStatus.Running, "Update Logging changed failed"); @@ -139,6 +139,6 @@ void RemoveAltMSMQQueues() } } - const string DeploymentCache = @"..\..\..\..\Zip"; + const string DeploymentCache = @"..\..\..\..\..\Zip"; } } \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine.UnitTests/Validation/PathsValidationTests.cs b/src/ServiceControlInstaller.Engine.UnitTests/Validation/PathsValidationTests.cs index dd25273aa2..f627c15cd4 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/Validation/PathsValidationTests.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/Validation/PathsValidationTests.cs @@ -9,14 +9,16 @@ [TestFixture] public class PathsValidationTests { + static readonly string BasePath = Path.Combine(Path.GetTempPath(), "PathsValidationTests"); + [Test] public void CheckPathsAreUnique_ShouldThrow() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1\bin"; - newInstance.LogPath = @"c:\test\1\bin"; - newInstance.DBPath = @"c:\test\1\bin"; + newInstance.InstallPath = Path.Combine(BasePath, "bin"); + newInstance.LogPath = Path.Combine(BasePath, "bin"); + newInstance.DBPath = Path.Combine(BasePath, "bin"); var p = new PathsValidator(newInstance); @@ -29,9 +31,9 @@ public void CheckPathsAreUnique_ShouldSucceed() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1\bin"; - newInstance.LogPath = @"c:\test\1\log"; - newInstance.DBPath = @"c:\test\1\db"; + newInstance.InstallPath = Path.Combine(BasePath, "bin"); + newInstance.LogPath = Path.Combine(BasePath, "log"); + newInstance.DBPath = Path.Combine(BasePath, "db"); var p = new PathsValidator(newInstance); Assert.DoesNotThrow(() => p.CheckPathsAreUnique()); @@ -43,9 +45,9 @@ public void CheckPathsAreValid_ShouldSucceed() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1\bin"; - newInstance.LogPath = @"c:\test\1\bin"; - newInstance.DBPath = @"c:\test\1\bin"; + newInstance.InstallPath = Path.Combine(BasePath, "bin"); + newInstance.LogPath = Path.Combine(BasePath, "bin"); + newInstance.DBPath = Path.Combine(BasePath, "bin"); var p = new PathsValidator(newInstance); Assert.DoesNotThrow(() => p.CheckPathsAreValid()); @@ -56,7 +58,7 @@ public void CheckPathsAreValid_ShouldThrow() { //Invalid path var instance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - instance.InstallPath = @"?>c:\test\1\bin"; + instance.InstallPath = @"?>" + Path.Combine(BasePath, "bin"); var p = new PathsValidator(instance); var ex = Assert.Throws(() => p.CheckPathsAreValid()); @@ -82,9 +84,9 @@ public void CheckNoNestedPaths_ShouldThrow() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1"; - newInstance.LogPath = @"c:\test\1\log"; - newInstance.DBPath = @"c:\test\1\db"; + newInstance.InstallPath = BasePath; + newInstance.LogPath = Path.Combine(BasePath, "log"); + newInstance.DBPath = Path.Combine(BasePath, "db"); var p = new PathsValidator(newInstance); var ex = Assert.Throws(() => p.CheckNoNestedPaths()); @@ -96,9 +98,9 @@ public void CheckNoNestedSiblingPaths_ShouldSucceed() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1\servicecontrol"; - newInstance.LogPath = @"c:\test\1\servicecontrollog"; - newInstance.DBPath = @"c:\test\1\servicecontroldb"; + newInstance.InstallPath = Path.Combine(BasePath, "servicecontrol"); + newInstance.LogPath = Path.Combine(BasePath, "servicecontrollog"); + newInstance.DBPath = Path.Combine(BasePath, "servicecontroldb"); var p = new PathsValidator(newInstance); Assert.DoesNotThrow(() => p.CheckNoNestedPaths()); @@ -109,9 +111,9 @@ public void CheckNoNestedPaths_ShouldSucceed() { var newInstance = ServiceControlNewInstance.CreateWithDefaultPersistence(); - newInstance.InstallPath = @"c:\test\1\bin"; - newInstance.LogPath = @"c:\test\1\log"; - newInstance.DBPath = @"c:\test\1\db"; + newInstance.InstallPath = Path.Combine(BasePath, "bin"); + newInstance.LogPath = Path.Combine(BasePath, "log"); + newInstance.DBPath = Path.Combine(BasePath, "db"); var p = new PathsValidator(newInstance); diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlCoreTransports.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlCoreTransports.cs index 0d14b025c8..e9ab522232 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlCoreTransports.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlCoreTransports.cs @@ -188,7 +188,7 @@ public class ServiceControlCoreTransports Name = TransportNames.LearningTransport, TypeName = "ServiceControl.Transports.Learning.LearningTransportCustomization, ServiceControl.Transports.Learning", ZipName = "LearningTransport", - SampleConnectionString = "C:\\tmp\\", + SampleConnectionString = "%TEMP%\\.learningtransport", AvailableInSCMU = IncludeLearningTransport(), Matches = name => name.Equals(TransportNames.LearningTransport, StringComparison.OrdinalIgnoreCase) || name.Equals("ServiceControl.Transports.Learning.LearningTransportCustomization, ServiceControl.Transports.Learning", StringComparison.OrdinalIgnoreCase)