From 236b8ee205e43cb5d4b7e39cf5b7d4b3ce7ba754 Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 13:35:17 +1000 Subject: [PATCH] Propagate cancellation tokens through monitoring hosting and background services This change continues the effort to consistently apply optional `CancellationToken` parameters to asynchronous methods across the monitoring project, including hosting commands, background services, and acceptance test infrastructure. Cancellation tokens are now propagated to underlying asynchronous operations, ensuring proper responsiveness to cancellation and retiring the remaining cancellation analyzer debt for the monitoring projects. --- .../.editorconfig | 6 ---- .../PerformanceTests.cs | 28 ++++++++++--------- .../ServiceControlComponentBehavior.cs | 2 ++ .../ServiceControlComponentRunner.cs | 8 +++--- src/ServiceControl.Monitoring/.editorconfig | 7 ----- .../Hosting/Commands/AbstractCommand.cs | 3 +- .../Hosting/Commands/CommandRunner.cs | 5 ++-- .../Hosting/Commands/RunCommand.cs | 3 +- .../Hosting/Commands/SetupCommand.cs | 5 ++-- .../RemoveExpiredEndpointInstances.cs | 2 +- .../ReportThroughputHostedService.cs | 5 +++- .../Licensing/LicenseCheckHostedService.cs | 4 +-- 12 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/.editorconfig b/src/ServiceControl.Monitoring.AcceptanceTests/.editorconfig index bbcb303765..5f68a610b3 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/.editorconfig +++ b/src/ServiceControl.Monitoring.AcceptanceTests/.editorconfig @@ -2,9 +2,3 @@ # Justification: Test project dotnet_diagnostic.CA2007.severity = none - -# Cancellation analyzer debt. These fire because Particular.Analyzers is no longer pinned to 0.9.0. -# They are scheduled work, not accepted exceptions: remove a line once this project has no -# violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0003.severity = none -dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs index 59ca54e8bd..009dd08a0c 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs @@ -64,9 +64,9 @@ public async Task GetMonitoredEndpointsQueryTest(int numberOfEndpoints, int numb var reporters = new[] { - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))) + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => criticalTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName)), source.Token), + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => processingTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName)), source.Token), + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => retriesStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName)), source.Token) }.SelectMany(i => i).ToArray(); var histogram = CreateTimeHistogram(); @@ -123,9 +123,9 @@ public async Task GetMonitoredSingleEndpointQueryTest(int numberOfInstances, int var reporters = new[] { - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, getter())), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, getter())), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, getter())) + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => criticalTimeStore.Store(e, i, getter()), source.Token), + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => processingTimeStore.Store(e, i, getter()), source.Token), + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, (e, i) => retriesStore.Store(e, i, getter()), source.Token) }.SelectMany(i => i).ToArray(); var histogram = CreateTimeHistogram(); @@ -149,21 +149,21 @@ public async Task GetMonitoredSingleEndpointQueryTest(int numberOfInstances, int Report("Reporters", reportFinalHistogram, TimeSpan.FromMilliseconds(20)); } - static IEnumerable> BuildReporters(int sendReportEvery, int numberOfEntriesInReport, EndpointInstanceId[] instances, CancellationTokenSource source, Action store) + static IEnumerable> BuildReporters(int sendReportEvery, int numberOfEntriesInReport, EndpointInstanceId[] instances, Action store, CancellationToken cancellationToken) { return instances - .Select(instance => StartReporter(sendReportEvery, numberOfEntriesInReport, source, instance, store)) + .Select(instance => StartReporter(sendReportEvery, numberOfEntriesInReport, instance, store, cancellationToken)) .ToArray(); } - static Task StartReporter(int sendReportEvery, int numberOfEntriesInReport, CancellationTokenSource source, EndpointInstanceId instance, Action store) + static Task StartReporter(int sendReportEvery, int numberOfEntriesInReport, EndpointInstanceId instance, Action store, CancellationToken cancellationToken) { return Task.Run(async () => { var entries = new RawMessage.Entry[numberOfEntriesInReport]; var histogram = CreateTimeHistogram(); - while (source.IsCancellationRequested == false) + while (cancellationToken.IsCancellationRequested == false) { var now = DateTime.UtcNow; @@ -178,7 +178,9 @@ static Task StartReporter(int sendReportEvery, int numberOfEntrie var elapsed = Stopwatch.GetTimestamp() - start; histogram.RecordValue(elapsed); - await Task.Delay(sendReportEvery); + // Not cancelled: the loop exits on the next check and the task has to complete + // normally, because MergeHistograms reads the histogram it returns. + await Task.Delay(sendReportEvery, CancellationToken.None); } return histogram; @@ -247,8 +249,8 @@ public void TrackEndpointInputQueue(EndpointToQueueMapping queueToTrack) { } - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; } } \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index 167d222f05..b8e0c97315 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -21,7 +21,9 @@ public ServiceControlComponentBehavior(ITransportIntegration transportToUse, Act public HttpClient HttpClient => runner.HttpClient; public JsonSerializerOptions SerializerOptions => runner.SerializerOptions; +#pragma warning disable PS0018 // IComponentBehavior declares this without a CancellationToken public async Task CreateRunner(RunDescriptor run) +#pragma warning restore PS0018 { runner = new ServiceControlComponentRunner(transportIntegration, setSettings, customConfiguration); await runner.Initialize(run); diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index a5d7623d79..545f2896e5 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -34,9 +34,9 @@ class ServiceControlComponentRunner( public HttpClient HttpClient { get; private set; } public JsonSerializerOptions SerializerOptions => Infrastructure.SerializerOptions.Default; - public Task Initialize(RunDescriptor run) => InitializeServiceControl(run.ScenarioContext); + public Task Initialize(RunDescriptor run, CancellationToken cancellationToken = default) => InitializeServiceControl(run.ScenarioContext, cancellationToken); - async Task InitializeServiceControl(ScenarioContext context) + async Task InitializeServiceControl(ScenarioContext context, CancellationToken cancellationToken) { LoggerUtil.ActiveLoggers = Loggers.Test; settings = new Settings(transportType: transportToUse.TypeName) @@ -79,7 +79,7 @@ async Task InitializeServiceControl(ScenarioContext context) using (new DiagnosticTimer($"Creating infrastructure for {settings.InstanceName}")) { var setupCommand = new SetupCommand(); - await setupCommand.Execute(new HostArguments([]), settings); + await setupCommand.Execute(new HostArguments([]), settings, cancellationToken); } var configuration = new EndpointConfiguration(settings.InstanceName); @@ -127,7 +127,7 @@ async Task InitializeServiceControl(ScenarioContext context) host.UseTestRemoteIp(); host.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled); host.UseServiceControlMonitoring(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.CorsSettings); - await host.StartAsync(); + await host.StartAsync(cancellationToken); HttpClient = host.Services.GetRequiredKeyedService(settings.InstanceName).CreateClient(); } diff --git a/src/ServiceControl.Monitoring/.editorconfig b/src/ServiceControl.Monitoring/.editorconfig index c1cb416fea..aff82c0034 100644 --- a/src/ServiceControl.Monitoring/.editorconfig +++ b/src/ServiceControl.Monitoring/.editorconfig @@ -2,10 +2,3 @@ # Justification: Application synchronization contexts don't require ConfigureAwait(false) dotnet_diagnostic.CA2007.severity = none - -# Cancellation analyzer debt. These fire because Particular.Analyzers is no longer pinned to 0.9.0. -# They are scheduled work, not accepted exceptions: remove a line once this project has no -# violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0003.severity = none -dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/AbstractCommand.cs b/src/ServiceControl.Monitoring/Hosting/Commands/AbstractCommand.cs index 9dc057200f..087fdef3be 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/AbstractCommand.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/AbstractCommand.cs @@ -1,9 +1,10 @@ namespace ServiceControl.Monitoring { + using System.Threading; using System.Threading.Tasks; abstract class AbstractCommand { - public abstract Task Execute(HostArguments args, Settings settings); + public abstract Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/CommandRunner.cs b/src/ServiceControl.Monitoring/Hosting/Commands/CommandRunner.cs index cf5a79c3d4..31fb3b9049 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/CommandRunner.cs @@ -1,14 +1,15 @@ namespace ServiceControl.Monitoring { using System; + using System.Threading; using System.Threading.Tasks; class CommandRunner(Type commandType) { - public async Task Execute(HostArguments args, Settings settings) + public async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { var command = (AbstractCommand)Activator.CreateInstance(commandType); - await command.Execute(args, settings); + await command.Execute(args, settings, cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs index 6e197e463a..8a60faf6ff 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Monitoring { + using System.Threading; using System.Threading.Tasks; using Infrastructure; using Infrastructure.WebApi; @@ -10,7 +11,7 @@ namespace ServiceControl.Monitoring class RunCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { var endpointConfiguration = new EndpointConfiguration(settings.InstanceName); diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/SetupCommand.cs b/src/ServiceControl.Monitoring/Hosting/Commands/SetupCommand.cs index 22a84b27f7..252419e37b 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/SetupCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Monitoring { + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using ServiceControl.Infrastructure; @@ -7,7 +8,7 @@ namespace ServiceControl.Monitoring class SetupCommand : AbstractCommand { - public override Task Execute(HostArguments args, Settings settings) + public override Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { if (args.SkipQueueCreation) { @@ -18,7 +19,7 @@ public override Task Execute(HostArguments args, Settings settings) var transportSettings = settings.ToTransportSettings(); transportSettings.ErrorQueue = settings.ErrorQueue; var transportCustomization = TransportFactory.Create(transportSettings); - return transportCustomization.ProvisionQueues(transportSettings, []); + return transportCustomization.ProvisionQueues(transportSettings, [], cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/Infrastructure/RemoveExpiredEndpointInstances.cs b/src/ServiceControl.Monitoring/Infrastructure/RemoveExpiredEndpointInstances.cs index c563aa45b0..4a7228a856 100644 --- a/src/ServiceControl.Monitoring/Infrastructure/RemoveExpiredEndpointInstances.cs +++ b/src/ServiceControl.Monitoring/Infrastructure/RemoveExpiredEndpointInstances.cs @@ -17,7 +17,7 @@ public class RemoveExpiredEndpointInstances( { const int IntervalInMinutes = 5; - protected override async Task ExecuteAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { logger.LogInformation($"Starting {nameof(RemoveExpiredEndpointInstances)}"); diff --git a/src/ServiceControl.Monitoring/Infrastructure/ReportThroughputHostedService.cs b/src/ServiceControl.Monitoring/Infrastructure/ReportThroughputHostedService.cs index 5789593d4e..d2af476bea 100644 --- a/src/ServiceControl.Monitoring/Infrastructure/ReportThroughputHostedService.cs +++ b/src/ServiceControl.Monitoring/Infrastructure/ReportThroughputHostedService.cs @@ -13,7 +13,7 @@ class ReportThroughputHostedService(ILogger logger, IMessageSession session, IEndpointMetricsApi endpointMetricsApi, Settings settings, TimeProvider timeProvider, ITransportCustomization transportCustomization) : BackgroundService { - protected override async Task ExecuteAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { logger.LogInformation($"Starting {nameof(ReportThroughputHostedService)}"); @@ -29,7 +29,10 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await ReportOnThroughput(serviceControlThroughputDataQueue, cancellationToken); } +#pragma warning disable PS0019 // The filter already excludes OperationCanceledException, so cancellation + // is left to the outer handler. catch (Exception ex) when (ex is not OperationCanceledException) +#pragma warning restore PS0019 { if (ex.InnerException is not null and QueueNotFoundException) { diff --git a/src/ServiceControl.Monitoring/Licensing/LicenseCheckHostedService.cs b/src/ServiceControl.Monitoring/Licensing/LicenseCheckHostedService.cs index 3bc9d0ec21..cad60e9ce5 100644 --- a/src/ServiceControl.Monitoring/Licensing/LicenseCheckHostedService.cs +++ b/src/ServiceControl.Monitoring/Licensing/LicenseCheckHostedService.cs @@ -9,7 +9,7 @@ class LicenseCheckHostedService(ActiveLicense activeLicense, IAsyncTimer scheduler, ILogger logger) : IHostedService { - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { var due = TimeSpan.FromHours(8); timer = scheduler.Schedule(_ => @@ -20,7 +20,7 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) => timer.Stop(cancellationToken); + public Task StopAsync(CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); TimerJob timer;