diff --git a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index 77b2ef7666..f718f461aa 100644 --- a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -28,5 +28,6 @@ "MaximumConcurrencyLevel": null, "ServiceControlQueueAddress": "Particular.ServiceControl", "TimeToRestartAuditIngestionAfterFailure": "00:01:00", - "EnableFullTextSearchOnBodies": true + "EnableFullTextSearchOnBodies": true, + "ShutdownTimeout": "00:00:05" } \ No newline at end of file diff --git a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs index 13d630456e..c38e023869 100644 --- a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs @@ -5,11 +5,13 @@ namespace ServiceControl.Audit; using System.Threading; using System.Threading.Tasks; using Auditing; +using Hosting; using Infrastructure; using Infrastructure.Settings; using Microsoft.AspNetCore.HttpLogging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Logging; using Monitoring; using NLog.Extensions.Logging; @@ -45,7 +47,7 @@ public static void AddServiceControlAudit(this IHostApplicationBuilder builder, var transportCustomization = TransportFactory.Create(transportSettings); transportCustomization.AddTransportForAudit(services, transportSettings); - services.Configure(options => options.ShutdownTimeout = TimeSpan.FromSeconds(30)); + services.Configure(options => options.ShutdownTimeout = settings.ShutdownTimeout); services.AddSingleton(settings); services.AddSingleton(); @@ -100,7 +102,11 @@ public static void AddServiceControlAudit(this IHostApplicationBuilder builder, services.AddHostedService(); } - builder.Services.AddWindowsService(); + if (WindowsServiceHelpers.IsWindowsService()) + { + // The if is added for clarity, internally AddWindowsService has a similar logic + builder.AddWindowsServiceWithRequestTimeout(); + } } public static void AddServiceControlAuditInstallers(this IHostApplicationBuilder builder, Settings settings) diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs index 0f23b2e771..4a92ce6b41 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs @@ -2,7 +2,9 @@ { using System.Threading.Tasks; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Hosting.WindowsServices; using Persistence; + using ServiceControl.Hosting; using Settings; class MaintenanceModeCommand : AbstractCommand @@ -17,7 +19,11 @@ public override async Task Execute(HostArguments args, Settings settings) var hostBuilder = Host.CreateApplicationBuilder(); hostBuilder.Services.AddPersistence(persistenceSettings, persistenceConfiguration); - hostBuilder.Services.AddWindowsService(); + if (WindowsServiceHelpers.IsWindowsService()) + { + // The if is added for clarity, internally AddWindowsService has a similar logic + hostBuilder.AddWindowsServiceWithRequestTimeout(); + } var host = hostBuilder.Build(); await host.RunAsync(); diff --git a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs index c0b1dfb892..9108c7d87f 100644 --- a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs @@ -49,6 +49,7 @@ public Settings(string transportType = null, string persisterType = null, Loggin ServiceControlQueueAddress = SettingsReader.Read(SettingsRootNamespace, "ServiceControlQueueAddress"); TimeToRestartAuditIngestionAfterFailure = GetTimeToRestartAuditIngestionAfterFailure(); EnableFullTextSearchOnBodies = SettingsReader.Read(SettingsRootNamespace, "EnableFullTextSearchOnBodies", true); + ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); } @@ -152,6 +153,12 @@ public int MaxBodySizeToStore public bool EnableFullTextSearchOnBodies { get; set; } + // The default value is set to the maximum allowed time by the most + // restrictive hosting platform, which is Linux containers. Linux + // containers allow for a maximum of 10 seconds. We set it to 5 to + // allow for cancellation and logging to take place + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + public TransportSettings ToTransportSettings() { var transportSettings = new TransportSettings diff --git a/src/ServiceControl.Audit/ServiceControl.Audit.csproj b/src/ServiceControl.Audit/ServiceControl.Audit.csproj index 8f41ba97b1..6e0a889e9f 100644 --- a/src/ServiceControl.Audit/ServiceControl.Audit.csproj +++ b/src/ServiceControl.Audit/ServiceControl.Audit.csproj @@ -20,6 +20,7 @@ + diff --git a/src/ServiceControl.Hosting/IHostApplicationBuilderExtensions.cs b/src/ServiceControl.Hosting/IHostApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..303595b337 --- /dev/null +++ b/src/ServiceControl.Hosting/IHostApplicationBuilderExtensions.cs @@ -0,0 +1,17 @@ +namespace ServiceControl.Hosting; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.WindowsServices; + +public static class IHostApplicationBuilderExtensions +{ + public static void AddWindowsServiceWithRequestTimeout(this IHostApplicationBuilder builder) + { + if (WindowsServiceHelpers.IsWindowsService()) + { + builder.Services.AddWindowsService(); + builder.Services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Hosting/ServiceControl.Hosting.csproj b/src/ServiceControl.Hosting/ServiceControl.Hosting.csproj new file mode 100644 index 0000000000..074686312c --- /dev/null +++ b/src/ServiceControl.Hosting/ServiceControl.Hosting.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + + + + + + + + diff --git a/src/ServiceControl.Hosting/WindowsServiceWithRequestTimeout.cs b/src/ServiceControl.Hosting/WindowsServiceWithRequestTimeout.cs new file mode 100644 index 0000000000..0e85c8c79b --- /dev/null +++ b/src/ServiceControl.Hosting/WindowsServiceWithRequestTimeout.cs @@ -0,0 +1,40 @@ +namespace ServiceControl.Hosting; + +using System; +using System.Runtime.Versioning; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.WindowsServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +[SupportedOSPlatform("windows")] +sealed class WindowsServiceWithRequestTimeout : WindowsServiceLifetime +{ + static readonly TimeSpan CancellationDuration = TimeSpan.FromSeconds(5); + readonly HostOptions hostOptions; + + public WindowsServiceWithRequestTimeout(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions optionsAccessor, IOptions windowsServiceOptionsAccessor) + : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor) + { + hostOptions = optionsAccessor.Value; + } + + protected override void OnStop() + { + var logger = NLog.LogManager.GetCurrentClassLogger(); + var additionalTime = hostOptions.ShutdownTimeout + CancellationDuration; + + logger.Info("OnStop invoked, going to ask for additional time: {additionalTime}", additionalTime); + RequestAdditionalTime(additionalTime); + logger.Info("Additional time requested"); + + base.OnStop(); + } + + protected override void OnShutdown() + { + var logger = NLog.LogManager.GetCurrentClassLogger(); + logger.Info("OnShutdown invoked, process may exit ungracefully"); + base.OnShutdown(); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.UnitTests/ApprovalFiles/SettingsTests.PlatformSampleSettings.approved.txt b/src/ServiceControl.Monitoring.UnitTests/ApprovalFiles/SettingsTests.PlatformSampleSettings.approved.txt index b855b960fc..5d32f42fbb 100644 --- a/src/ServiceControl.Monitoring.UnitTests/ApprovalFiles/SettingsTests.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.Monitoring.UnitTests/ApprovalFiles/SettingsTests.PlatformSampleSettings.approved.txt @@ -15,5 +15,6 @@ "EndpointUptimeGracePeriod": "00:00:40", "RootUrl": "http://localhost:9999/", "MaximumConcurrencyLevel": null, - "ServiceControlThroughputDataQueue": "ServiceControl.ThroughputData" + "ServiceControlThroughputDataQueue": "ServiceControl.ThroughputData", + "ShutdownTimeout": "00:00:05" } \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs index 06e6e2b386..90adce523d 100644 --- a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs @@ -5,6 +5,7 @@ namespace ServiceControl.Monitoring; using System.Threading; using System.Threading.Tasks; using Configuration; +using Hosting; using Infrastructure; using Infrastructure.BackgroundTasks; using Infrastructure.Extensions; @@ -13,6 +14,7 @@ namespace ServiceControl.Monitoring; using Microsoft.AspNetCore.HttpLogging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using NServiceBus; @@ -39,7 +41,13 @@ public static void AddServiceControlMonitoring(this IHostApplicationBuilder host var transportCustomization = TransportFactory.Create(transportSettings); transportCustomization.AddTransportForMonitoring(services, transportSettings); - services.AddWindowsService(); + services.Configure(options => options.ShutdownTimeout = settings.ShutdownTimeout); + + if (WindowsServiceHelpers.IsWindowsService()) + { + // The if is added for clarity, internally AddWindowsService has a similar logic + hostBuilder.AddWindowsServiceWithRequestTimeout(); + } services.AddSingleton(settings); services.AddSingleton(); diff --git a/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj b/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj index 6314e0699b..35246ea1b8 100644 --- a/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj +++ b/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj @@ -18,6 +18,7 @@ + diff --git a/src/ServiceControl.Monitoring/Settings.cs b/src/ServiceControl.Monitoring/Settings.cs index 6900919e78..3412307042 100644 --- a/src/ServiceControl.Monitoring/Settings.cs +++ b/src/ServiceControl.Monitoring/Settings.cs @@ -39,6 +39,7 @@ public Settings(LoggingSettings loggingSettings = null, string transportType = n EndpointUptimeGracePeriod = TimeSpan.Parse(SettingsReader.Read(SettingsRootNamespace, "EndpointUptimeGracePeriod", "00:00:40")); MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel"); ServiceControlThroughputDataQueue = SettingsReader.Read(SettingsRootNamespace, "ServiceControlThroughputDataQueue", "ServiceControl.ThroughputData"); + ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); } @@ -68,6 +69,12 @@ public Settings(LoggingSettings loggingSettings = null, string transportType = n public string ServiceControlThroughputDataQueue { get; set; } + // The default value is set to the maximum allowed time by the most + // restrictive hosting platform, which is Linux containers. Linux + // containers allow for a maximum of 10 seconds. We set it to 5 to + // allow for cancellation and logging to take place + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + public TransportSettings ToTransportSettings() { var transportSettings = new TransportSettings diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index 98ed016ffb..a23480af26 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -42,5 +42,6 @@ "MaximumConcurrencyLevel": null, "RetryHistoryDepth": 10, "RemoteInstances": [], - "DisableHealthChecks": false + "DisableHealthChecks": false, + "ShutdownTimeout": "00:00:05" } \ No newline at end of file diff --git a/src/ServiceControl.sln b/src/ServiceControl.sln index 7c57134866..fa8d9a30e6 100644 --- a/src/ServiceControl.sln +++ b/src/ServiceControl.sln @@ -183,7 +183,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Transports.P EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceControl.Transports.PostgreSql.Tests", "ServiceControl.Transports.PostgreSql.Tests\ServiceControl.Transports.PostgreSql.Tests.csproj", "{18DBEEF5-42EE-4C1D-A05B-87B21C067D53}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetupProcessFake", "SetupProcessFake\SetupProcessFake.csproj", "{36D53BA0-C1E1-4D74-81AE-C33B40C84958}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceControl.Hosting", "ServiceControl.Hosting\ServiceControl.Hosting.csproj", "{481032A1-1106-4C6C-B75E-512F2FB08882}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetupProcessFake", "SetupProcessFake\SetupProcessFake.csproj", "{5837F789-69B9-44BE-B114-3A2880F06CAB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -999,18 +1001,30 @@ Global {18DBEEF5-42EE-4C1D-A05B-87B21C067D53}.Release|x64.Build.0 = Release|Any CPU {18DBEEF5-42EE-4C1D-A05B-87B21C067D53}.Release|x86.ActiveCfg = Release|Any CPU {18DBEEF5-42EE-4C1D-A05B-87B21C067D53}.Release|x86.Build.0 = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|Any CPU.Build.0 = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|x64.ActiveCfg = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|x64.Build.0 = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|x86.ActiveCfg = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Debug|x86.Build.0 = Debug|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|Any CPU.ActiveCfg = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|Any CPU.Build.0 = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|x64.ActiveCfg = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|x64.Build.0 = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|x86.ActiveCfg = Release|Any CPU - {36D53BA0-C1E1-4D74-81AE-C33B40C84958}.Release|x86.Build.0 = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|Any CPU.Build.0 = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|x64.ActiveCfg = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|x64.Build.0 = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|x86.ActiveCfg = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Debug|x86.Build.0 = Debug|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|Any CPU.ActiveCfg = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|Any CPU.Build.0 = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|x64.ActiveCfg = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|x64.Build.0 = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|x86.ActiveCfg = Release|Any CPU + {481032A1-1106-4C6C-B75E-512F2FB08882}.Release|x86.Build.0 = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|x64.Build.0 = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Debug|x86.Build.0 = Debug|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|Any CPU.Build.0 = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|x64.ActiveCfg = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|x64.Build.0 = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|x86.ActiveCfg = Release|Any CPU + {5837F789-69B9-44BE-B114-3A2880F06CAB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1094,7 +1108,8 @@ Global {51F5504E-E915-40EC-B96E-CA700A57982C} = {80C55E70-4B7A-4EF2-BB9E-C42F8DB0495D} {448CBDCF-718D-4BC7-8F7C-099C9A362B59} = {A21A1A89-0B07-4E87-8E3C-41D9C280DCB8} {18DBEEF5-42EE-4C1D-A05B-87B21C067D53} = {E0E45F22-35E3-4AD8-B09E-EFEA5A2F18EE} - {36D53BA0-C1E1-4D74-81AE-C33B40C84958} = {927A078A-E271-4878-A153-86D71AE510E2} + {481032A1-1106-4C6C-B75E-512F2FB08882} = {9AF9D3C7-E859-451B-BA4D-B954D289213A} + {5837F789-69B9-44BE-B114-3A2880F06CAB} = {927A078A-E271-4878-A153-86D71AE510E2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3B9E5B72-F580-465A-A22C-2D2148AF4EB4} diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs index 0c365fecbc..50d0733fb9 100644 --- a/src/ServiceControl/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs @@ -5,6 +5,7 @@ namespace Particular.ServiceControl using System.Runtime.InteropServices; using global::ServiceControl.CustomChecks; using global::ServiceControl.ExternalIntegrations; + using global::ServiceControl.Hosting; using global::ServiceControl.Infrastructure.BackgroundTasks; using global::ServiceControl.Infrastructure.DomainEvents; using global::ServiceControl.Infrastructure.Metrics; @@ -17,6 +18,7 @@ namespace Particular.ServiceControl using Microsoft.AspNetCore.HttpLogging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using NServiceBus; @@ -51,7 +53,7 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S var transportCustomization = TransportFactory.Create(transportSettings); transportCustomization.AddTransportForPrimary(services, transportSettings); - services.Configure(options => options.ShutdownTimeout = TimeSpan.FromSeconds(30)); + services.Configure(options => options.ShutdownTimeout = settings.ShutdownTimeout); services.AddSingleton(); services.AddSingleton(); @@ -95,7 +97,11 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S hostBuilder.AddInternalCustomChecks(); } - hostBuilder.Services.AddWindowsService(); + if (WindowsServiceHelpers.IsWindowsService()) + { + // The if is added for clarity, internally AddWindowsService has a similar logic + hostBuilder.AddWindowsServiceWithRequestTimeout(); + } hostBuilder.AddServiceControlComponents(settings, transportCustomization, ServiceControlMainInstance.Components); } diff --git a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs index 95f1a13421..f3ba85e132 100644 --- a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs @@ -2,6 +2,7 @@ { using System.Threading.Tasks; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Hosting.WindowsServices; using Particular.ServiceControl.Hosting; using Persistence; using ServiceBus.Management.Infrastructure.Settings; @@ -13,7 +14,11 @@ public override async Task Execute(HostArguments args, Settings settings) var hostBuilder = Host.CreateApplicationBuilder(); hostBuilder.Services.AddPersistence(settings, maintenanceMode: true); - hostBuilder.Services.AddWindowsService(); + if (WindowsServiceHelpers.IsWindowsService()) + { + // The if is added for clarity, internally AddWindowsService has a similar logic + hostBuilder.AddWindowsServiceWithRequestTimeout(); + } var host = hostBuilder.Build(); await host.RunAsync(); diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 04b7a232bb..cf6dbeffe7 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -65,6 +65,7 @@ public Settings( TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); DisableExternalIntegrationsPublishing = SettingsReader.Read(SettingsRootNamespace, "DisableExternalIntegrationsPublishing", false); TrackInstancesInitialValue = SettingsReader.Read(SettingsRootNamespace, "TrackInstancesInitialValue", true); + ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); } @@ -180,6 +181,12 @@ public TimeSpan HeartbeatGracePeriod public bool DisableHealthChecks { get; set; } + // The default value is set to the maximum allowed time by the most + // restrictive hosting platform, which is Linux containers. Linux + // containers allow for a maximum of 10 seconds. We set it to 5 to + // allow for cancellation and logging to take place + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + public string GetConnectionString() { var settingsValue = SettingsReader.Read(SettingsRootNamespace, "ConnectionString"); diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 9cd517591d..04f5956ccf 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -24,6 +24,7 @@ + diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.RavenDB.approved.txt b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.RavenDB.approved.txt index 16ddbafe24..5050490958 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.RavenDB.approved.txt +++ b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/InstallationTests.Audit_install_should_write_expected_config_file.RavenDB.approved.txt @@ -10,6 +10,7 @@ + diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/NewAuditInstanceTests.Should_install_modern_raven_for_new_instances.approved.txt b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/NewAuditInstanceTests.Should_install_modern_raven_for_new_instances.approved.txt index 16ddbafe24..5050490958 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/NewAuditInstanceTests.Should_install_modern_raven_for_new_instances.approved.txt +++ b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/NewAuditInstanceTests.Should_install_modern_raven_for_new_instances.approved.txt @@ -10,6 +10,7 @@ + diff --git a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/AppConfig.cs b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/AppConfig.cs index 3a0c42d636..ce845f1ca2 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/AppConfig.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/AppConfig.cs @@ -22,6 +22,12 @@ public void Save() settings.Set(SettingsList.TransportType, details.TransportPackage.Name, version); settings.Set(SettingsList.ErrorQueue, details.ErrorQueue); + // Windows services allow a maximum of 125 seconds when stopping a service. + // When shutting down or restarting the OS we have no control over the + // shutdown timeout. This is by the installer engine that is run _only_ on + // Windows via SCMU or PowerShell + settings.Set(SettingsList.ShutdownTimeout, "00:02:00", version); + // Retired settings settings.RemoveIfRetired(SettingsList.EndpointName, version); diff --git a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs index c8aed89e42..e7651cd20e 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs @@ -21,5 +21,10 @@ public static class SettingsList public static SettingInfo LogPath = new() { Name = "Monitoring/LogPath" }; public static SettingInfo TransportType = new() { Name = "Monitoring/TransportType" }; public static SettingInfo ErrorQueue = new() { Name = "Monitoring/ErrorQueue" }; + public static SettingInfo ShutdownTimeout = new() + { + Name = "Monitoring/ShutdownTimeout", + SupportedFrom = new SemanticVersion(6, 4, 1) + }; } } \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/AuditInstanceSettingsList.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/AuditInstanceSettingsList.cs index a2b0800cb7..d557bf4bcf 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/AuditInstanceSettingsList.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/AuditInstanceSettingsList.cs @@ -35,5 +35,11 @@ public static class AuditInstanceSettingsList Name = "ServiceControl.Audit/EnableFullTextSearchOnBodies", SupportedFrom = new SemanticVersion(4, 17, 0) }; + + public static readonly SettingInfo ShutdownTimeout = new() + { + Name = "ServiceControl.Audit/ShutdownTimeout", + SupportedFrom = new SemanticVersion(6, 4, 1) + }; } } \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs index 950058cfa7..c00b1d8ee5 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs @@ -32,6 +32,12 @@ protected override void UpdateSettings() settings.Set(ServiceControlSettings.EnableFullTextSearchOnBodies, details.EnableFullTextSearchOnBodies.ToString(), version); settings.Set(ServiceControlSettings.RemoteInstances, RemoteInstanceConverter.ToJson(details.RemoteInstances), version); + // Windows services allow a maximum of 125 seconds when stopping a service. + // When shutting down or restarting the OS we have no control over the + // shutdown timeout. This is by the installer engine that is run _only_ on + // Windows via SCMU or PowerShell + settings.Set(ServiceControlSettings.ShutdownTimeout, "00:02:00", version); + // Retired settings settings.RemoveIfRetired(ServiceControlSettings.AuditQueue, version); settings.RemoveIfRetired(ServiceControlSettings.AuditLogQueue, version); diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAuditAppConfig.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAuditAppConfig.cs index bcbae4a3e1..81386283eb 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAuditAppConfig.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAuditAppConfig.cs @@ -30,6 +30,12 @@ protected override void UpdateSettings() settings.Set(AuditInstanceSettingsList.ServiceControlQueueAddress, instance.ServiceControlQueueAddress); settings.Set(AuditInstanceSettingsList.EnableFullTextSearchOnBodies, instance.EnableFullTextSearchOnBodies.ToString().ToLowerInvariant(), version); + // Windows services allow a maximum of 125 seconds when stopping a service. + // When shutting down or restarting the OS we have no control over the + // shutdown timeout. This is by the installer engine that is run _only_ on + // Windows via SCMU or PowerShell + settings.Set(AuditInstanceSettingsList.ShutdownTimeout, "00:02:00", version); + foreach (var manifestSetting in instance.PersistenceManifest.Settings) { if (!settings.AllKeys.Contains(manifestSetting.Name)) diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs index bd0b5e4c87..f982b8bc34 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs @@ -89,5 +89,11 @@ public static class ServiceControlSettings Name = "ServiceControl/EnableFullTextSearchOnBodies", SupportedFrom = new SemanticVersion(4, 17, 0) }; + + public static SettingInfo ShutdownTimeout = new() + { + Name = "ServiceControl/ShutdownTimeout", + SupportedFrom = new SemanticVersion(6, 4, 1) + }; } } \ No newline at end of file