From ea783610264e34ce54dce4baae0a1f13b281df12 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 15:44:10 -0700 Subject: [PATCH 01/12] fix flaky HeartbeatEndpointSettingsSyncHostedService tests by polling instead of fixed delay --- ...tEndpointSettingsSyncHostedServiceTests.cs | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 6b8b1ca1c8..cb66705dbf 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,23 +17,37 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { + // Background service work happens on the thread pool asynchronously. Waiting a fixed + // wall-clock duration before asserting is flaky on slower/loaded CI machines because the + // work may not have completed yet. Instead, poll for the expected condition until it is + // met or a generous timeout elapses. + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10)); + while (!condition() && DateTime.UtcNow < deadline) + { + await Task.Delay(20); + } + } + [Test] public async Task Should_handle_cancellation_token_gracefully() { using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore([]), new MockEndpointSettingsStore([]), - new MockEndpointInstanceMonitoring([]), new Settings { TrackInstancesInitialValue = true }, + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = true }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(service.ExecuteTask?.IsCompletedSuccessfully, Is.True); @@ -59,7 +73,7 @@ public async Task Should_delete_settings_from_endpoints_that_are_no_longer_live( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Deleted.Count, Is.EqualTo(2)); @@ -86,7 +100,7 @@ public async Task Should_set_the_default_for_settings_if_does_not_exist_already( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Updated.Count >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(1)); @@ -106,11 +120,12 @@ public async Task Should_not_set_the_default_if_already_exists() var mockEndpointSettingsStore = new MockEndpointSettingsStore([ new EndpointSettings { Name = string.Empty, TrackInstances = expectedTrackInstancesInitialValue } ]); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore( []), mockEndpointSettingsStore, - new MockEndpointInstanceMonitoring([]), + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = expectedTrackInstancesInitialValue }, fakeTimeProvider, NullLogger.Instance) { @@ -118,7 +133,7 @@ public async Task Should_not_set_the_default_if_already_exists() }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(0)); @@ -158,7 +173,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockMonitoringDataStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(2)); @@ -197,7 +212,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(0)); @@ -220,7 +235,16 @@ public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, b public Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public EndpointsView[] GetEndpoints() => endpointsViews; + public EndpointsView[] GetEndpoints() + { + GetEndpointsCallCount++; + return endpointsViews; + } + + // GetEndpoints() is called by PurgeMonitoringDataThatDoesNotNeedToBeTracked, which runs + // at the end of each sync cycle. Waiting for this to be invoked gives a deterministic + // signal that a full sync cycle has completed, without relying on a fixed wall-clock delay. + public int GetEndpointsCallCount { get; private set; } public List GetKnownEndpoints() => throw new NotImplementedException(); From b6a19aa64c6007bddde40665017a9103d780ccd6 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 15:46:51 -0700 Subject: [PATCH 02/12] remove comment --- .../HeartbeatEndpointSettingsSyncHostedServiceTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index cb66705dbf..4ee83c9335 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,10 +17,7 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { - // Background service work happens on the thread pool asynchronously. Waiting a fixed - // wall-clock duration before asserting is flaky on slower/loaded CI machines because the - // work may not have completed yet. Instead, poll for the expected condition until it is - // met or a generous timeout elapses. + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) { var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10)); From a3ab21da837aae8b92424e6e171e0ab4ca0dc3a9 Mon Sep 17 00:00:00 2001 From: John Simons Date: Thu, 13 Aug 2026 10:08:55 +1000 Subject: [PATCH 03/12] Update SSH.NET to 2026.0.0 to address CVE --- src/Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a1aa2fd253..fc7ed7ed5d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -106,6 +106,7 @@ + From f976a31808d17724bd60c0a4d0013f5ebf550aa7 Mon Sep 17 00:00:00 2001 From: John Simons Date: Thu, 13 Aug 2026 08:12:21 +1000 Subject: [PATCH 04/12] Move Event Source setup to shared acceptance test projects Relocates the Event Source creation and wait logic from RavenDB-specific folders to the base acceptance test projects. This ensures the setup is available for all persistence types and prevents race conditions when multiple tests attempt to create the same event source during parallel execution. --- .../SetupFixture.cs | 29 ------------- .../SetupFixture.cs | 42 +++++++++++++++++++ .../SetupFixture.cs | 29 ------------- .../SetupFixture.cs | 42 +++++++++++++++++++ 4 files changed, 84 insertions(+), 58 deletions(-) delete mode 100644 src/ServiceControl.AcceptanceTests.RavenDB/SetupFixture.cs create mode 100644 src/ServiceControl.AcceptanceTests/SetupFixture.cs delete mode 100644 src/ServiceControl.Audit.AcceptanceTests.RavenDB/SetupFixture.cs create mode 100644 src/ServiceControl.Audit.AcceptanceTests/SetupFixture.cs diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/SetupFixture.cs b/src/ServiceControl.AcceptanceTests.RavenDB/SetupFixture.cs deleted file mode 100644 index 5a24a3417e..0000000000 --- a/src/ServiceControl.AcceptanceTests.RavenDB/SetupFixture.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace ServiceControl.AcceptanceTests.RavenDB; - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; - -[SetUpFixture] -public class SetupFixture -{ - [OneTimeSetUp] - public async Task Setup() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - ServiceBus.Management.Infrastructure.Installers.EventSourceCreator.Create(); - - //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - while (!EventLog.SourceExists(ServiceBus.Management.Infrastructure.Installers.EventSourceCreator.SourceName)) - { - cts.Token.ThrowIfCancellationRequested(); - await Task.Delay(500, CancellationToken.None); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/SetupFixture.cs b/src/ServiceControl.AcceptanceTests/SetupFixture.cs new file mode 100644 index 0000000000..55ddf35101 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests/SetupFixture.cs @@ -0,0 +1,42 @@ +namespace ServiceControl.AcceptanceTests; + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using ServiceBus.Management.Infrastructure.Installers; + +[SetUpFixture] +public class SetupFixture +{ + [OneTimeSetUp] + public async Task Setup() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Every test runs the setup command, which creates the event source if it is missing. Tests run in + // parallel, so without creating it up front several of them race and all but one fail with + // "Source ServiceControl already exists". + EventSourceCreator.Create(); + + await WaitForSource(EventSourceCreator.SourceName); + } + + //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. + [SupportedOSPlatform("windows")] + static async Task WaitForSource(string sourceName) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + while (!EventLog.SourceExists(sourceName)) + { + cts.Token.ThrowIfCancellationRequested(); + await Task.Delay(500, CancellationToken.None); + } + } +} diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/SetupFixture.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/SetupFixture.cs deleted file mode 100644 index 100af7975a..0000000000 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/SetupFixture.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace ServiceControl.Audit.AcceptanceTests.RavenDB; - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; - -[SetUpFixture] -public class SetupFixture -{ - [OneTimeSetUp] - public async Task Setup() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Infrastructure.EventSourceCreator.Create(); - - //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - while (!EventLog.SourceExists(Infrastructure.EventSourceCreator.SourceName)) - { - cts.Token.ThrowIfCancellationRequested(); - await Task.Delay(500, CancellationToken.None); - } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.AcceptanceTests/SetupFixture.cs b/src/ServiceControl.Audit.AcceptanceTests/SetupFixture.cs new file mode 100644 index 0000000000..70acaa0843 --- /dev/null +++ b/src/ServiceControl.Audit.AcceptanceTests/SetupFixture.cs @@ -0,0 +1,42 @@ +namespace ServiceControl.Audit.AcceptanceTests; + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using ServiceControl.Audit.Infrastructure; + +[SetUpFixture] +public class SetupFixture +{ + [OneTimeSetUp] + public async Task Setup() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Every test runs the setup command, which creates the event source if it is missing. Tests run in + // parallel, so without creating it up front several of them race and all but one fail with + // "Source ServiceControl.Audit already exists". + EventSourceCreator.Create(); + + await WaitForSource(EventSourceCreator.SourceName); + } + + //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. + [SupportedOSPlatform("windows")] + static async Task WaitForSource(string sourceName) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + while (!EventLog.SourceExists(sourceName)) + { + cts.Token.ThrowIfCancellationRequested(); + await Task.Delay(500, CancellationToken.None); + } + } +} From 236b8ee205e43cb5d4b7e39cf5b7d4b3ce7ba754 Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 13:35:17 +1000 Subject: [PATCH 05/12] 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; From 5945faff04c32004663bc8c0ad9a4040b97c63eb Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 17:12:23 +1000 Subject: [PATCH 06/12] Propagate cancellation tokens through the installer engine and the Config app Retires the cancellation analyzer debt blocks in ServiceControlInstaller.Engine, ServiceControl.Management.PowerShell and ServiceControl.Config.Tests, and narrows the ServiceControl.Config block to the one boundary that cannot carry a token. Phase 7 of the propagation work. The engine change is what forces these projects to move together. AbstractCommandChecks' eight abstract members take tokens, the prompt callback becomes Func> throughout PathsValidator, the installable bases and the unattended installers, and ValidateNewInstance loses params so the token can be last. PowerShell's PowerShellCommandChecks and the three New-*Instance cmdlets follow, along with the Config app's ScmuCommandChecks, InstallerModule and add attachments. Two token drops are fixed in the Config app's Caliburn layer. RxScreen received a CancellationToken in ActivateAsync and DeactivateAsync and discarded it instead of passing it to OnInitialize, OnActivate and OnDeactivate; RxConductorBase did the same in ActivateItemAsync and DeactivateItemAsync. Both now forward it, so screen activation is cancellable. Command bodies reached through ReactiveCommand.CreateFromTask now take a required token, which binds its Func overload and gives them a real one. Config's own Command.Create path bottoms out in System.Windows.Input.ICommand.Execute, which returns void and has no token to offer, so those command types keep a file-scoped block that says so, and their call sites pass CancellationToken.None explicitly. Caliburn's IClose.TryCloseAsync and IEventAggregator declare no token and cannot be changed, so those are inline pragmas rather than fixes. --- src/ServiceControl.Config.Tests/.editorconfig | 6 -- ...tAggregationAutoSubscriptionModuleTests.cs | 4 +- src/ServiceControl.Config/.editorconfig | 16 +++-- .../ForceUpgradeAuditInstanceCommand.cs | 21 +++--- ...rceUpgradeServiceControlInstanceCommand.cs | 21 +++--- .../Commands/ScmuCommandChecks.cs | 34 ++++----- .../Commands/UpgradeAuditInstanceCommand.cs | 18 ++--- .../UpgradeServiceControlInstanceCommand.cs | 17 ++--- .../Framework/Modules/InstallerModule.cs | 17 ++--- .../Framework/RaygunFeedBack.cs | 24 ++++--- .../Framework/Rx/RxConductor.cs | 20 +++--- .../Framework/Rx/RxConductorBase.cs | 8 +-- .../Rx/RxConductorBaseWithActiveItem.cs | 7 +- .../Rx/RxConductorWithCollectionOneActive.cs | 34 ++++----- .../Framework/Rx/RxProgressScreen.cs | 2 +- .../Framework/Rx/RxScreen.cs | 16 +++-- .../Framework/ServiceControlWindowManager.cs | 55 +++++++------- .../ServiceControlAdvancedViewModel.cs | 10 +-- .../UI/FeedBack/FeedBackViewModel.cs | 12 +++- .../UI/InstanceAdd/MonitoringAddAttachment.cs | 15 ++-- .../ServiceControlAddAttachment.cs | 27 +++---- .../InstanceDetailsViewModel.cs | 8 +-- .../InstanceEdit/MonitoringEditAttachment.cs | 11 +-- .../ServiceControlAuditEditAttachment.cs | 11 +-- .../ServiceControlEditAttachment.cs | 11 +-- .../UI/License/LicenseViewModel.cs | 5 +- .../ListInstances/ListInstancesViewModel.cs | 6 +- .../UI/MessageBox/ExceptionMessageBox.xaml.cs | 3 + .../UI/Shell/FeedBackAttachment.cs | 5 +- .../UI/Shell/LicenseStatusManager.cs | 4 +- .../UI/Shell/ShellViewModel.cs | 26 ++++--- .../UI/Shell/VersionCheckerHelper.cs | 13 ++-- .../Upgrades/AddNewAuditInstanceAttachment.cs | 3 +- .../.editorconfig | 5 -- .../NewServiceControlAuditInstance.cs | 5 +- .../NewMonitoringInstance.cs | 5 +- .../NewServiceControlInstance.cs | 5 +- .../Validation/PowerShellCommandChecks.cs | 17 ++--- .../RunEngineTasksExplicitly.cs | 4 +- .../.editorconfig | 6 +- .../Instances/MonitoringInstance.cs | 5 +- .../Instances/MonitoringNewInstance.cs | 5 +- .../Instances/ServiceControlBaseService.cs | 7 +- .../ServiceControlInstallableBase.cs | 9 +-- .../Instances/ServiceControlInstance.cs | 5 +- .../Unattended/UnattendAuditInstaller.cs | 5 +- .../Unattended/UnattendMonitoringInstaller.cs | 9 +-- .../UnattendServiceControlInstaller.cs | 9 +-- .../Validation/AbstractCommandChecks.cs | 71 ++++++++++--------- .../Validation/PathsValidator.cs | 17 +++-- 50 files changed, 367 insertions(+), 312 deletions(-) diff --git a/src/ServiceControl.Config.Tests/.editorconfig b/src/ServiceControl.Config.Tests/.editorconfig index 235eef7320..c5410d8c02 100644 --- a/src/ServiceControl.Config.Tests/.editorconfig +++ b/src/ServiceControl.Config.Tests/.editorconfig @@ -6,9 +6,3 @@ dotnet_diagnostic.CA2007.severity = none # Justification: Executable specifications intentionally assign properties after the # object initializer to mirror user interaction order (e.g. typing a name after load) dotnet_diagnostic.IDE0017.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.PS0013.severity = none diff --git a/src/ServiceControl.Config.Tests/EventAggregationAutoSubscriptionModuleTests.cs b/src/ServiceControl.Config.Tests/EventAggregationAutoSubscriptionModuleTests.cs index 7db70027fe..b378faae8c 100644 --- a/src/ServiceControl.Config.Tests/EventAggregationAutoSubscriptionModuleTests.cs +++ b/src/ServiceControl.Config.Tests/EventAggregationAutoSubscriptionModuleTests.cs @@ -40,7 +40,7 @@ class FakeEvent; class FakeEventHandler : IHandle { - public Task HandleAsync(FakeEvent message, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task HandleAsync(FakeEvent message, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } class FakeNonEventHandler; @@ -51,11 +51,13 @@ class FakeEventAggregator : IEventAggregator public bool HandlerExistsFor(Type messageType) => throw new NotImplementedException(); +#pragma warning disable PS0013 // Caliburn.Micro's IEventAggregator declares the marshal delegate as Func, Task> public void Subscribe(object subscriber, Func, Task> marshal) => Subscribers.Add(subscriber); public void Unsubscribe(object subscriber) => throw new NotImplementedException(); public Task PublishAsync(object message, Func, Task> marshal, CancellationToken cancellationToken = new CancellationToken()) => throw new NotImplementedException(); +#pragma warning restore PS0013 } } } \ No newline at end of file diff --git a/src/ServiceControl.Config/.editorconfig b/src/ServiceControl.Config/.editorconfig index d2502b8251..6181205b06 100644 --- a/src/ServiceControl.Config/.editorconfig +++ b/src/ServiceControl.Config/.editorconfig @@ -3,10 +3,14 @@ # Justification: Application synchronization contexts don't require ConfigureAwait(false) dotnet_diagnostic.CA2007.severity = none -# may be enabled in future -dotnet_diagnostic.PS0003.severity = none # A parameter of type CancellationToken on a non-private delegate or method should be optional -dotnet_diagnostic.PS0013.severity = none # A Func used as a method parameter with a Task, ValueTask, or ValueTask return type argument should have at least one CancellationToken parameter type argument unless it has a parameter type argument implementing ICancellableContext -dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext - # Use auto property - should be fixed in an independent PR -dotnet_diagnostic.IDE0032.severity = suggestion \ No newline at end of file +dotnet_diagnostic.IDE0032.severity = suggestion + +# System.Windows.Input.ICommand.Execute(object) returns void and supplies no CancellationToken, +# so the async command chain built on it has no token to accept or forward. A token added here +# could only ever be CancellationToken.None. Command bodies themselves do take tokens: they are +# wired through ReactiveCommand.CreateFromTask(Func), which supplies a +# real one. +[{Framework/Commands/*.cs,Framework/Command.cs,Commands/SelectPathCommand.cs}] +dotnet_diagnostic.PS0013.severity = none +dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.Config/Commands/ForceUpgradeAuditInstanceCommand.cs b/src/ServiceControl.Config/Commands/ForceUpgradeAuditInstanceCommand.cs index c500588817..341c3a4815 100644 --- a/src/ServiceControl.Config/Commands/ForceUpgradeAuditInstanceCommand.cs +++ b/src/ServiceControl.Config/Commands/ForceUpgradeAuditInstanceCommand.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Commands; using System.IO; +using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -36,34 +37,34 @@ public override async Task ExecuteAsync(ServiceControlAdvancedViewModel model) return; } - await UpgradeServiceControlInstance(model, instance, new ServiceControlUpgradeOptions()); + await UpgradeServiceControlInstance(model, instance, new ServiceControlUpgradeOptions(), CancellationToken.None); await eventAggregator.PublishOnUIThreadAsync(new ResetInstances()); } - async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, ServiceControlAuditInstance instance, ServiceControlUpgradeOptions upgradeOptions) + async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, ServiceControlAuditInstance instance, ServiceControlUpgradeOptions upgradeOptions, CancellationToken cancellationToken) { using (var progress = model.GetProgressObject($"UPGRADING {model.Name}")) { var reportCard = new ReportCard(); var restartAgain = model.IsRunning; - var stopped = await model.StopService(progress); + var stopped = await model.StopService(progress, cancellationToken); if (!stopped) { - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); reportCard.Errors.Add("Failed to stop the service"); reportCard.SetStatus(); - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", cancellationToken: cancellationToken); return; } if (Directory.Exists(model.ForcedUpgradeBackupLocation)) { - await windowManager.ShowMessage("Cannot make database backup.", $"The target database backup location: {model.ForcedUpgradeBackupLocation} already exists.", hideCancel: true); + await windowManager.ShowMessage("Cannot make database backup.", $"The target database backup location: {model.ForcedUpgradeBackupLocation} already exists.", hideCancel: true, cancellationToken: cancellationToken); return; } @@ -78,18 +79,18 @@ async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:", cancellationToken: cancellationToken); return; } if (restartAgain) { - var serviceStarted = await model.StartService(progress, maintenanceMode: false); + var serviceStarted = await model.StartService(progress, maintenanceMode: false, cancellationToken); if (!serviceStarted) { reportCard.Errors.Add("The Service failed to start. Please consult the ServiceControl logs for this instance"); - await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:"); + await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:", cancellationToken: cancellationToken); return; } @@ -97,7 +98,7 @@ async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, } await model.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new ResetInstances()); + await eventAggregator.PublishOnUIThreadAsync(new ResetInstances(), cancellationToken); } readonly IEventAggregator eventAggregator; diff --git a/src/ServiceControl.Config/Commands/ForceUpgradeServiceControlInstanceCommand.cs b/src/ServiceControl.Config/Commands/ForceUpgradeServiceControlInstanceCommand.cs index 40992c6083..b3d8cb3549 100644 --- a/src/ServiceControl.Config/Commands/ForceUpgradeServiceControlInstanceCommand.cs +++ b/src/ServiceControl.Config/Commands/ForceUpgradeServiceControlInstanceCommand.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Commands; using System.IO; +using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -36,34 +37,34 @@ public override async Task ExecuteAsync(ServiceControlAdvancedViewModel model) return; } - await UpgradeServiceControlInstance(model, instance); + await UpgradeServiceControlInstance(model, instance, CancellationToken.None); await eventAggregator.PublishOnUIThreadAsync(new ResetInstances()); } - async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, ServiceControlInstance instance) + async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, ServiceControlInstance instance, CancellationToken cancellationToken) { using (var progress = model.GetProgressObject($"UPGRADING {model.Name}")) { var reportCard = new ReportCard(); var restartAgain = model.IsRunning; - var stopped = await model.StopService(progress); + var stopped = await model.StopService(progress, cancellationToken); if (!stopped) { - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); reportCard.Errors.Add("Failed to stop the service"); reportCard.SetStatus(); - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", cancellationToken: cancellationToken); return; } if (Directory.Exists(model.ForcedUpgradeBackupLocation)) { - await windowManager.ShowMessage("Cannot make database backup.", $"The target database backup location: {model.ForcedUpgradeBackupLocation} already exists.", hideCancel: true); + await windowManager.ShowMessage("Cannot make database backup.", $"The target database backup location: {model.ForcedUpgradeBackupLocation} already exists.", hideCancel: true, cancellationToken: cancellationToken); return; } @@ -78,20 +79,20 @@ async Task UpgradeServiceControlInstance(ServiceControlAdvancedViewModel model, if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:", cancellationToken: cancellationToken); return; } if (restartAgain) { - var serviceStarted = await model.StartService(progress, maintenanceMode: false); + var serviceStarted = await model.StartService(progress, maintenanceMode: false, cancellationToken); if (!serviceStarted) { reportCard.Errors.Add( "The Service failed to start. Please consult the ServiceControl logs for this instance"); await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", - "Instance reported this error after upgrade:"); + "Instance reported this error after upgrade:", cancellationToken: cancellationToken); return; } @@ -99,7 +100,7 @@ await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", } await model.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new ResetInstances()); + await eventAggregator.PublishOnUIThreadAsync(new ResetInstances(), cancellationToken); } readonly IEventAggregator eventAggregator; diff --git a/src/ServiceControl.Config/Commands/ScmuCommandChecks.cs b/src/ServiceControl.Config/Commands/ScmuCommandChecks.cs index b21e0b37fd..5b47955af4 100644 --- a/src/ServiceControl.Config/Commands/ScmuCommandChecks.cs +++ b/src/ServiceControl.Config/Commands/ScmuCommandChecks.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; using System.Text; + using System.Threading; using System.Threading.Tasks; using ServiceControl.Config.Framework; using ServiceControlInstaller.Engine; @@ -18,7 +19,7 @@ public ScmuCommandChecks(IServiceControlWindowManager windowManager) this.windowManager = windowManager; } - protected override async Task PromptForRabbitMqCheck(bool isUpgrade) + protected override async Task PromptForRabbitMqCheck(bool isUpgrade, CancellationToken cancellationToken = default) { var title = isUpgrade ? "UPGRADE WARNING" : "INSTALL WARNING"; var beforeWhat = isUpgrade ? "upgrading" : "installing"; @@ -34,27 +35,28 @@ protected override async Task PromptForRabbitMqCheck(bool isUpgrade) message.AppendLine(); message.AppendLine($"Please confirm your broker meets the minimum requirements before {beforeWhat}."); - var continueInstall = await windowManager.ShowYesNoDialog(title, message.ToString(), question, yes, no); + var continueInstall = await windowManager.ShowYesNoDialog(title, message.ToString(), question, yes, no, cancellationToken); return continueInstall; } - protected override Task NotifyForDeprecatedMessageTransport(TransportInfo transport) + protected override Task NotifyForDeprecatedMessageTransport(TransportInfo transport, CancellationToken cancellationToken = default) { - return windowManager.ShowMessage("DEPRECATED MESSAGE TRANSPORT", $"The message transport '{transport.DisplayName}' is not available in this version of ServiceControl, and this instance cannot be upgraded.", acceptText: "Cancel Upgrade", hideCancel: true); + return windowManager.ShowMessage("DEPRECATED MESSAGE TRANSPORT", $"The message transport '{transport.DisplayName}' is not available in this version of ServiceControl, and this instance cannot be upgraded.", acceptText: "Cancel Upgrade", hideCancel: true, cancellationToken: cancellationToken); } - protected override Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage) + protected override Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage, CancellationToken cancellationToken = default) { - return windowManager.ShowMessage("Missing prerequisites", missingPrereqsMessage, acceptText: "Cancel", hideCancel: true); + return windowManager.ShowMessage("Missing prerequisites", missingPrereqsMessage, acceptText: "Cancel", hideCancel: true, cancellationToken: cancellationToken); } - protected override async Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance) + protected override async Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance, CancellationToken cancellationToken = default) { var openUpgradeGuide = await windowManager.ShowYesNoDialog("STORAGE ENGINE INCOMPATIBLE", $"The storage format has changed and the {baseInstance.PersistenceManifest.DisplayName} storage engine is no longer available. Upgrading requires a side-by-side deployment of both versions. Migration guidance is available in the version 4 to 5 upgrade guidance at {UpgradeGuide4to5Url}", "Open online ServiceControl 4 to 5 upgrade guide in system default browser?", "Yes", - "No" + "No", + cancellationToken ); if (openUpgradeGuide) @@ -63,7 +65,7 @@ protected override async Task NotifyForIncompatibleStorageEngine(IServiceControl } } - protected override async Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo) + protected override async Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo, CancellationToken cancellationToken = default) { var nextVersion = upgradeInfo.UpgradePath[0]; await windowManager.ShowMessage("VERSION UPGRADE INCOMPATIBLE", @@ -76,26 +78,26 @@ await windowManager.ShowMessage("VERSION UPGRADE INCOMPATIBLE", "Upgrade this instance to the latest version of ServiceControl.\r\n" + "\r\n" + "", - hideCancel: true); + hideCancel: true, cancellationToken: cancellationToken); } - protected override Task NotifyError(string title, string message) + protected override Task NotifyError(string title, string message, CancellationToken cancellationToken = default) { - return windowManager.ShowMessage(title.ToUpperInvariant(), message, hideCancel: true); + return windowManager.ShowMessage(title.ToUpperInvariant(), message, hideCancel: true, cancellationToken: cancellationToken); } - protected override Task PromptToStopRunningInstance(BaseService instance) + protected override Task PromptToStopRunningInstance(BaseService instance, CancellationToken cancellationToken = default) { return windowManager.ShowYesNoDialog($"STOP INSTANCE AND UPGRADE TO {Constants.CurrentVersion}", $"{instance.Name} needs to be stopped in order to upgrade to version {Constants.CurrentVersion}.", "Do you want to proceed?", - "Yes, I want to proceed", "No"); + "Yes, I want to proceed", "No", cancellationToken); } - protected override Task PromptToContinueWithForcedUpgrade() + protected override Task PromptToContinueWithForcedUpgrade(CancellationToken cancellationToken = default) { return windowManager.ShowMessage("Forced migration", - "Do you want to proceed with forced migration to ServiceControl 5? The current RavenDB 3.5 database will be moved aside and a new database will be created.", "Yes"); + "Do you want to proceed with forced migration to ServiceControl 5? The current RavenDB 3.5 database will be moved aside and a new database will be created.", "Yes", cancellationToken: cancellationToken); } } } diff --git a/src/ServiceControl.Config/Commands/UpgradeAuditInstanceCommand.cs b/src/ServiceControl.Config/Commands/UpgradeAuditInstanceCommand.cs index 1c0d966a94..6ddcb8295c 100644 --- a/src/ServiceControl.Config/Commands/UpgradeAuditInstanceCommand.cs +++ b/src/ServiceControl.Config/Commands/UpgradeAuditInstanceCommand.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Commands { using System; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -46,7 +47,7 @@ public override async Task ExecuteAsync(InstanceDetailsViewModel model) return; } - await UpgradeAuditInstance(model, instance, upgradeOptions); + await UpgradeAuditInstance(model, instance, upgradeOptions, CancellationToken.None); await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); } @@ -54,7 +55,8 @@ public override async Task ExecuteAsync(InstanceDetailsViewModel model) async Task UpgradeAuditInstance( InstanceDetailsViewModel model, ServiceControlAuditInstance instance, - ServiceControlUpgradeOptions upgradeOptions + ServiceControlUpgradeOptions upgradeOptions, + CancellationToken cancellationToken ) { using (var progress = model.GetProgressObject($"UPGRADING {model.Name}")) @@ -62,15 +64,15 @@ ServiceControlUpgradeOptions upgradeOptions var reportCard = new ReportCard(); var restartAgain = model.IsRunning; - var stopped = await model.StopService(progress); + var stopped = await model.StopService(progress, cancellationToken); if (!stopped) { - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); reportCard.Errors.Add("Failed to stop the service"); reportCard.SetStatus(); - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", cancellationToken: cancellationToken); return; } @@ -79,17 +81,17 @@ ServiceControlUpgradeOptions upgradeOptions if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:", cancellationToken: cancellationToken); return; } if (restartAgain) { - var serviceStarted = await model.StartService(progress); + var serviceStarted = await model.StartService(progress, cancellationToken); if (!serviceStarted) { reportCard.Errors.Add("The Service failed to start. Please consult the ServiceControl logs for this instance"); - await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:"); + await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:", cancellationToken: cancellationToken); } } } diff --git a/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs b/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs index 26f2671b7e..fc7bdc26f0 100644 --- a/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs +++ b/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Commands { using System; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -146,27 +147,27 @@ public override async Task ExecuteAsync(InstanceDetailsViewModel model) return; } - await UpgradeServiceControlInstance(model, instance, upgradeOptions); + await UpgradeServiceControlInstance(model, instance, upgradeOptions, CancellationToken.None); await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); } - async Task UpgradeServiceControlInstance(InstanceDetailsViewModel model, ServiceControlInstance instance, ServiceControlUpgradeOptions upgradeOptions) + async Task UpgradeServiceControlInstance(InstanceDetailsViewModel model, ServiceControlInstance instance, ServiceControlUpgradeOptions upgradeOptions, CancellationToken cancellationToken) { using (var progress = model.GetProgressObject($"UPGRADING {model.Name}")) { var reportCard = new ReportCard(); var restartAgain = model.IsRunning; - var stopped = await model.StopService(progress); + var stopped = await model.StopService(progress, cancellationToken); if (!stopped) { - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); reportCard.Errors.Add("Failed to stop the service"); reportCard.SetStatus(); - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", cancellationToken: cancellationToken); return; } @@ -175,17 +176,17 @@ async Task UpgradeServiceControlInstance(InstanceDetailsViewModel model, Service if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES UPGRADING INSTANCE", "Could not upgrade instance because of the following errors:", "There were some warnings while upgrading the instance:", cancellationToken: cancellationToken); } else { if (restartAgain) { - var serviceStarted = await model.StartService(progress); + var serviceStarted = await model.StartService(progress, cancellationToken); if (!serviceStarted) { reportCard.Errors.Add("The Service failed to start. Please consult the ServiceControl logs for this instance"); - await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:"); + await windowManager.ShowActionReport(reportCard, "UPGRADE FAILURE", "Instance reported this error after upgrade:", cancellationToken: cancellationToken); } } } diff --git a/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs b/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs index 531997dba8..8bbe5c60cb 100644 --- a/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs +++ b/src/ServiceControl.Config/Framework/Modules/InstallerModule.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Framework.Modules { using System; + using System.Threading; using System.Threading.Tasks; using Autofac; using ServiceControlInstaller.Engine.FileSystem; @@ -43,7 +44,7 @@ public abstract class InstallerBase public class ServiceControlInstallerBase : InstallerBase { - internal async Task Add(ServiceControlInstallableBase details, IProgress progress, Func> promptToProceed) + internal async Task Add(ServiceControlInstallableBase details, IProgress progress, Func> promptToProceed, CancellationToken cancellationToken = default) { ZipInfo.ValidateZip(); @@ -51,7 +52,7 @@ internal async Task Add(ServiceControlInstallableBase details, IProg instanceInstaller.ReportCard = new ReportCard(); //Validation - await instanceInstaller.Validate(promptToProceed); + await instanceInstaller.Validate(promptToProceed, cancellationToken); if (instanceInstaller.ReportCard.HasErrors || instanceInstaller.ReportCard.CancelRequested) { instanceInstaller.ReportCard.Status = Status.FailedValidation; @@ -148,12 +149,12 @@ protected virtual void UpgradeOptions(ServiceControlUpgradeOptions upgradeOption upgradeOptions.ApplyChangesToInstance(instance); } - internal async Task Update(ServiceControlBaseService instance, bool startService) + internal async Task Update(ServiceControlBaseService instance, bool startService, CancellationToken cancellationToken = default) { try { instance.ReportCard = new ReportCard(); - await instance.ValidateChanges(); + await instance.ValidateChanges(cancellationToken); if (instance.ReportCard.HasErrors) { instance.ReportCard.Status = Status.FailedValidation; @@ -238,7 +239,7 @@ public MonitoringInstanceInstaller() ZipInfo = new PlatformZipInfo(Constants.MonitoringExe, "ServiceControl Monitoring", "Particular.ServiceControl.Monitoring.zip"); } - internal async Task Add(MonitoringNewInstance details, IProgress progress, Func> promptToProceed) + internal async Task Add(MonitoringNewInstance details, IProgress progress, Func> promptToProceed, CancellationToken cancellationToken = default) { ZipInfo.ValidateZip(); @@ -246,7 +247,7 @@ internal async Task Add(MonitoringNewInstance details, IProgress prog return instance.ReportCard; } - internal async Task Update(MonitoringInstance instance, bool startService) + internal async Task Update(MonitoringInstance instance, bool startService, CancellationToken cancellationToken = default) { try { instance.ReportCard = new ReportCard(); - await instance.ValidateChanges(); + await instance.ValidateChanges(cancellationToken); if (instance.ReportCard.HasErrors) { instance.ReportCard.Status = Status.FailedValidation; diff --git a/src/ServiceControl.Config/Framework/RaygunFeedBack.cs b/src/ServiceControl.Config/Framework/RaygunFeedBack.cs index 6128a68511..b4e62316c9 100644 --- a/src/ServiceControl.Config/Framework/RaygunFeedBack.cs +++ b/src/ServiceControl.Config/Framework/RaygunFeedBack.cs @@ -7,6 +7,7 @@ namespace ServiceControl.Config.Framework using System.Net; using System.Net.Http; using System.Reflection; + using System.Threading; using System.Threading.Tasks; using Extensions; using Mindscape.Raygun4Net; @@ -21,9 +22,9 @@ public RaygunFeedback() init = Task.Run(async () => { - enabled = await TryInitializeRaygunClientWithCredentials() || - await TryInitializeRaygunClientWithCredentials(CredentialCache.DefaultCredentials) || - await TryInitializeRaygunClientWithCredentials(CredentialCache.DefaultNetworkCredentials); + enabled = await TryInitializeRaygunClientWithCredentials(null, CancellationToken.None) || + await TryInitializeRaygunClientWithCredentials(CredentialCache.DefaultCredentials, CancellationToken.None) || + await TryInitializeRaygunClientWithCredentials(CredentialCache.DefaultNetworkCredentials, CancellationToken.None); version = GetVersion(); }); } @@ -45,7 +46,7 @@ static Guid GetOrSetTrackingId() return trackingId; } - public Task SendFeedBack(string emailAddress, string message, bool includeSystemInfo) + public Task SendFeedBack(string emailAddress, string message, bool includeSystemInfo, CancellationToken cancellationToken = default) { var userInfo = ((IRaygunUserProvider)this).GetUser()!; userInfo.Email = emailAddress; @@ -62,10 +63,10 @@ public Task SendFeedBack(string emailAddress, string message, bool includeSystem } var m = raygunMessage.Build(); - return raygunClient.Send(m); + return raygunClient.Send(m, cancellationToken); } - public Task SendException(Exception ex, bool includeSystemInfo) + public Task SendException(Exception ex, bool includeSystemInfo, CancellationToken cancellationToken = default) { var userInfo = ((IRaygunUserProvider)this).GetUser()!; @@ -81,7 +82,7 @@ public Task SendException(Exception ex, bool includeSystemInfo) } var m = raygunMessage.Build(); - return raygunClient.Send(m); + return raygunClient.Send(m, cancellationToken); } RaygunIdentifierMessage IRaygunUserProvider.GetUser() => new(trackingId.BareString()) @@ -102,7 +103,7 @@ public bool Enabled } } - async Task TryInitializeRaygunClientWithCredentials(ICredentials? credentials = default) + async Task TryInitializeRaygunClientWithCredentials(ICredentials? credentials, CancellationToken cancellationToken) { HttpClient? http = null; try @@ -111,12 +112,17 @@ async Task TryInitializeRaygunClientWithCredentials(ICredentials? credenti { Timeout = TimeSpan.FromSeconds(5) }; - using var response = await http.GetAsync(RaygunUrl); + using var response = await http.GetAsync(RaygunUrl, cancellationToken); response.EnsureSuccessStatusCode(); raygunClient = new RaygunClient(raygunSettings, http, this); return true; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + http?.Dispose(); + throw; + } catch { http?.Dispose(); diff --git a/src/ServiceControl.Config/Framework/Rx/RxConductor.cs b/src/ServiceControl.Config/Framework/Rx/RxConductor.cs index db071efcde..fb912b2ac4 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxConductor.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxConductor.cs @@ -7,23 +7,23 @@ public partial class RxConductor : RxConductorBaseWithActiveItem where T : class { - public override async Task ActivateItem(T item) + public override async Task ActivateItem(T item, CancellationToken cancellationToken = default) { if (item != null && item.Equals(ActiveItem)) { if (IsActive) { - await ScreenExtensions.TryActivateAsync(item); + await ScreenExtensions.TryActivateAsync(item, cancellationToken); OnActivationProcessed(item, true); } return; } - var result = await CloseStrategy.ExecuteAsync(new[] { ActiveItem }); + var result = await CloseStrategy.ExecuteAsync(new[] { ActiveItem }, cancellationToken); if (result.CloseCanOccur) { - await ChangeActiveItem(item, true); + await ChangeActiveItem(item, true, cancellationToken); } else { @@ -31,29 +31,29 @@ public override async Task ActivateItem(T item) } } - public override async Task DeactivateItem(T item, bool close) + public override async Task DeactivateItem(T item, bool close, CancellationToken cancellationToken = default) { if (item == null || !item.Equals(ActiveItem)) { return; } - var result = await CloseStrategy.ExecuteAsync(new[] { ActiveItem }); + var result = await CloseStrategy.ExecuteAsync(new[] { ActiveItem }, cancellationToken); if (result.CloseCanOccur) { - await ChangeActiveItem(default, close); + await ChangeActiveItem(default, close, cancellationToken); } } - public override async Task CanCloseAsync(CancellationToken cancellationToken) + public override async Task CanCloseAsync(CancellationToken cancellationToken = default) { var result = await CloseStrategy.ExecuteAsync(new[] { ActiveItem }, cancellationToken); return result.CloseCanOccur; } - protected override Task OnActivate() => ScreenExtensions.TryActivateAsync(ActiveItem); + protected override Task OnActivate(CancellationToken cancellationToken = default) => ScreenExtensions.TryActivateAsync(ActiveItem, cancellationToken); - protected override Task OnDeactivate(bool close) => ScreenExtensions.TryDeactivateAsync(ActiveItem, close); + protected override Task OnDeactivate(bool close, CancellationToken cancellationToken = default) => ScreenExtensions.TryDeactivateAsync(ActiveItem, close, cancellationToken); public override IEnumerable GetChildren() { diff --git a/src/ServiceControl.Config/Framework/Rx/RxConductorBase.cs b/src/ServiceControl.Config/Framework/Rx/RxConductorBase.cs index 7dc66ca48d..7d38d94cc4 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxConductorBase.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxConductorBase.cs @@ -17,12 +17,12 @@ public ICloseStrategy CloseStrategy Task IConductor.ActivateItemAsync(object item, CancellationToken cancellationToken) { - return ActivateItem((T)item); + return ActivateItem((T)item, cancellationToken); } Task IConductor.DeactivateItemAsync(object item, bool close, CancellationToken cancellationToken) { - return DeactivateItem((T)item, close); + return DeactivateItem((T)item, close, cancellationToken); } IEnumerable IParent.GetChildren() @@ -34,9 +34,9 @@ IEnumerable IParent.GetChildren() public abstract IEnumerable GetChildren(); - public abstract Task ActivateItem(T item); + public abstract Task ActivateItem(T item, CancellationToken cancellationToken = default); - public abstract Task DeactivateItem(T item, bool close); + public abstract Task DeactivateItem(T item, bool close, CancellationToken cancellationToken = default); protected virtual void OnActivationProcessed(T item, bool success) { diff --git a/src/ServiceControl.Config/Framework/Rx/RxConductorBaseWithActiveItem.cs b/src/ServiceControl.Config/Framework/Rx/RxConductorBaseWithActiveItem.cs index 6e3669f930..5b6107fe2b 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxConductorBaseWithActiveItem.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxConductorBaseWithActiveItem.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Config.Framework.Rx { + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; @@ -17,15 +18,15 @@ object IHaveActiveItem.ActiveItem set { ActiveItem = (T)value; } } - protected virtual async Task ChangeActiveItem(T newItem, bool closePrevious) + protected virtual async Task ChangeActiveItem(T newItem, bool closePrevious, CancellationToken cancellationToken = default) { - await ScreenExtensions.TryDeactivateAsync(activeItem, closePrevious); + await ScreenExtensions.TryDeactivateAsync(activeItem, closePrevious, cancellationToken); newItem = EnsureItem(newItem); if (IsActive) { - await ScreenExtensions.TryActivateAsync(newItem); + await ScreenExtensions.TryActivateAsync(newItem, cancellationToken); } activeItem = newItem; diff --git a/src/ServiceControl.Config/Framework/Rx/RxConductorWithCollectionOneActive.cs b/src/ServiceControl.Config/Framework/Rx/RxConductorWithCollectionOneActive.cs index 95cb7a932a..375cd87aaa 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxConductorWithCollectionOneActive.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxConductorWithCollectionOneActive.cs @@ -48,23 +48,23 @@ public override IEnumerable GetChildren() return items; } - public override async Task ActivateItem(T item) + public override async Task ActivateItem(T item, CancellationToken cancellationToken = default) { if (item != null && item.Equals(ActiveItem)) { if (IsActive) { - await ScreenExtensions.TryActivateAsync(item); + await ScreenExtensions.TryActivateAsync(item, cancellationToken); OnActivationProcessed(item, true); } return; } - await ChangeActiveItem(item, false); + await ChangeActiveItem(item, false, cancellationToken); } - public override async Task DeactivateItem(T item, bool close) + public override async Task DeactivateItem(T item, bool close, CancellationToken cancellationToken = default) { if (item == null) { @@ -73,30 +73,30 @@ public override async Task DeactivateItem(T item, bool close) if (!close) { - await ScreenExtensions.TryDeactivateAsync(item, false); + await ScreenExtensions.TryDeactivateAsync(item, false, cancellationToken); } else { - var result = await CloseStrategy.ExecuteAsync(new[] { item }); + var result = await CloseStrategy.ExecuteAsync(new[] { item }, cancellationToken); if (result.CloseCanOccur) { - await CloseItemCore(item); + await CloseItemCore(item, cancellationToken); } } } - async Task CloseItemCore(T item) + async Task CloseItemCore(T item, CancellationToken cancellationToken) { if (item.Equals(ActiveItem)) { var index = items.IndexOf(item); var next = DetermineNextItemToActivate(items, index); - await ChangeActiveItem(next, true); + await ChangeActiveItem(next, true, cancellationToken); } else { - await ScreenExtensions.TryDeactivateAsync(item, true); + await ScreenExtensions.TryDeactivateAsync(item, true, cancellationToken); } items.Remove(item); @@ -119,7 +119,7 @@ protected virtual T DetermineNextItemToActivate(IList list, int lastIndex) return default; } - public override async Task CanCloseAsync(CancellationToken cancellationToken) + public override async Task CanCloseAsync(CancellationToken cancellationToken = default) { var result = await CloseStrategy.ExecuteAsync(items.ToList(), cancellationToken); var canClose = result.CloseCanOccur; @@ -140,7 +140,7 @@ public override async Task CanCloseAsync(CancellationToken cancellationTok while (closable.Contains(next)); var previousActive = ActiveItem; - await ChangeActiveItem(next, true); + await ChangeActiveItem(next, true, cancellationToken); items.Remove(previousActive); var stillToClose = closable.ToList(); @@ -150,7 +150,7 @@ public override async Task CanCloseAsync(CancellationToken cancellationTok await Task.WhenAll( from deactivatable in closable.OfType() - select deactivatable.DeactivateAsync(true) + select deactivatable.DeactivateAsync(true, cancellationToken) ); items.RemoveRange(closable); @@ -159,9 +159,9 @@ select deactivatable.DeactivateAsync(true) return canClose; } - protected override Task OnActivate() => ScreenExtensions.TryActivateAsync(ActiveItem); + protected override Task OnActivate(CancellationToken cancellationToken = default) => ScreenExtensions.TryActivateAsync(ActiveItem, cancellationToken); - protected override async Task OnDeactivate(bool close) + protected override async Task OnDeactivate(bool close, CancellationToken cancellationToken = default) { if (close) { @@ -169,14 +169,14 @@ protected override async Task OnDeactivate(bool close) { if (item is IDeactivate deactivatable) { - await deactivatable.DeactivateAsync(true); + await deactivatable.DeactivateAsync(true, cancellationToken); } } items.Clear(); } else { - await ScreenExtensions.TryDeactivateAsync(ActiveItem, false); + await ScreenExtensions.TryDeactivateAsync(ActiveItem, false, cancellationToken); } } diff --git a/src/ServiceControl.Config/Framework/Rx/RxProgressScreen.cs b/src/ServiceControl.Config/Framework/Rx/RxProgressScreen.cs index 5453f17cd7..736fb942f6 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxProgressScreen.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxProgressScreen.cs @@ -47,7 +47,7 @@ public int ProgressPercent } } - public override Task CanCloseAsync(CancellationToken cancellationToken) => Task.FromResult(!InProgress); + public override Task CanCloseAsync(CancellationToken cancellationToken = default) => Task.FromResult(!InProgress); void NotifyUpdates() { diff --git a/src/ServiceControl.Config/Framework/Rx/RxScreen.cs b/src/ServiceControl.Config/Framework/Rx/RxScreen.cs index 44b7a33f5f..7d5a91b6d9 100644 --- a/src/ServiceControl.Config/Framework/Rx/RxScreen.cs +++ b/src/ServiceControl.Config/Framework/Rx/RxScreen.cs @@ -63,12 +63,12 @@ async Task IActivate.ActivateAsync(CancellationToken cancellationToken) if (!IsInitialized) { IsInitialized = initialized = true; - await OnInitialize(); + await OnInitialize(cancellationToken); } IsActive = true; Log.Info("Activating {0}.", this); - await OnActivate(); + await OnActivate(cancellationToken); await Activated(this, new ActivationEventArgs { @@ -87,7 +87,7 @@ async Task IDeactivate.DeactivateAsync(bool close, CancellationToken cancellatio IsActive = false; Log.Info("Deactivating {0}.", this); - await OnDeactivate(close); + await OnDeactivate(close, cancellationToken); await Deactivated(this, new DeactivationEventArgs { @@ -106,14 +106,16 @@ async Task IDeactivate.DeactivateAsync(bool close, CancellationToken cancellatio /// Called to check whether or not this instance can close. /// /// The implementor calls this action with the result of the close check. - public virtual Task CanCloseAsync(CancellationToken cancellationToken) => Task.FromResult(true); + public virtual Task CanCloseAsync(CancellationToken cancellationToken = default) => Task.FromResult(true); /// /// Tries to close this instance by asking its Parent to initiate shutdown or by asking its corresponding view to close. /// Also provides an opportunity to pass a dialog result to it's corresponding view. /// /// The dialog result. +#pragma warning disable PS0018 // Caliburn.Micro's IClose.TryCloseAsync declares no CancellationToken, so one cannot be added here public virtual Task TryCloseAsync(bool? dialogResult = null) +#pragma warning restore PS0018 { Result = dialogResult; @@ -141,18 +143,18 @@ static bool IsHandler(object obj) /// /// Called when initializing. /// - protected virtual Task OnInitialize() => Task.CompletedTask; + protected virtual Task OnInitialize(CancellationToken cancellationToken = default) => Task.CompletedTask; /// /// Called when activating. /// - protected virtual Task OnActivate() => Task.CompletedTask; + protected virtual Task OnActivate(CancellationToken cancellationToken = default) => Task.CompletedTask; /// /// Called when deactivating. /// /// Inidicates whether this instance will be closed. - protected virtual Task OnDeactivate(bool close) => Task.CompletedTask; + protected virtual Task OnDeactivate(bool close, CancellationToken cancellationToken = default) => Task.CompletedTask; static readonly ILog Log = LogManager.GetLog(typeof(Screen)); } diff --git a/src/ServiceControl.Config/Framework/ServiceControlWindowManager.cs b/src/ServiceControl.Config/Framework/ServiceControlWindowManager.cs index 5de1ec8bdc..cff3833016 100644 --- a/src/ServiceControl.Config/Framework/ServiceControlWindowManager.cs +++ b/src/ServiceControl.Config/Framework/ServiceControlWindowManager.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using System.Windows; using Caliburn.Micro; @@ -13,23 +14,23 @@ public interface IServiceControlWindowManager : IWindowManager { - Task NavigateTo(RxScreen screen, object context = null, IDictionary settings = null); + Task NavigateTo(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default); - Task ShowInnerDialog(RxScreen screen, object context = null, IDictionary settings = null); + Task ShowInnerDialog(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default); - Task ShowOverlayDialog(RxScreen screen, object context = null, IDictionary settings = null); + Task ShowOverlayDialog(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default); - Task ShowMessage(string title, string message, string acceptText = "Ok", bool hideCancel = false); + Task ShowMessage(string title, string message, string acceptText = "Ok", bool hideCancel = false, CancellationToken cancellationToken = default); - Task ShowYesNoCancelDialog(string title, string message, string question, string yesText, string noText); + Task ShowYesNoCancelDialog(string title, string message, string question, string yesText, string noText, CancellationToken cancellationToken = default); - Task ShowYesNoDialog(string title, string message, string question, string yesText, string noText); + Task ShowYesNoDialog(string title, string message, string question, string yesText, string noText, CancellationToken cancellationToken = default); - Task ShowSliderDialog(SliderDialogViewModel viewModel); + Task ShowSliderDialog(SliderDialogViewModel viewModel, CancellationToken cancellationToken = default); - Task ShowTextBoxDialog(TextBoxDialogViewModel viewModel); + Task ShowTextBoxDialog(TextBoxDialogViewModel viewModel, CancellationToken cancellationToken = default); - Task ShowActionReport(ReportCard reportcard, string title, string errorsMessage = "", string warningsMessage = ""); + Task ShowActionReport(ReportCard reportcard, string title, string errorsMessage = "", string warningsMessage = "", CancellationToken cancellationToken = default); void ScrollFirstErrorIntoView(object viewModel, object context = null); } @@ -41,22 +42,22 @@ public ServiceControlWindowManager(Func reportC this.reportCardViewModelFactory = reportCardViewModelFactory; } - public Task NavigateTo(RxScreen screen, object context = null, IDictionary settings = null) + public Task NavigateTo(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default) { var shell = GetShell(); shell.ActiveContext = context; - return shell.ActivateItem(screen); + return shell.ActivateItem(screen, cancellationToken); } - public async Task ShowInnerDialog(RxScreen screen, object context = null, IDictionary settings = null) + public async Task ShowInnerDialog(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default) { var shell = GetShell(); var previousContext = shell.ActiveContext; shell.IsModal = true; shell.ActiveContext = context; - await shell.ActivateItem(screen); + await shell.ActivateItem(screen, cancellationToken); screen.RunModal(); shell.IsModal = false; shell.ActiveContext = previousContext; @@ -69,62 +70,62 @@ public Task NavigateTo(RxScreen screen, object context = null, IDictionary ShowOverlayDialog(RxScreen screen, object context = null, IDictionary settings = null) + public async Task ShowOverlayDialog(RxScreen screen, object context = null, IDictionary settings = null, CancellationToken cancellationToken = default) { var shell = GetShell(); var previousContext = shell.ActiveContext; shell.Overlay = screen; shell.ActiveContext = context; - await screen.ActivateAsync(); + await ((IActivate)screen).ActivateAsync(cancellationToken); screen.RunModal(); shell.Overlay = null; shell.ActiveContext = previousContext; return screen.Result; } - public async Task ShowMessage(string title, string message, string acceptText = "Ok", bool hideCancel = false) + public async Task ShowMessage(string title, string message, string acceptText = "Ok", bool hideCancel = false, CancellationToken cancellationToken = default) { var messageBox = new MessageBoxViewModel(title, message, acceptText, hideCancel); - var result = await ShowOverlayDialog(messageBox); + var result = await ShowOverlayDialog(messageBox, cancellationToken: cancellationToken); return result ?? false; } - public Task ShowYesNoCancelDialog(string title, string message, string question, string yesText, string noText) + public Task ShowYesNoCancelDialog(string title, string message, string question, string yesText, string noText, CancellationToken cancellationToken = default) { var messageBox = new YesNoCancelViewModel(title, message, question, yesText, noText); - return ShowOverlayDialog(messageBox); + return ShowOverlayDialog(messageBox, cancellationToken: cancellationToken); } - public async Task ShowYesNoDialog(string title, string message, string question, string yesText, string noText) + public async Task ShowYesNoDialog(string title, string message, string question, string yesText, string noText, CancellationToken cancellationToken = default) { var messageBox = new YesNoCancelViewModel(title, message, question, yesText, noText) { ShowCancelButton = false }; - var result = await ShowOverlayDialog(messageBox); + var result = await ShowOverlayDialog(messageBox, cancellationToken: cancellationToken); return result.Value; } - public async Task ShowSliderDialog(SliderDialogViewModel viewModel) + public async Task ShowSliderDialog(SliderDialogViewModel viewModel, CancellationToken cancellationToken = default) { - var result = await ShowOverlayDialog(viewModel); + var result = await ShowOverlayDialog(viewModel, cancellationToken: cancellationToken); return result ?? false; } - public async Task ShowTextBoxDialog(TextBoxDialogViewModel viewModel) + public async Task ShowTextBoxDialog(TextBoxDialogViewModel viewModel, CancellationToken cancellationToken = default) { - var result = await ShowOverlayDialog(viewModel); + var result = await ShowOverlayDialog(viewModel, cancellationToken: cancellationToken); return result ?? false; } - public async Task ShowActionReport(ReportCard reportcard, string title, string errorsMessage = "", string warningsMessage = "") + public async Task ShowActionReport(ReportCard reportcard, string title, string errorsMessage = "", string warningsMessage = "", CancellationToken cancellationToken = default) { var messageBox = reportCardViewModelFactory(reportcard); messageBox.Title = title; messageBox.ErrorsMessage = errorsMessage; messageBox.WarningsMessage = warningsMessage; - var result = await ShowOverlayDialog(messageBox); + var result = await ShowOverlayDialog(messageBox, cancellationToken: cancellationToken); return result ?? false; } diff --git a/src/ServiceControl.Config/UI/AdvancedOptions/ServiceControlAdvancedViewModel.cs b/src/ServiceControl.Config/UI/AdvancedOptions/ServiceControlAdvancedViewModel.cs index d83e9edb0a..638fad9e38 100644 --- a/src/ServiceControl.Config/UI/AdvancedOptions/ServiceControlAdvancedViewModel.cs +++ b/src/ServiceControl.Config/UI/AdvancedOptions/ServiceControlAdvancedViewModel.cs @@ -140,7 +140,7 @@ public bool AllowStop public string ForcedUpgradeBackupLocation => ServiceControlInstance.DatabaseBackupPath; - public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken) + public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken = default) { NotifyOfPropertyChange("AllowStop"); NotifyOfPropertyChange("IsRunning"); @@ -149,7 +149,7 @@ public Task HandleAsync(PostRefreshInstances message, CancellationToken cancella return Task.CompletedTask; } - public async Task StartService(IProgressObject progress, bool maintenanceMode) + public async Task StartService(IProgressObject progress, bool maintenanceMode, CancellationToken cancellationToken = default) { var disposeProgress = progress == null; var result = false; @@ -170,7 +170,7 @@ await Task.Run(() => } result = ServiceControlInstance.TryStartService(); - }); + }, cancellationToken); return result; } @@ -183,7 +183,7 @@ await Task.Run(() => } } - public async Task StopService(IProgressObject progress = null) + public async Task StopService(IProgressObject progress = null, CancellationToken cancellationToken = default) { var disposeProgress = progress == null; var result = false; @@ -200,7 +200,7 @@ await Task.Run(() => { ServiceControlInstance.DisableMaintenanceMode(); } - }); + }, cancellationToken); return result; } diff --git a/src/ServiceControl.Config/UI/FeedBack/FeedBackViewModel.cs b/src/ServiceControl.Config/UI/FeedBack/FeedBackViewModel.cs index ed627be581..04dee4fccd 100644 --- a/src/ServiceControl.Config/UI/FeedBack/FeedBackViewModel.cs +++ b/src/ServiceControl.Config/UI/FeedBack/FeedBackViewModel.cs @@ -1,5 +1,7 @@ namespace ServiceControl.Config.UI.FeedBack { + using System; + using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Framework; @@ -15,7 +17,7 @@ public FeedBackViewModel(RaygunFeedback raygunFeedBack) feedBack = raygunFeedBack; validationTemplate = new ValidationTemplate(this); Cancel = Command.Create(async () => await TryCloseAsync(false)); - SendFeedBack = Command.Create(async () => await Send()); + SendFeedBack = Command.Create(async () => await Send(CancellationToken.None)); } public string EmailAddress { get; set; } @@ -30,7 +32,7 @@ public FeedBackViewModel(RaygunFeedback raygunFeedBack) public bool SubmitAttempted { get; set; } - async Task Send() + async Task Send(CancellationToken cancellationToken) { SubmitAttempted = true; if (!validationTemplate.Validate()) @@ -42,9 +44,13 @@ async Task Send() try { - await feedBack.SendFeedBack(EmailAddress, Message, IncludeSystemInfo); + await feedBack.SendFeedBack(EmailAddress, Message, IncludeSystemInfo, cancellationToken); Success = true; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch { Success = false; diff --git a/src/ServiceControl.Config/UI/InstanceAdd/MonitoringAddAttachment.cs b/src/ServiceControl.Config/UI/InstanceAdd/MonitoringAddAttachment.cs index 384118f339..d6d3ada53b 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/MonitoringAddAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/MonitoringAddAttachment.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.UI.InstanceAdd { using System; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -40,7 +41,7 @@ bool IsInProgress() return viewModel != null && !viewModel.InProgress; } - async Task Add() + async Task Add(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; if (!viewModel.ValidationTemplate.Validate()) @@ -71,7 +72,7 @@ async Task Add() ServiceAccountPwd = viewModel.Password }; - if (!await commandChecks.ValidateNewInstance(instanceMetadata)) + if (!await commandChecks.ValidateNewInstance([instanceMetadata], cancellationToken)) { viewModel.InProgress = false; return; @@ -79,11 +80,11 @@ async Task Add() using (var progress = viewModel.GetProgressObject("ADDING INSTANCE")) { - var reportCard = await Task.Run(() => installer.Add(instanceMetadata, progress, PromptToProceed)); + var reportCard = await Task.Run(() => installer.Add(instanceMetadata, progress, PromptToProceed, cancellationToken), cancellationToken); if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:", cancellationToken); return; } @@ -95,14 +96,14 @@ async Task Add() await viewModel.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); } - async Task PromptToProceed(PathInfo pathInfo) + async Task PromptToProceed(PathInfo pathInfo, CancellationToken cancellationToken) { var result = false; - await Execute.OnUIThreadAsync(async () => { result = await windowManager.ShowYesNoDialog("ADDING INSTANCE QUESTION - DIRECTORY NOT EMPTY", $"The directory specified as the {pathInfo.Name} is not empty.", $"Are you sure you want to use '{pathInfo.Path}' ?", "Yes use it", "No I want to change it"); }); + await Execute.OnUIThreadAsync(async () => { result = await windowManager.ShowYesNoDialog("ADDING INSTANCE QUESTION - DIRECTORY NOT EMPTY", $"The directory specified as the {pathInfo.Name} is not empty.", $"Are you sure you want to use '{pathInfo.Path}' ?", "Yes use it", "No I want to change it", cancellationToken); }); return result; } diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs index 5c9863dd03..8cf5c88352 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.UI.InstanceAdd { using System; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -38,7 +39,7 @@ protected override void OnAttach() bool IsInProgress() => viewModel != null && !viewModel.InProgress; - async Task Add() + async Task Add(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; if (!viewModel.ValidationTemplate.Validate()) @@ -104,7 +105,7 @@ async Task Add() auditNewInstance.EnableFullTextSearchOnBodies = viewModel.ServiceControlAudit.EnableFullTextSearchOnBodies.Value; } - if (!await commandChecks.ValidateNewInstance(serviceControlNewInstance, auditNewInstance)) + if (!await commandChecks.ValidateNewInstance([serviceControlNewInstance, auditNewInstance], cancellationToken)) { viewModel.InProgress = false; return; @@ -119,7 +120,7 @@ async Task Add() { using (var progress = viewModel.GetProgressObject("ADDING INSTANCE")) { - var installationCancelled = await InstallInstance(serviceControlNewInstance, progress); + var installationCancelled = await InstallInstance(serviceControlNewInstance, progress, cancellationToken); if (installationCancelled) { return; @@ -131,7 +132,7 @@ async Task Add() { using (var progress = viewModel.GetProgressObject("ADDING AUDIT INSTANCE")) { - var installationCancelled = await InstallInstance(auditNewInstance, progress); + var installationCancelled = await InstallInstance(auditNewInstance, progress, cancellationToken); if (installationCancelled) { return; @@ -141,16 +142,16 @@ async Task Add() await viewModel.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); } - async Task InstallInstance(ServiceControlNewInstance instanceData, IProgressObject progress) + async Task InstallInstance(ServiceControlNewInstance instanceData, IProgressObject progress, CancellationToken cancellationToken) { - var reportCard = await Task.Run(() => serviceControlInstaller.Add(instanceData, progress, PromptToProceed)); + var reportCard = await Task.Run(() => serviceControlInstaller.Add(instanceData, progress, PromptToProceed, cancellationToken), cancellationToken); if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:", cancellationToken); return true; } @@ -162,13 +163,13 @@ async Task InstallInstance(ServiceControlNewInstance instanceData, IProgre return false; } - async Task InstallInstance(ServiceControlAuditNewInstance instanceData, IProgressObject progress) + async Task InstallInstance(ServiceControlAuditNewInstance instanceData, IProgressObject progress, CancellationToken cancellationToken) { - var reportCard = await Task.Run(() => serviceControlAuditInstaller.Add(instanceData, progress, PromptToProceed)); + var reportCard = await Task.Run(() => serviceControlAuditInstaller.Add(instanceData, progress, PromptToProceed, cancellationToken), cancellationToken); if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES ADDING INSTANCE", "Could not add new instance because of the following errors:", "There were some warnings while adding the instance:", cancellationToken); return true; } @@ -180,11 +181,11 @@ async Task InstallInstance(ServiceControlAuditNewInstance instanceData, IP return false; } - async Task PromptToProceed(PathInfo pathInfo) + async Task PromptToProceed(PathInfo pathInfo, CancellationToken cancellationToken) { var result = false; - await Execute.OnUIThreadAsync(async () => { result = await windowManager.ShowYesNoDialog("ADDING INSTANCE QUESTION - DIRECTORY NOT EMPTY", $"The directory specified as the {pathInfo.Name} is not empty.", $"Are you sure you want to use '{pathInfo.Path}' ?", "Yes use it", "No I want to change it"); }); + await Execute.OnUIThreadAsync(async () => { result = await windowManager.ShowYesNoDialog("ADDING INSTANCE QUESTION - DIRECTORY NOT EMPTY", $"The directory specified as the {pathInfo.Name} is not empty.", $"Are you sure you want to use '{pathInfo.Path}' ?", "Yes use it", "No I want to change it", cancellationToken); }); return result; } diff --git a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs index 4a1c9dd14d..8264f7b54d 100644 --- a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs @@ -362,7 +362,7 @@ public bool AllowStop public bool Exists() => ServiceInstance.Service.Exists(); - public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken) + public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken = default) { UpdateServiceProperties(); @@ -383,7 +383,7 @@ public Task HandleAsync(PostRefreshInstances message, CancellationToken cancella return Task.CompletedTask; } - public async Task StartService(IProgressObject progress = null) + public async Task StartService(IProgressObject progress = null, CancellationToken cancellationToken = default) { var disposeProgress = progress == null; var result = false; @@ -414,7 +414,7 @@ public async Task StartService(IProgressObject progress = null) } } - public async Task StopService(IProgressObject progress = null) + public async Task StopService(IProgressObject progress = null, CancellationToken cancellationToken = default) { var disposeProgress = progress == null; var result = false; @@ -431,7 +431,7 @@ await Task.Run(() => { ServiceControlInstance.DisableMaintenanceMode(); } - }); + }, cancellationToken); UpdateServiceProperties(); diff --git a/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditAttachment.cs b/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditAttachment.cs index 86d38420de..4c0d22e74d 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditAttachment.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Config.UI.InstanceEdit { using System; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -28,7 +29,7 @@ protected override void OnAttach() viewModel.Cancel = Command.Create(async () => { await viewModel.TryCloseAsync(false); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), CancellationToken.None); }, IsInProgress); } @@ -37,7 +38,7 @@ bool IsInProgress() return viewModel != null && !viewModel.InProgress; } - async Task Save() + async Task Save(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; if (!viewModel.ValidationTemplate.Validate()) @@ -53,7 +54,7 @@ async Task Save() if (instance.Service.Status == ServiceControllerStatus.Running) { var shouldProceed = await windowManager.ShowMessage("STOP INSTANCE AND MODIFY", - $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed."); + $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed.", cancellationToken: cancellationToken); if (!shouldProceed) { return; @@ -81,7 +82,7 @@ async Task Save() if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:", cancellationToken: cancellationToken); return; } @@ -90,7 +91,7 @@ async Task Save() await viewModel.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); } readonly IServiceControlWindowManager windowManager; diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditAttachment.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditAttachment.cs index b7b876dbff..3a3c4f8e97 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditAttachment.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Config.UI.InstanceEdit { using System; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -28,7 +29,7 @@ protected override void OnAttach() viewModel.Cancel = Command.Create(async () => { await viewModel.TryCloseAsync(false); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), CancellationToken.None); }, IsInProgress); } @@ -37,7 +38,7 @@ bool IsInProgress() return viewModel != null && !viewModel.InProgress; } - async Task Save() + async Task Save(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; if (!viewModel.ValidationTemplate.Validate()) @@ -52,7 +53,7 @@ async Task Save() if (instance.Service.Status == ServiceControllerStatus.Running) { var shouldProceed = await windowManager.ShowMessage("STOP INSTANCE AND MODIFY", - $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed."); + $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed.", cancellationToken: cancellationToken); if (!shouldProceed) { return; @@ -86,7 +87,7 @@ async Task Save() if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:", cancellationToken: cancellationToken); return; } @@ -95,7 +96,7 @@ async Task Save() await viewModel.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); } readonly IServiceControlWindowManager windowManager; diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs index f978f2f82c..3ae699c8fd 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Config.UI.InstanceEdit { using System; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using Caliburn.Micro; using Events; @@ -28,7 +29,7 @@ protected override void OnAttach() viewModel.Cancel = Command.Create(async () => { await viewModel.TryCloseAsync(false); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), CancellationToken.None); }, IsInProgress); } @@ -37,7 +38,7 @@ bool IsInProgress() return viewModel != null && !viewModel.InProgress; } - async Task Save() + async Task Save(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; if (!viewModel.ValidationTemplate.Validate()) @@ -52,7 +53,7 @@ async Task Save() if (instance.Service.Status == ServiceControllerStatus.Running) { var shouldProceed = await windowManager.ShowMessage("STOP INSTANCE AND MODIFY", - $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed."); + $"{instance.Name} needs to be stopped in order to modify the settings. Do you want to proceed.", cancellationToken: cancellationToken); if (!shouldProceed) { return; @@ -88,7 +89,7 @@ async Task Save() if (reportCard.HasErrors || reportCard.HasWarnings) { - await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:"); + await windowManager.ShowActionReport(reportCard, "ISSUES MODIFYING INSTANCE", "Could not modify instance because of the following errors:", "There were some warnings while modifying the instance:", cancellationToken: cancellationToken); return; } @@ -97,7 +98,7 @@ async Task Save() await viewModel.TryCloseAsync(true); - await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances(), cancellationToken); } readonly IServiceControlWindowManager windowManager; diff --git a/src/ServiceControl.Config/UI/License/LicenseViewModel.cs b/src/ServiceControl.Config/UI/License/LicenseViewModel.cs index efcfd2d27d..8eecdde9d1 100644 --- a/src/ServiceControl.Config/UI/License/LicenseViewModel.cs +++ b/src/ServiceControl.Config/UI/License/LicenseViewModel.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Caliburn.Micro; @@ -29,7 +30,7 @@ class LicenseViewModel : RxScreen public string ExtendLicenseUrl { get; set; } - protected override Task OnActivate() + protected override Task OnActivate(CancellationToken cancellationToken = default) { RefreshLicenseInfo(); return Task.CompletedTask; @@ -46,7 +47,9 @@ void RefreshLicenseInfo() ExtendLicenseUrl = $"https://particular.net/license/nservicebus?t={(license.IsEvaluationLicense ? 0 : 1)}&p=servicecontrol"; } +#pragma warning disable PS0018 // Bound to AwaitableSelectPathCommand's Func, which the tokenless ICommand.Execute drives async Task OpenLicenseFile(string path) +#pragma warning restore PS0018 { if (LicenseManager.TryImportLicense(path, out var importError)) { diff --git a/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs b/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs index 0313b9de8e..bb89720cd6 100644 --- a/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs +++ b/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs @@ -67,7 +67,7 @@ public string ConfigurationErrorMessage [AlsoNotifyFor(nameof(OrderedInstances), nameof(HasConfigurationErrors), nameof(ConfigurationErrorMessage))] IList Instances { get; } - public Task HandleAsync(LicenseUpdated licenseUpdatedEvent, CancellationToken cancellationToken) + public Task HandleAsync(LicenseUpdated licenseUpdatedEvent, CancellationToken cancellationToken = default) { // on license change inform each instance to refresh the license (1.23.0 and below don't support this) foreach (var instance in Instances) @@ -105,13 +105,13 @@ public Task HandleAsync(LicenseUpdated licenseUpdatedEvent, CancellationToken ca /// before the PostRefreshInstances handlers do all their rebinding. That way, deleting an instance /// in PowerShell won't cause an error from a deleted instance viewmodel trying to refresh itself. /// - public async Task HandleAsync(RefreshInstances message, CancellationToken cancellationToken) + public async Task HandleAsync(RefreshInstances message, CancellationToken cancellationToken = default) { AddAndRemoveInstances(); await EventAggregator.PublishOnUIThreadAsync(new PostRefreshInstances(), cancellationToken); } - public async Task HandleAsync(ResetInstances message, CancellationToken cancellationToken) + public async Task HandleAsync(ResetInstances message, CancellationToken cancellationToken = default) { foreach (var instance in Instances) { diff --git a/src/ServiceControl.Config/UI/MessageBox/ExceptionMessageBox.xaml.cs b/src/ServiceControl.Config/UI/MessageBox/ExceptionMessageBox.xaml.cs index bfb921fa8e..40f7cb82f6 100644 --- a/src/ServiceControl.Config/UI/MessageBox/ExceptionMessageBox.xaml.cs +++ b/src/ServiceControl.Config/UI/MessageBox/ExceptionMessageBox.xaml.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.UI.MessageBox { using System; + using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -95,7 +96,9 @@ void CopyClick(object sender, RoutedEventArgs e) Clipboard.SetText(ErrorDetails.Text); } +#pragma warning disable PS0018 // Bound to AwaitableDelegateCommand's Func, which the tokenless ICommand.Execute drives async Task CallReportClick(object sender) +#pragma warning restore PS0018 { ProgressTitle = "Processing"; ProgressMessage = "Sending Exception Details..."; diff --git a/src/ServiceControl.Config/UI/Shell/FeedBackAttachment.cs b/src/ServiceControl.Config/UI/Shell/FeedBackAttachment.cs index 128ba1d451..a0616a2f56 100644 --- a/src/ServiceControl.Config/UI/Shell/FeedBackAttachment.cs +++ b/src/ServiceControl.Config/UI/Shell/FeedBackAttachment.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.UI.Shell { using System; + using System.Threading; using System.Threading.Tasks; using FeedBack; using Framework; @@ -23,10 +24,10 @@ public FeedBackAttachment( protected override void OnAttach() { - viewModel.OpenFeedBack = Command.Create(async () => await FeedBack()); + viewModel.OpenFeedBack = Command.Create(async () => await FeedBack(CancellationToken.None)); } - async Task FeedBack() + async Task FeedBack(CancellationToken cancellationToken) { if (raygunFeedBack.Enabled) { diff --git a/src/ServiceControl.Config/UI/Shell/LicenseStatusManager.cs b/src/ServiceControl.Config/UI/Shell/LicenseStatusManager.cs index 241163baac..f73dc3e7de 100644 --- a/src/ServiceControl.Config/UI/Shell/LicenseStatusManager.cs +++ b/src/ServiceControl.Config/UI/Shell/LicenseStatusManager.cs @@ -37,13 +37,13 @@ public bool ShowPopup public bool IsSerious { get; private set; } public bool HasFocus { get; private set; } - public Task HandleAsync(FocusChanged message, CancellationToken cancellationToken) + public Task HandleAsync(FocusChanged message, CancellationToken cancellationToken = default) { HasFocus = message.HasFocus; return Task.CompletedTask; } - public Task HandleAsync(LicenseUpdated message, CancellationToken cancellationToken) + public Task HandleAsync(LicenseUpdated message, CancellationToken cancellationToken = default) { RefreshStatus(false); return Task.CompletedTask; diff --git a/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs b/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs index ea9328d98f..fb50e3a731 100644 --- a/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs +++ b/src/ServiceControl.Config/UI/Shell/ShellViewModel.cs @@ -86,20 +86,20 @@ IEventAggregator eventAggregator public string AvailableUpgradeReleaseLink { get; set; } - public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken) => RefreshInstances(); + public Task HandleAsync(PostRefreshInstances message, CancellationToken cancellationToken = default) => RefreshInstances(cancellationToken); - public Task HandleAsync(ResetInstances message, CancellationToken cancellationToken) => RefreshInstances(); + public Task HandleAsync(ResetInstances message, CancellationToken cancellationToken = default) => RefreshInstances(cancellationToken); - protected override Task OnInitialize() => RefreshInstances(); + protected override Task OnInitialize(CancellationToken cancellationToken = default) => RefreshInstances(cancellationToken); - protected override async Task OnActivate() + protected override async Task OnActivate(CancellationToken cancellationToken = default) { - await base.OnActivate(); + await base.OnActivate(cancellationToken); BeginCheckForUpdates(); } - public async Task RefreshInstances() + public async Task RefreshInstances(CancellationToken cancellationToken = default) { HasInstances = InstanceFinder.AllInstances().Any(); @@ -107,11 +107,11 @@ public async Task RefreshInstances() { if (HasInstances) { - await ActivateItem(listInstances); + await ActivateItem(listInstances, cancellationToken); } else { - await ActivateItem(noInstances); + await ActivateItem(noInstances, cancellationToken); } } } @@ -125,16 +125,16 @@ void BeginCheckForUpdates() return; } - updateCheckTask = CheckForUpdates(); + updateCheckTask = CheckForUpdates(CancellationToken.None); NotifyOfPropertyChange(nameof(IsCheckingForUpdate)); } - async Task CheckForUpdates() + async Task CheckForUpdates(CancellationToken cancellationToken) { try { - var availableUpgradeRelease = await VersionCheckerHelper.GetLatestRelease(AppVersion); + var availableUpgradeRelease = await VersionCheckerHelper.GetLatestRelease(AppVersion, cancellationToken); if (availableUpgradeRelease.Version == AppVersion) { @@ -147,6 +147,10 @@ async Task CheckForUpdates() UpdateAvailable = true; } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch { UpdateAvailable = false; diff --git a/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs b/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs index e91190b5e3..d1bf7786d3 100644 --- a/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs +++ b/src/ServiceControl.Config/UI/Shell/VersionCheckerHelper.cs @@ -8,14 +8,15 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; + using System.Threading; using System.Threading.Tasks; using NuGet.Versioning; public static class VersionCheckerHelper { - public static async Task GetLatestRelease(SemanticVersion currentVersion) + public static async Task GetLatestRelease(SemanticVersion currentVersion, CancellationToken cancellationToken = default) { - List releases = await GetVersionInformation(); + List releases = await GetVersionInformation(cancellationToken); if (releases != null) { @@ -31,11 +32,15 @@ public static async Task GetLatestRelease(SemanticVersion currentVersio return new Release(currentVersion); } - static async Task> GetVersionInformation() + static async Task> GetVersionInformation(CancellationToken cancellationToken) { try { - return await httpClient.GetFromJsonAsync>("https://s3.us-east-1.amazonaws.com/platformupdate.particular.net/servicecontrol.txt"); + return await httpClient.GetFromJsonAsync>("https://s3.us-east-1.amazonaws.com/platformupdate.particular.net/servicecontrol.txt", cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch { diff --git a/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceAttachment.cs b/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceAttachment.cs index 49cd2b357d..80e0b464de 100644 --- a/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceAttachment.cs +++ b/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceAttachment.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Config.UI.Upgrades { + using System.Threading; using System.Threading.Tasks; using Framework; using ReactiveUI; @@ -28,7 +29,7 @@ protected override void OnAttach() viewModel.Continue = ReactiveCommand.CreateFromTask(Continue); } - async Task Continue() + async Task Continue(CancellationToken cancellationToken) { viewModel.SubmitAttempted = true; diff --git a/src/ServiceControl.Management.PowerShell/.editorconfig b/src/ServiceControl.Management.PowerShell/.editorconfig index 2815635efb..c0448b0dda 100644 --- a/src/ServiceControl.Management.PowerShell/.editorconfig +++ b/src/ServiceControl.Management.PowerShell/.editorconfig @@ -1,6 +1 @@ [*.cs] - -# 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.PS0018.severity = none diff --git a/src/ServiceControl.Management.PowerShell/Cmdlets/AuditInstances/NewServiceControlAuditInstance.cs b/src/ServiceControl.Management.PowerShell/Cmdlets/AuditInstances/NewServiceControlAuditInstance.cs index c531667c82..768041419f 100644 --- a/src/ServiceControl.Management.PowerShell/Cmdlets/AuditInstances/NewServiceControlAuditInstance.cs +++ b/src/ServiceControl.Management.PowerShell/Cmdlets/AuditInstances/NewServiceControlAuditInstance.cs @@ -2,6 +2,7 @@ { using System; using System.Management.Automation; + using System.Threading; using System.Threading.Tasks; using ServiceControlInstaller.Engine.Instances; using ServiceControlInstaller.Engine.Unattended; @@ -172,7 +173,7 @@ protected override void ProcessRecord() { return; } - if (!checks.ValidateNewInstance(newAuditInstance).GetAwaiter().GetResult()) + if (!checks.ValidateNewInstance([newAuditInstance]).GetAwaiter().GetResult()) { return; } @@ -201,7 +202,7 @@ protected override void ProcessRecord() } } - Task PromptToProceed(PathInfo pathInfo) + Task PromptToProceed(PathInfo pathInfo, CancellationToken cancellationToken) { if (!pathInfo.CheckIfEmpty) { diff --git a/src/ServiceControl.Management.PowerShell/Cmdlets/MonitoringInstances/NewMonitoringInstance.cs b/src/ServiceControl.Management.PowerShell/Cmdlets/MonitoringInstances/NewMonitoringInstance.cs index 8f90c7e732..315a8bb828 100644 --- a/src/ServiceControl.Management.PowerShell/Cmdlets/MonitoringInstances/NewMonitoringInstance.cs +++ b/src/ServiceControl.Management.PowerShell/Cmdlets/MonitoringInstances/NewMonitoringInstance.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Management.PowerShell { using System; using System.Management.Automation; + using System.Threading; using System.Threading.Tasks; using Cmdlets.Instances; using ServiceControlInstaller.Engine.Instances; @@ -142,7 +143,7 @@ protected override void ProcessRecord() { return; } - if (!checks.ValidateNewInstance(monitoringNewInstance).GetAwaiter().GetResult()) + if (!checks.ValidateNewInstance([monitoringNewInstance]).GetAwaiter().GetResult()) { return; } @@ -171,7 +172,7 @@ protected override void ProcessRecord() } } - Task PromptToProceed(PathInfo pathInfo) + Task PromptToProceed(PathInfo pathInfo, CancellationToken cancellationToken) { if (!pathInfo.CheckIfEmpty) { diff --git a/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs b/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs index 96e905e064..eb2e792fcf 100644 --- a/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs +++ b/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs @@ -4,6 +4,7 @@ namespace ServiceControl.Management.PowerShell using System.IO; using System.Linq; using System.Management.Automation; + using System.Threading; using System.Threading.Tasks; using ServiceControlInstaller.Engine.Instances; using ServiceControlInstaller.Engine.Unattended; @@ -188,7 +189,7 @@ protected override void ProcessRecord() { return; } - if (!checks.ValidateNewInstance(details).GetAwaiter().GetResult()) + if (!checks.ValidateNewInstance([details]).GetAwaiter().GetResult()) { return; } @@ -228,7 +229,7 @@ protected override void ProcessRecord() } } - Task PromptToProceed(PathInfo pathInfo) + Task PromptToProceed(PathInfo pathInfo, CancellationToken cancellationToken) { if (!pathInfo.CheckIfEmpty) { diff --git a/src/ServiceControl.Management.PowerShell/Validation/PowerShellCommandChecks.cs b/src/ServiceControl.Management.PowerShell/Validation/PowerShellCommandChecks.cs index 9d9ffd27bc..ca81261a14 100644 --- a/src/ServiceControl.Management.PowerShell/Validation/PowerShellCommandChecks.cs +++ b/src/ServiceControl.Management.PowerShell/Validation/PowerShellCommandChecks.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Management.Automation; using System.Text; + using System.Threading; using System.Threading.Tasks; using ServiceControlInstaller.Engine; using ServiceControlInstaller.Engine.Configuration.ServiceControl; @@ -28,21 +29,21 @@ void Terminate(string message, string errorId, ErrorCategory category) cmdlet.ThrowTerminatingError(errorRecord); } - protected override Task NotifyForDeprecatedMessageTransport(TransportInfo transport) + protected override Task NotifyForDeprecatedMessageTransport(TransportInfo transport, CancellationToken cancellationToken = default) { var terminateMsg = $"The message transport '{transport.DisplayName}' is not available in this version of ServiceControl, and this instance cannot be upgraded."; Terminate(terminateMsg, "Install Error", ErrorCategory.InvalidOperation); return Task.CompletedTask; } - protected override Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance) + protected override Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance, CancellationToken cancellationToken = default) { var msg = $"The storage format has changed and the {baseInstance.PersistenceManifest.DisplayName} storage engine is no longer available. Upgrading requires a side-by-side deployment of both versions. Migration guidance is available in the version 4 to 5 upgrade guidance at {UpgradeGuide4to5Url}"; Terminate(msg, "Install Error", ErrorCategory.InvalidOperation); return Task.CompletedTask; } - protected override Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo) + protected override Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo, CancellationToken cancellationToken = default) { var nextVersion = upgradeInfo.UpgradePath[0]; var b = new StringBuilder(); @@ -57,19 +58,19 @@ protected override Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeI return Task.CompletedTask; } - protected override Task NotifyError(string title, string message) + protected override Task NotifyError(string title, string message, CancellationToken cancellationToken = default) { Terminate(message, title, ErrorCategory.InvalidOperation); return Task.CompletedTask; } - protected override Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage) + protected override Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage, CancellationToken cancellationToken = default) { Terminate(missingPrereqsMessage, "Missing Prerequisites", ErrorCategory.NotInstalled); return Task.CompletedTask; } - protected override Task PromptForRabbitMqCheck(bool isUpgrade) + protected override Task PromptForRabbitMqCheck(bool isUpgrade, CancellationToken cancellationToken = default) { if (!acknowledgements.Any(ack => ack.Equals(AcknowledgementValues.RabbitMQBrokerVersion310, StringComparison.OrdinalIgnoreCase))) { @@ -90,13 +91,13 @@ protected override Task PromptForRabbitMqCheck(bool isUpgrade) return Task.FromResult(true); } - protected override Task PromptToStopRunningInstance(BaseService instance) + protected override Task PromptToStopRunningInstance(BaseService instance, CancellationToken cancellationToken = default) { // PowerShell assumes you always want to stop the service if it's running return Task.FromResult(true); } - protected override Task PromptToContinueWithForcedUpgrade() + protected override Task PromptToContinueWithForcedUpgrade(CancellationToken cancellationToken = default) { // In PowerShell, you passed the -Force parameter to get here in the first place return Task.FromResult(true); diff --git a/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs b/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs index 97927c0bcf..d45f8f1866 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/RunEngineTasksExplicitly.cs @@ -72,13 +72,13 @@ public async Task CreateInstanceMSMQ() // constructer of ServiceControlInstanceMetadata extracts version from zip details.Version = Constants.CurrentVersion; - await details.Validate(s => Task.FromResult(false)).ConfigureAwait(false); + await details.Validate((s, token) => Task.FromResult(false)).ConfigureAwait(false); if (details.ReportCard.HasErrors) { throw new Exception($"Validation errors: {string.Join("\r\n", details.ReportCard.Errors)}"); } - Assert.DoesNotThrowAsync(() => installer.Add(details, s => Task.FromResult(false))); + Assert.DoesNotThrowAsync(() => installer.Add(details, (s, token) => Task.FromResult(false))); } [Test] diff --git a/src/ServiceControlInstaller.Engine/.editorconfig b/src/ServiceControlInstaller.Engine/.editorconfig index e3bf403607..c0448b0dda 100644 --- a/src/ServiceControlInstaller.Engine/.editorconfig +++ b/src/ServiceControlInstaller.Engine/.editorconfig @@ -1,5 +1 @@ -[*.cs] - -# may be enabled in future -dotnet_diagnostic.PS0013.severity = none # A Func used as a method parameter with a Task, ValueTask, or ValueTask return type argument should have at least one CancellationToken parameter type argument unless it has a parameter type argument implementing ICancellableContext -dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext +[*.cs] diff --git a/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs b/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs index 09a91d848c..acc834a920 100644 --- a/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs +++ b/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.AccessControl; using System.Security.Principal; + using System.Threading; using System.Threading.Tasks; using Accounts; using Configuration; @@ -136,11 +137,11 @@ TransportInfo DetermineTransportPackage() return transport ?? throw new Exception($"{SettingsList.TransportType.Name} value of '{transportAppSetting}' in app.config is invalid."); } - public async Task ValidateChanges() + public async Task ValidateChanges(CancellationToken cancellationToken = default) { try { - await new PathsValidator(this).RunValidation(false).ConfigureAwait(false); + await new PathsValidator(this).RunValidation(false, cancellationToken).ConfigureAwait(false); } catch (EngineValidationException ex) { diff --git a/src/ServiceControlInstaller.Engine/Instances/MonitoringNewInstance.cs b/src/ServiceControlInstaller.Engine/Instances/MonitoringNewInstance.cs index 3339de8823..6b629e8e9e 100644 --- a/src/ServiceControlInstaller.Engine/Instances/MonitoringNewInstance.cs +++ b/src/ServiceControlInstaller.Engine/Instances/MonitoringNewInstance.cs @@ -5,6 +5,7 @@ using System.IO; using System.Security.AccessControl; using System.Security.Principal; + using System.Threading; using System.Threading.Tasks; using Accounts; using Configuration.Monitoring; @@ -142,7 +143,7 @@ public void SetupInstance() } } - public async Task Validate(Func> promptToProceed) + public async Task Validate(Func> promptToProceed, CancellationToken cancellationToken = default) { if (TransportPackage.ZipName.Equals("MSMQ", StringComparison.OrdinalIgnoreCase)) { @@ -167,7 +168,7 @@ public async Task Validate(Func> promptToProceed) try { - ReportCard.CancelRequested = await new PathsValidator(this).RunValidation(true, promptToProceed).ConfigureAwait(false); + ReportCard.CancelRequested = await new PathsValidator(this).RunValidation(true, promptToProceed, cancellationToken).ConfigureAwait(false); } catch (EngineValidationException ex) { diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs index b0c5916ff1..e2983cb02e 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs @@ -7,6 +7,7 @@ namespace ServiceControlInstaller.Engine.Instances using System.Linq; using System.Security.AccessControl; using System.Security.Principal; + using System.Threading; using System.Threading.Tasks; using Accounts; using Configuration; @@ -300,7 +301,7 @@ protected virtual void ValidateConnectionString() { } - protected virtual Task ValidatePaths() => Task.CompletedTask; + protected virtual Task ValidatePaths(CancellationToken cancellationToken = default) => Task.CompletedTask; protected virtual void ValidateQueueNames() { @@ -360,9 +361,9 @@ public void SetupInstance() } } - public async Task ValidateChanges() + public async Task ValidateChanges(CancellationToken cancellationToken = default) { - await ValidatePaths().ConfigureAwait(false); + await ValidatePaths(cancellationToken).ConfigureAwait(false); ValidateQueueNames(); diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs index 49d7b73ca9..d66df319ee 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs @@ -5,6 +5,7 @@ using System.IO; using System.Security.AccessControl; using System.Security.Principal; + using System.Threading; using System.Threading.Tasks; using System.Xml.Serialization; using Accounts; @@ -219,7 +220,7 @@ public void Save(string path) } } - public async Task Validate(Func> promptToProceed) + public async Task Validate(Func> promptToProceed, CancellationToken cancellationToken = default) { RunValidation(ValidateTransport); RunValidation(ValidatePort); @@ -227,7 +228,7 @@ public async Task Validate(Func> promptToProceed) try { - ReportCard.CancelRequested = await ValidatePaths(promptToProceed).ConfigureAwait(false); + ReportCard.CancelRequested = await ValidatePaths(promptToProceed, cancellationToken).ConfigureAwait(false); } catch (EngineValidationException ex) { @@ -267,9 +268,9 @@ protected virtual void ValidateQueueNames() { } - protected virtual Task ValidatePaths(Func> promptToProceed) + protected virtual Task ValidatePaths(Func> promptToProceed, CancellationToken cancellationToken = default) { - return new PathsValidator(this).RunValidation(true, promptToProceed); + return new PathsValidator(this).RunValidation(true, promptToProceed, cancellationToken); } protected virtual void ValidateMaintenancePort() diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs index 9817315782..34328ebe5d 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs @@ -5,6 +5,7 @@ namespace ServiceControlInstaller.Engine.Instances using System.Configuration; using System.IO; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Configuration; using Configuration.ServiceControl; @@ -96,11 +97,11 @@ protected override void ValidateQueueNames() } } - protected override async Task ValidatePaths() + protected override async Task ValidatePaths(CancellationToken cancellationToken = default) { try { - await new PathsValidator(this).RunValidation(false).ConfigureAwait(false); + await new PathsValidator(this).RunValidation(false, cancellationToken).ConfigureAwait(false); } catch (EngineValidationException ex) { diff --git a/src/ServiceControlInstaller.Engine/Unattended/UnattendAuditInstaller.cs b/src/ServiceControlInstaller.Engine/Unattended/UnattendAuditInstaller.cs index eb80879cdb..8280fb29a8 100644 --- a/src/ServiceControlInstaller.Engine/Unattended/UnattendAuditInstaller.cs +++ b/src/ServiceControlInstaller.Engine/Unattended/UnattendAuditInstaller.cs @@ -2,6 +2,7 @@ { using System; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using FileSystem; using Instances; @@ -18,7 +19,7 @@ public UnattendAuditInstaller(ILogging loggingInstance) public PlatformZipInfo ZipInfo { get; } - public async Task Add(ServiceControlAuditNewInstance details, Func> promptToProceed) + public async Task Add(ServiceControlAuditNewInstance details, Func> promptToProceed, CancellationToken cancellationToken = default) { ZipInfo.ValidateZip(); @@ -27,7 +28,7 @@ public async Task Add(ServiceControlAuditNewInstance details, Func Add(MonitoringNewInstance details, Func> promptToProceed) + public async Task Add(MonitoringNewInstance details, Func> promptToProceed, CancellationToken cancellationToken = default) { ZipInfo.ValidateZip(); @@ -26,7 +27,7 @@ public async Task Add(MonitoringNewInstance details, Func Update(MonitoringInstance instance, bool startService) + internal async Task Update(MonitoringInstance instance, bool startService, CancellationToken cancellationToken = default) { instance.ReportCard = new ReportCard(); - await instance.ValidateChanges().ConfigureAwait(false); + await instance.ValidateChanges(cancellationToken).ConfigureAwait(false); if (instance.ReportCard.HasErrors) { foreach (var error in instance.ReportCard.Errors) diff --git a/src/ServiceControlInstaller.Engine/Unattended/UnattendServiceControlInstaller.cs b/src/ServiceControlInstaller.Engine/Unattended/UnattendServiceControlInstaller.cs index ff2fc16964..565e9cf783 100644 --- a/src/ServiceControlInstaller.Engine/Unattended/UnattendServiceControlInstaller.cs +++ b/src/ServiceControlInstaller.Engine/Unattended/UnattendServiceControlInstaller.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using Configuration.ServiceControl; using FileSystem; @@ -20,7 +21,7 @@ public UnattendServiceControlInstaller(ILogging loggingInstance) public PlatformZipInfo ZipInfo { get; } - public async Task Add(ServiceControlNewInstance details, Func> promptToProceed) + public async Task Add(ServiceControlNewInstance details, Func> promptToProceed, CancellationToken cancellationToken = default) { ZipInfo.ValidateZip(); @@ -32,7 +33,7 @@ public async Task Add(ServiceControlNewInstance details, Func Update(ServiceControlInstance instance, bool startService) + internal async Task Update(ServiceControlInstance instance, bool startService, CancellationToken cancellationToken = default) { instance.ReportCard = new ReportCard(); - await instance.ValidateChanges().ConfigureAwait(false); + await instance.ValidateChanges(cancellationToken).ConfigureAwait(false); if (instance.ReportCard.HasErrors) { return false; diff --git a/src/ServiceControlInstaller.Engine/Validation/AbstractCommandChecks.cs b/src/ServiceControlInstaller.Engine/Validation/AbstractCommandChecks.cs index 9f4e2771b4..4e9363e14a 100644 --- a/src/ServiceControlInstaller.Engine/Validation/AbstractCommandChecks.cs +++ b/src/ServiceControlInstaller.Engine/Validation/AbstractCommandChecks.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.ServiceProcess; + using System.Threading; using System.Threading.Tasks; using NuGet.Versioning; using ServiceControl.LicenseManagement; @@ -14,24 +15,24 @@ public abstract class AbstractCommandChecks { protected const string UpgradeGuide4to5Url = "https://docs.particular.net/servicecontrol/upgrades/4to5/"; - protected abstract Task PromptForRabbitMqCheck(bool isUpgrade); - protected abstract Task PromptToStopRunningInstance(BaseService instance); - protected abstract Task PromptToContinueWithForcedUpgrade(); - protected abstract Task NotifyForDeprecatedMessageTransport(TransportInfo transport); - protected abstract Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage); - protected abstract Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance); - protected abstract Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo); - protected abstract Task NotifyError(string title, string message); + protected abstract Task PromptForRabbitMqCheck(bool isUpgrade, CancellationToken cancellationToken = default); + protected abstract Task PromptToStopRunningInstance(BaseService instance, CancellationToken cancellationToken = default); + protected abstract Task PromptToContinueWithForcedUpgrade(CancellationToken cancellationToken = default); + protected abstract Task NotifyForDeprecatedMessageTransport(TransportInfo transport, CancellationToken cancellationToken = default); + protected abstract Task NotifyForMissingSystemPrerequisites(string missingPrereqsMessage, CancellationToken cancellationToken = default); + protected abstract Task NotifyForIncompatibleStorageEngine(IServiceControlBaseInstance baseInstance, CancellationToken cancellationToken = default); + protected abstract Task NotifyForIncompatibleUpgradeVersion(UpgradeInfo upgradeInfo, CancellationToken cancellationToken = default); + protected abstract Task NotifyError(string title, string message, CancellationToken cancellationToken = default); - public async Task CanAddInstance(bool allowExpiredLicense = false) + public async Task CanAddInstance(bool allowExpiredLicense = false, CancellationToken cancellationToken = default) { // Check for license - if (!allowExpiredLicense && !await IsLicenseOk().ConfigureAwait(false)) + if (!allowExpiredLicense && !await IsLicenseOk(cancellationToken).ConfigureAwait(false)) { return false; } - if (await OldVersionOfServiceControlInstalled().ConfigureAwait(false)) + if (await OldVersionOfServiceControlInstalled(cancellationToken).ConfigureAwait(false)) { return false; } @@ -39,7 +40,7 @@ public async Task CanAddInstance(bool allowExpiredLicense = false) return true; } - public async Task ValidateNewInstance(params IServiceInstance[] instances) + public async Task ValidateNewInstance(IServiceInstance[] instances, CancellationToken cancellationToken = default) { var transport = instances .OfType() @@ -47,16 +48,16 @@ public async Task ValidateNewInstance(params IServiceInstance[] instances) .Select(i => i.TransportPackage) .First(t => t is not null); - var continueInstall = await RabbitMqCheckIsOK(transport, Constants.CurrentVersion, false).ConfigureAwait(false); + var continueInstall = await RabbitMqCheckIsOK(transport, Constants.CurrentVersion, false, cancellationToken).ConfigureAwait(false); return continueInstall; } - public Task CanEditInstance(BaseService instance) => CanEditOrDelete(instance, isDelete: false); + public Task CanEditInstance(BaseService instance, CancellationToken cancellationToken = default) => CanEditOrDelete(instance, isDelete: false, cancellationToken); - public Task CanDeleteInstance(BaseService instance) => CanEditOrDelete(instance, isDelete: true); + public Task CanDeleteInstance(BaseService instance, CancellationToken cancellationToken = default) => CanEditOrDelete(instance, isDelete: true, cancellationToken); - async Task CanEditOrDelete(BaseService instance, bool isDelete) + async Task CanEditOrDelete(BaseService instance, bool isDelete, CancellationToken cancellationToken) { var instanceVersion = instance.Version; var instanceIsNewer = instanceVersion > Constants.CurrentVersion; @@ -68,21 +69,21 @@ async Task CanEditOrDelete(BaseService instance, bool isDelete) { var verb = isDelete ? "remove" : "edit"; var message = $"This instance version {instanceVersion} is newer than the installer version {Constants.CurrentVersion}. This installer can only {verb} instances with versions between {Constants.CurrentVersion.Major}.0.0 and {Constants.CurrentVersion}."; - await NotifyError(title, message).ConfigureAwait(false); + await NotifyError(title, message, cancellationToken).ConfigureAwait(false); return false; } if (installerOfDifferentMajor && !isDelete) { var message = $"This installer cannot edit instances created by a different major version. A {instanceVersion.Major}.* installer version greater or equal to {instanceVersion.Major}.{instanceVersion.Minor}.{instanceVersion.Patch} must be used instead."; - await NotifyError(title, message).ConfigureAwait(false); + await NotifyError(title, message, cancellationToken).ConfigureAwait(false); return false; } return true; } - async Task RabbitMqCheckIsOK(TransportInfo transport, SemanticVersion instanceVersion, bool isUpgrade) + async Task RabbitMqCheckIsOK(TransportInfo transport, SemanticVersion instanceVersion, bool isUpgrade, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(transport); @@ -99,18 +100,18 @@ async Task RabbitMqCheckIsOK(TransportInfo transport, SemanticVersion inst return true; } - return await PromptForRabbitMqCheck(isUpgrade).ConfigureAwait(false); + return await PromptForRabbitMqCheck(isUpgrade, cancellationToken).ConfigureAwait(false); } - public async Task CanUpgradeInstance(BaseService instance, bool forceUpgradeDb = false) + public async Task CanUpgradeInstance(BaseService instance, bool forceUpgradeDb = false, CancellationToken cancellationToken = default) { // Check for license - if (!await IsLicenseOk().ConfigureAwait(false)) + if (!await IsLicenseOk(cancellationToken).ConfigureAwait(false)) { return false; } - if (await OldVersionOfServiceControlInstalled().ConfigureAwait(false)) + if (await OldVersionOfServiceControlInstalled(cancellationToken).ConfigureAwait(false)) { return false; } @@ -119,7 +120,7 @@ public async Task CanUpgradeInstance(BaseService instance, bool forceUpgra var cantUpdateTransport = instance.TransportPackage.Removed && instance.TransportPackage.AutoMigrateTo is null; if (cantUpdateTransport) { - await NotifyForDeprecatedMessageTransport(instance.TransportPackage).ConfigureAwait(false); + await NotifyForDeprecatedMessageTransport(instance.TransportPackage, cancellationToken).ConfigureAwait(false); return false; } @@ -134,11 +135,11 @@ public async Task CanUpgradeInstance(BaseService instance, bool forceUpgra if (!forceUpgradeAllowed) { - await NotifyError("Cannot run the command", "Only ServiceControl 4.x primary instances that use RavenDB 3.5 persistence can be force-upgraded.").ConfigureAwait(false); + await NotifyError("Cannot run the command", "Only ServiceControl 4.x primary instances that use RavenDB 3.5 persistence can be force-upgraded.", cancellationToken).ConfigureAwait(false); return false; } - if (!await PromptToContinueWithForcedUpgrade().ConfigureAwait(false)) + if (!await PromptToContinueWithForcedUpgrade(cancellationToken).ConfigureAwait(false)) { return false; } @@ -149,7 +150,7 @@ public async Task CanUpgradeInstance(BaseService instance, bool forceUpgra if (!compatibleStorageEngine) { - await NotifyForIncompatibleStorageEngine(baseInstance).ConfigureAwait(false); + await NotifyForIncompatibleStorageEngine(baseInstance, cancellationToken).ConfigureAwait(false); return false; } } @@ -159,12 +160,12 @@ public async Task CanUpgradeInstance(BaseService instance, bool forceUpgra var upgradeInfo = UpgradeInfo.GetUpgradePathFor(instance.Version); if (upgradeInfo.HasIncompatibleVersion) { - await NotifyForIncompatibleUpgradeVersion(upgradeInfo).ConfigureAwait(false); + await NotifyForIncompatibleUpgradeVersion(upgradeInfo, cancellationToken).ConfigureAwait(false); return false; } } - if (!await RabbitMqCheckIsOK(instance.TransportPackage, instance.Version, isUpgrade: true).ConfigureAwait(false)) + if (!await RabbitMqCheckIsOK(instance.TransportPackage, instance.Version, isUpgrade: true, cancellationToken).ConfigureAwait(false)) { return false; } @@ -172,38 +173,38 @@ public async Task CanUpgradeInstance(BaseService instance, bool forceUpgra return true; } - async Task OldVersionOfServiceControlInstalled() + async Task OldVersionOfServiceControlInstalled(CancellationToken cancellationToken) { if (OldScmuCheck.OldVersionOfServiceControlInstalled(out var installedVersion)) { var message = $"An old version {installedVersion} of ServiceControl Management is installed, which will not work after installing new instances. Before installing ServiceControl 5 instances, you must either uninstall the {installedVersion} instance or update it to a 4.x version at least {OldScmuCheck.MinimumScmuVersion}."; - await NotifyError("Outdated Version Installed", message).ConfigureAwait(false); + await NotifyError("Outdated Version Installed", message, cancellationToken).ConfigureAwait(false); return true; } return false; } - async Task IsLicenseOk() + async Task IsLicenseOk(CancellationToken cancellationToken) { var licenseCheckResult = CheckLicenseIsValid(); if (!licenseCheckResult.Valid) { - await NotifyError("License Error", $"Upgrade could not continue due to an issue with the current license. {licenseCheckResult.Message}. Contact contact@particular.net").ConfigureAwait(false); + await NotifyError("License Error", $"Upgrade could not continue due to an issue with the current license. {licenseCheckResult.Message}. Contact contact@particular.net", cancellationToken).ConfigureAwait(false); return false; } return true; } - public async Task StopBecauseInstanceIsRunning(BaseService instance) + public async Task StopBecauseInstanceIsRunning(BaseService instance, CancellationToken cancellationToken = default) { if (instance.Service.Status == ServiceControllerStatus.Stopped) { return false; } - var proceed = await PromptToStopRunningInstance(instance).ConfigureAwait(false); + var proceed = await PromptToStopRunningInstance(instance, cancellationToken).ConfigureAwait(false); return !proceed; } diff --git a/src/ServiceControlInstaller.Engine/Validation/PathsValidator.cs b/src/ServiceControlInstaller.Engine/Validation/PathsValidator.cs index b24eec3886..edfa7a5c5f 100644 --- a/src/ServiceControlInstaller.Engine/Validation/PathsValidator.cs +++ b/src/ServiceControlInstaller.Engine/Validation/PathsValidator.cs @@ -4,6 +4,7 @@ namespace ServiceControlInstaller.Engine.Validation using System.Collections.Generic; using System.IO; using System.Linq; + using System.Threading; using System.Threading.Tasks; class PathsValidator @@ -52,12 +53,12 @@ public PathsValidator(IServiceControlPaths instance) paths = pathList.Where(p => !string.IsNullOrWhiteSpace(p.Path)).ToList(); } - public Task RunValidation(bool includeNewInstanceChecks) + public Task RunValidation(bool includeNewInstanceChecks, CancellationToken cancellationToken = default) { - return RunValidation(includeNewInstanceChecks, info => Task.FromResult(false)); + return RunValidation(includeNewInstanceChecks, (info, token) => Task.FromResult(false), cancellationToken); } - public async Task RunValidation(bool includeNewInstanceChecks, Func> promptToProceed) + public async Task RunValidation(bool includeNewInstanceChecks, Func> promptToProceed, CancellationToken cancellationToken = default) { try { @@ -69,7 +70,7 @@ public async Task RunValidation(bool includeNewInstanceChecks, Func RunValidation(bool includeNewInstanceChecks, Func CheckPathsAreEmpty(Func> promptToProceed) + async Task CheckPathsAreEmpty(Func> promptToProceed, CancellationToken cancellationToken) { foreach (var pathInfo in paths) { @@ -104,7 +109,7 @@ async Task CheckPathsAreEmpty(Func> promptToProceed) if (directory.EnumerateFileSystemInfos().Any()) { - var shouldProceed = await promptToProceed(pathInfo).ConfigureAwait(false); + var shouldProceed = await promptToProceed(pathInfo, cancellationToken).ConfigureAwait(false); if (!shouldProceed) { return true; From 1840e2c592ef4c06467f55c0e26b60a7f6d365f1 Mon Sep 17 00:00:00 2001 From: John Simons Date: Thu, 13 Aug 2026 09:57:55 +1000 Subject: [PATCH 07/12] Refactor body storage retrieval to use explicit result states Update IBodyStorage and its implementations to return a result object that distinguishes between not found, empty, and unavailable bodies. This allows the API and retry logic to handle these scenarios more accurately and provide better feedback. --- ...eControl.AcceptanceTests.PostgreSql.csproj | 3 +- ...ceControl.AcceptanceTests.SqlServer.csproj | 3 +- .../When_a_retry_fails_to_be_sent.cs | 8 +-- .../Implementation/BodyStorage/BodyStorage.cs | 51 +++++++++------- .../FailedMessageRetryDataStore.cs | 19 ++++-- .../ErrorMessagesDataStore.cs | 42 +++++++------ .../RavenAttachmentsBodyStorage.cs | 33 ++++++---- .../AttachmentsBodyStorageTests.cs | 11 ++-- .../EFCore/BodyReadTests.cs | 26 ++++---- .../FailedMessageRetryBodyDataStoreTests.cs | 4 +- .../ReturnToSenderDequeuerTests.cs | 60 ++++++++++++++++--- .../RetryStateTests.cs | 18 +++--- .../IBodyStorage.cs | 45 +++++++++++--- .../BodyStorage/MessageBodyResultTests.cs | 40 +++++++++++++ .../Messages/GetMessagesController.cs | 8 +-- .../Retrying/Infrastructure/ReturnToSender.cs | 32 ++++++---- 16 files changed, 281 insertions(+), 122 deletions(-) create mode 100644 src/ServiceControl.UnitTests/BodyStorage/MessageBodyResultTests.cs diff --git a/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj b/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj index d5ac47492d..74ed777f0b 100644 --- a/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj +++ b/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj @@ -52,8 +52,7 @@ - - + diff --git a/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj b/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj index cecd9a36e8..5dda932f0e 100644 --- a/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj +++ b/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj @@ -52,8 +52,7 @@ - - + diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs index e367de15cc..bbeea18019 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs @@ -17,7 +17,7 @@ using NUnit.Framework; using ServiceControl.MessageFailures; using ServiceControl.MessageFailures.Api; - using ServiceControl.Persistence; + using ServiceControl.Operations.BodyStorage; using ServiceControl.Recoverability; using TestSupport; @@ -31,7 +31,7 @@ public async Task SubsequentBatchesShouldBeProcessed(CancellationToken cancellat CustomizeHostBuilder = hostBuilder => { - hostBuilder.Services.AddSingleton(provider => new FakeReturnToSender(provider.GetRequiredService(), provider.GetRequiredService())); + hostBuilder.Services.AddSingleton(provider => new FakeReturnToSender(provider.GetRequiredService(), provider.GetRequiredService())); }; await Define() @@ -148,8 +148,8 @@ public class MyContext : ScenarioContext public class MessageThatWillFail : ICommand; - public class FakeReturnToSender(IFailedMessageRetryDataStore errorMessageStore, MyContext myContext) - : ReturnToSender(errorMessageStore, NullLogger.Instance) + public class FakeReturnToSender(IBodyStorage bodyStorage, MyContext myContext) + : ReturnToSender(bodyStorage, NullLogger.Instance) { public override Task HandleMessage(MessageContext message, IMessageDispatcher sender, string errorQueueTransportAddress, CancellationToken cancellationToken = default) { diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/BodyStorage/BodyStorage.cs b/src/ServiceControl.Persistence.EFCore/Implementation/BodyStorage/BodyStorage.cs index 233771e24e..cdfdda6b4e 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/BodyStorage/BodyStorage.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/BodyStorage/BodyStorage.cs @@ -19,13 +19,13 @@ namespace ServiceControl.Persistence.EFCore.Implementation.BodyStorage; /// public class BodyStorage(IServiceScopeFactory scopeFactory, IBodyStoragePersistence storagePersistence) : DataStoreBase(scopeFactory), IBodyStorage { - public async Task TryFetch(string bodyId, CancellationToken cancellationToken = default) + public async Task TryFetch(string bodyId, CancellationToken cancellationToken = default) { var row = await ExecuteWithDbContext((dbContext, token) => ResolveBody(dbContext, bodyId, token), cancellationToken); if (row == null) { - return null; // No such message: the API turns this into a 404. + return MessageBodyResult.NotFound(); } // Bodies are immutable per message, so the id is a stable ETag. @@ -35,33 +35,42 @@ public class BodyStorage(IServiceScopeFactory scopeFactory, IBodyStoragePersiste { var external = await storagePersistence.ReadBody(uniqueMessageId, cancellationToken); - return external == null - ? new MessageBodyStreamResult { HasResult = false } - : new MessageBodyStreamResult - { - HasResult = true, - Stream = external.Stream, - ContentType = external.ContentType, - BodySize = external.BodySize, - Etag = uniqueMessageId - }; + if (external == null) + { + return MessageBodyResult.Unavailable(); + } + + if (external.BodySize == 0) + { + await external.Stream.DisposeAsync(); + return MessageBodyResult.Empty(); + } + + return MessageBodyResult.Available(new MessageBodyStreamContent(external.Stream, external.ContentType, external.BodySize, uniqueMessageId)); } if (row.BodyText != null) { var bytes = Encoding.UTF8.GetBytes(row.BodyText); - return new MessageBodyStreamResult + if (bytes.Length == 0) { - HasResult = true, - Stream = new MemoryStream(bytes, writable: false), - ContentType = row.BodyContentType, - BodySize = bytes.Length, - Etag = uniqueMessageId - }; + return MessageBodyResult.Empty(); + } + + return MessageBodyResult.Available(new MessageBodyStreamContent( + new MemoryStream(bytes, writable: false), + row.BodyContentType, + bytes.Length, + uniqueMessageId)); + } + + if (row.BodySize == 0) + { + return MessageBodyResult.Empty(); } - return new MessageBodyStreamResult { HasResult = false }; // Message exists but carries no body. + return MessageBodyResult.Unavailable(); } static async Task ResolveBody(ServiceControlDbContext dbContext, string bodyId, CancellationToken cancellationToken) @@ -88,6 +97,7 @@ public class BodyStorage(IServiceScopeFactory scopeFactory, IBodyStoragePersiste UniqueMessageId = message.UniqueMessageId, BodyText = message.BodyText, BodyStoredExternally = message.BodyStoredExternally, + BodySize = message.BodySize, BodyContentType = message.BodyContentType }) .FirstOrDefaultAsync(cancellationToken); @@ -97,6 +107,7 @@ sealed class BodyRow public Guid UniqueMessageId { get; init; } public string? BodyText { get; init; } public bool BodyStoredExternally { get; init; } + public int BodySize { get; init; } public string? BodyContentType { get; init; } } } diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/FailedMessageRetryDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/FailedMessageRetryDataStore.cs index a2850f9ddd..4bef78cb47 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/FailedMessageRetryDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/FailedMessageRetryDataStore.cs @@ -55,18 +55,27 @@ public Task ProcessPendingRetries(DateTime periodFrom, DateTime periodTo, string public async Task GetFailedMessageBody(string uniqueMessageId, CancellationToken cancellationToken = default) { - var result = await bodyStorage.TryFetch(uniqueMessageId, cancellationToken) - ?? throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + var result = await bodyStorage.TryFetch(uniqueMessageId, cancellationToken); - if (!result.HasResult) + if (result.State == MessageBodyState.NotFound) + { + throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + } + + if (result.State == MessageBodyState.Unavailable) { throw new InvalidOperationException("IBodyStorage.TryFetch did not return a body"); } - await using (result.Stream) + if (result.State == MessageBodyState.Empty) + { + return []; + } + + await using (result.Content.Stream) { using var memoryStream = new MemoryStream(); - await result.Stream.CopyToAsync(memoryStream, cancellationToken); + await result.Content.Stream.CopyToAsync(memoryStream, cancellationToken); return memoryStream.ToArray(); } } diff --git a/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs b/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs index 2e5bbce988..fec09b8f8f 100644 --- a/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs @@ -510,26 +510,34 @@ record struct FailedMessageProjection(string UniqueMessageId); public async Task GetFailedMessageBody(string uniqueMessageId, CancellationToken cancellationToken = default) { - byte[] body = null; - var result = await bodyStorage.TryFetch(uniqueMessageId, cancellationToken) - ?? throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + var result = await bodyStorage.TryFetch(uniqueMessageId, cancellationToken); - if (result.HasResult) + if (result.State == MessageBodyState.NotFound) { - await using (result.Stream) // Not strictly required for MemoryStream but might be different behavior in future .NET versions - { - // Unfortunately we can't use the buffer manager here yet because core doesn't allow to set the length property so usage of GetBuffer is not possible - // furthermore call ToArray would neglect many of the benefits of the recyclable stream - // RavenDB always returns a memory stream in ver. 3.5 so there is no need to pretend we need to do buffered reads since the memory is anyway fully allocated already - // this assumption might change when we stop supporting RavenDB 3.5 but right now this is the most memory efficient way to do things - // https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream#getbuffer-and-toarray - using var memoryStream = new MemoryStream(); - await result.Stream.CopyToAsync(memoryStream, cancellationToken); - - body = memoryStream.ToArray(); - } + throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + } + + if (result.State == MessageBodyState.Unavailable) + { + throw new InvalidOperationException("IBodyStorage.TryFetch result cannot be null"); + } + + if (result.State == MessageBodyState.Empty) + { + return []; + } + + await using (result.Content.Stream) // Not strictly required for MemoryStream but might be different behavior in future .NET versions + { + // Unfortunately we can't use the buffer manager here yet because core doesn't allow to set the length property so usage of GetBuffer is not possible + // furthermore call ToArray would neglect many of the benefits of the recyclable stream + // RavenDB always returns a memory stream in ver. 3.5 so there is no need to pretend we need to do buffered reads since the memory is anyway fully allocated already + // this assumption might change when we stop supporting RavenDB 3.5 but right now this is the most memory efficient way to do things + // https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream#getbuffer-and-toarray + using var memoryStream = new MemoryStream(); + await result.Content.Stream.CopyToAsync(memoryStream, cancellationToken); + return memoryStream.ToArray(); } - return body; } } } diff --git a/src/ServiceControl.Persistence.RavenDB/RavenAttachmentsBodyStorage.cs b/src/ServiceControl.Persistence.RavenDB/RavenAttachmentsBodyStorage.cs index 64cc215ebc..69a498fac0 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenAttachmentsBodyStorage.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenAttachmentsBodyStorage.cs @@ -14,7 +14,7 @@ class RavenAttachmentsBodyStorage(IRavenSessionProvider sessionProvider) : IBody { public const string AttachmentName = "body"; - public async Task TryFetch(string bodyId, CancellationToken cancellationToken = default) + public async Task TryFetch(string bodyId, CancellationToken cancellationToken = default) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); @@ -23,7 +23,7 @@ public async Task TryFetch(string bodyId, CancellationT if (Guid.TryParse(bodyId, out _)) { var result = await ResultForUniqueId(session, bodyId, cancellationToken); - if (result != null) + if (result.State != MessageBodyState.NotFound) { return result; } @@ -42,28 +42,37 @@ public async Task TryFetch(string bodyId, CancellationT return await ResultForUniqueId(session, uniqueId, cancellationToken); } - return null; + return MessageBodyResult.NotFound(); } - async Task ResultForUniqueId(IAsyncDocumentSession session, string uniqueId, CancellationToken cancellationToken) + async Task ResultForUniqueId(IAsyncDocumentSession session, string uniqueId, CancellationToken cancellationToken) { var documentId = FailedMessageIdGenerator.MakeDocumentId(uniqueId); + var failedMessage = await session.LoadAsync(documentId, cancellationToken); + + if (failedMessage == null) + { + return MessageBodyResult.NotFound(); + } var result = await session.Advanced.Attachments.GetAsync(documentId, AttachmentName, cancellationToken); if (result == null) { - return null; + return MessageBodyResult.Unavailable(); } - return new MessageBodyStreamResult + if (result.Details.Size == 0) { - HasResult = true, - Stream = result.Stream, - ContentType = result.Details.ContentType, - BodySize = (int)result.Details.Size, - Etag = result.Details.ChangeVector - }; + await result.Stream.DisposeAsync(); + return MessageBodyResult.Empty(); + } + + return MessageBodyResult.Available(new MessageBodyStreamContent( + result.Stream, + result.Details.ContentType, + (int)result.Details.Size, + result.Details.ChangeVector)); } } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/BodyStorage/AttachmentsBodyStorageTests.cs b/src/ServiceControl.Persistence.Tests/BodyStorage/AttachmentsBodyStorageTests.cs index 8fd23948d9..d4053ac891 100644 --- a/src/ServiceControl.Persistence.Tests/BodyStorage/AttachmentsBodyStorageTests.cs +++ b/src/ServiceControl.Persistence.Tests/BodyStorage/AttachmentsBodyStorageTests.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using ServiceControl.MessageFailures; using ServiceControl.Operations; + using ServiceControl.Operations.BodyStorage; using ServiceControl.Persistence.UnitOfWork; [TestFixture] @@ -81,14 +82,14 @@ async Task RunTest(Func, string> getIdToQuery) Assert.That(retrieved, Is.Not.Null); using (Assert.EnterMultipleScope()) { - Assert.That(retrieved.HasResult, Is.True); - Assert.That(retrieved.ContentType, Is.EqualTo(contentType)); + Assert.That(retrieved.State, Is.EqualTo(MessageBodyState.Available)); + Assert.That(retrieved.Content.ContentType, Is.EqualTo(contentType)); } - var buffer = new byte[retrieved.BodySize]; - await using (retrieved.Stream) + var buffer = new byte[retrieved.Content.BodySize]; + await using (retrieved.Content.Stream) { - retrieved.Stream.ReadExactly(buffer); + retrieved.Content.Stream.ReadExactly(buffer); } Assert.That(buffer, Is.EqualTo(body)); diff --git a/src/ServiceControl.Persistence.Tests/EFCore/BodyReadTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/BodyReadTests.cs index 3223dc8323..7a2996c262 100644 --- a/src/ServiceControl.Persistence.Tests/EFCore/BodyReadTests.cs +++ b/src/ServiceControl.Persistence.Tests/EFCore/BodyReadTests.cs @@ -26,9 +26,9 @@ public async Task Fetches_an_inline_text_body() Assert.That(result, Is.Not.Null); using (Assert.EnterMultipleScope()) { - Assert.That(result.HasResult, Is.True); - Assert.That(result.ContentType, Is.EqualTo("text/xml")); - Assert.That(Encoding.UTF8.GetString(ReadAll(result.Stream)), Is.EqualTo("1")); + Assert.That(result.State, Is.EqualTo(MessageBodyState.Available)); + Assert.That(result.Content.ContentType, Is.EqualTo("text/xml")); + Assert.That(Encoding.UTF8.GetString(ReadAll(result.Content.Stream)), Is.EqualTo("1")); } } @@ -44,9 +44,9 @@ public async Task Fetches_an_external_binary_body() Assert.That(result, Is.Not.Null); using (Assert.EnterMultipleScope()) { - Assert.That(result.HasResult, Is.True); - Assert.That(result.ContentType, Is.EqualTo("application/octet-stream")); - Assert.That(ReadAll(result.Stream), Is.EqualTo(body)); + Assert.That(result.State, Is.EqualTo(MessageBodyState.Available)); + Assert.That(result.Content.ContentType, Is.EqualTo("application/octet-stream")); + Assert.That(ReadAll(result.Content.Stream), Is.EqualTo(body)); } } @@ -62,8 +62,8 @@ public async Task Fetches_the_whole_body_for_large_text_not_the_inline_prefix() Assert.That(result, Is.Not.Null); using (Assert.EnterMultipleScope()) { - Assert.That(result.HasResult, Is.True); - Assert.That(ReadAll(result.Stream), Is.EqualTo(body), "external storage is authoritative, not the inline search prefix"); + Assert.That(result.State, Is.EqualTo(MessageBodyState.Available)); + Assert.That(ReadAll(result.Content.Stream), Is.EqualTo(body), "external storage is authoritative, not the inline search prefix"); } } @@ -76,11 +76,11 @@ public async Task Fetches_by_message_id() var result = await Fetch(failure.MessageId); Assert.That(result, Is.Not.Null); - Assert.That(result.HasResult, Is.True); + Assert.That(result.State, Is.EqualTo(MessageBodyState.Available)); } [Test] - public async Task Reports_no_body_for_an_empty_body() + public async Task Reports_an_empty_body() { var failure = new IngestedFailure { Body = [] }; await Ingest(failure); @@ -88,7 +88,7 @@ public async Task Reports_no_body_for_an_empty_body() var result = await Fetch(failure.UniqueMessageIdString); Assert.That(result, Is.Not.Null); - Assert.That(result.HasResult, Is.False); + Assert.That(result.State, Is.EqualTo(MessageBodyState.Empty)); } [Test] @@ -96,10 +96,10 @@ public async Task Returns_null_for_an_unknown_message() { var result = await Fetch(Guid.NewGuid().ToString()); - Assert.That(result, Is.Null); + Assert.That(result.State, Is.EqualTo(MessageBodyState.NotFound)); } - async Task Fetch(string bodyId) + async Task Fetch(string bodyId) { using var scope = ServiceProvider.CreateScope(); var bodyStorage = scope.ServiceProvider.GetRequiredService(); diff --git a/src/ServiceControl.Persistence.Tests/EFCore/FailedMessageRetryBodyDataStoreTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/FailedMessageRetryBodyDataStoreTests.cs index 0a46b887cc..d964caedec 100644 --- a/src/ServiceControl.Persistence.Tests/EFCore/FailedMessageRetryBodyDataStoreTests.cs +++ b/src/ServiceControl.Persistence.Tests/EFCore/FailedMessageRetryBodyDataStoreTests.cs @@ -24,7 +24,7 @@ public async Task GetFailedMessageBody_returns_inline_BodyText_bytes() [Test] public async Task GetFailedMessageBody_throws_when_the_body_is_unavailable() { - var id = await SeedFailedMessage(); + var id = await SeedFailedMessage(bodyStoredExternally: true); Assert.ThrowsAsync(() => FailedMessageRetryStore.GetFailedMessageBody(id.ToString())); @@ -33,8 +33,8 @@ public async Task GetFailedMessageBody_throws_when_the_body_is_unavailable() [Test] public async Task GetFailedMessageBody_returns_external_storage_body_when_BodyStoredExternally() { - var id = await SeedFailedMessage(bodyStoredExternally: true); var expected = Encoding.UTF8.GetBytes("external body payload"); + var id = await SeedFailedMessage(bodyStoredExternally: true); await RecordedBodies.WriteBody(id.ToString(), expected, "text/plain"); diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs index 7adc218149..bc366dfcb4 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -18,6 +19,7 @@ using Persistence.Infrastructure; using ServiceControl.CompositeViews.Messages; using ServiceControl.Operations; + using ServiceControl.Operations.BodyStorage; using ServiceControl.Recoverability; [TestFixture] @@ -67,11 +69,47 @@ public async Task It_fetches_the_body_from_storage_if_provided() }; var message = CreateMessage(Guid.NewGuid().ToString(), headers); - await new ReturnToSender(new FakeErrorMessageDataStore(), NullLogger.Instance).HandleMessage(message, sender, "error"); + await new ReturnToSender(new FakeBodyStorage(), NullLogger.Instance).HandleMessage(message, sender, "error"); Assert.That(Encoding.UTF8.GetString(sender.Message.Body.ToArray()), Is.EqualTo("MessageBodyId")); } + [Test] + public async Task It_sends_an_empty_body_when_storage_reports_empty() + { + var sender = new FakeSender(); + var headers = new Dictionary + { + ["ServiceControl.TargetEndpointAddress"] = "TargetEndpoint", + ["ServiceControl.Retry.Attempt.MessageId"] = "MessageBodyId", + ["ServiceControl.Retry.UniqueMessageId"] = "MessageBodyId" + }; + var message = CreateMessage(Guid.NewGuid().ToString(), headers); + + await new ReturnToSender(new FakeBodyStorage(MessageBodyState.Empty), NullLogger.Instance).HandleMessage(message, sender, "error"); + + Assert.That(sender.Message.Body.ToArray(), Is.Empty); + } + + [TestCase(MessageBodyState.NotFound)] + [TestCase(MessageBodyState.Unavailable)] + public void It_does_not_send_when_the_body_cannot_be_retrieved(MessageBodyState state) + { + var sender = new FakeSender(); + var headers = new Dictionary + { + ["ServiceControl.TargetEndpointAddress"] = "TargetEndpoint", + ["ServiceControl.Retry.Attempt.MessageId"] = "MessageBodyId", + ["ServiceControl.Retry.UniqueMessageId"] = "MessageBodyId" + }; + var message = CreateMessage(Guid.NewGuid().ToString(), headers); + + Assert.ThrowsAsync(() => + new ReturnToSender(new FakeBodyStorage(state), NullLogger.Instance).HandleMessage(message, sender, "error")); + + Assert.That(sender.Message, Is.Null); + } + [Test] public async Task It_uses_retry_to_if_provided() { @@ -167,15 +205,19 @@ public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction } } - class FakeErrorMessageDataStore : IFailedMessageRetryDataStore + class FakeBodyStorage(MessageBodyState state = MessageBodyState.Available) : IBodyStorage { - public Task ProcessPendingRetries(DateTime periodFrom, DateTime periodTo, string queueAddress, Func processCallback, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public Task GetRetryPendingMessages(DateTime from, DateTime to, string queueAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public Task RemoveFailedMessageRetry(string uniqueMessageId, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public Task GetFailedMessageBody(string bodyId, CancellationToken cancellationToken = default) => Task.FromResult(Encoding.UTF8.GetBytes(bodyId)); + public Task TryFetch(string bodyId, CancellationToken cancellationToken = default) => + Task.FromResult(state switch + { + MessageBodyState.NotFound => MessageBodyResult.NotFound(), + MessageBodyState.Empty => MessageBodyResult.Empty(), + MessageBodyState.Unavailable => MessageBodyResult.Unavailable(), + MessageBodyState.Available => MessageBodyResult.Available(Content(Encoding.UTF8.GetBytes(bodyId))), + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }); + + static MessageBodyStreamContent Content(byte[] body) => new(new MemoryStream(body), "text/plain", body.Length, "etag"); } } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs index 34b3cabb1d..5cdac0108c 100644 --- a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -71,7 +71,7 @@ public async Task When_the_dequeuer_is_created_then_the_error_address_is_cached( var transportCustomization = new TestTransportCustomization { TransportInfrastructure = transportInfrastructure }; - var testReturnToSenderDequeuer = new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", + var testReturnToSenderDequeuer = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", errorQueueNameCache, transportCustomization); await testReturnToSenderDequeuer.StartAsync(new CancellationToken()); @@ -93,7 +93,7 @@ public async Task When_a_group_is_prepared_with_three_batches_and_SC_is_restarte MessageRedirectsDataStore, domainEvents, new TestReturnToSenderDequeuer( - new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), + new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", @@ -121,7 +121,7 @@ public async Task When_a_group_is_prepared_with_three_batches_and_SC_is_restarte MessageRedirectsDataStore, domainEvents, new TestReturnToSenderDequeuer( - new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), + new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", @@ -148,7 +148,7 @@ public async Task When_a_group_is_forwarded_the_status_is_Completed() var sender = new TestSender(); - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); var processor = new RetryProcessor(RetryStagingStore, MessageRedirectsDataStore, domainEvents, returnToSender, retryManager, new Lazy(() => sender), new RecordingMessageActionAuditLog(), NullLogger.Instance); await processor.ProcessBatches(); // mark ready @@ -213,7 +213,7 @@ public async Task When_there_is_one_poison_message_it_is_removed_from_batch_and_ } }; - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); var processor = new RetryProcessor(RetryStagingStore, MessageRedirectsDataStore, domainEvents, returnToSender, retryManager, new Lazy(() => sender), new RecordingMessageActionAuditLog(), NullLogger.Instance); bool c; @@ -250,7 +250,7 @@ public async Task When_a_group_has_one_batch_out_of_two_forwarded_the_status_is_ await CreateAFailedMessageAndMarkAsPartOfRetryBatch(retryManager, "Test-group", true, 1001); - var returnToSender = new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance); + var returnToSender = new ReturnToSender(BodyStorage, NullLogger.Instance); var sender = new TestSender(); @@ -300,7 +300,7 @@ public async Task When_a_selection_is_staged_each_message_is_audited_as_a_batch( var audit = new RecordingMessageActionAuditLog(); var sender = new TestSender(); - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); var processor = new RetryProcessor(RetryStagingStore, MessageRedirectsDataStore, domainEvents, returnToSender, retryManager, new Lazy(() => sender), audit, NullLogger.Instance); await processor.ProcessBatches(); // stage @@ -327,7 +327,7 @@ public async Task When_a_group_is_staged_each_message_is_audited_with_the_initia var audit = new RecordingMessageActionAuditLog(); var sender = new TestSender(); - var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); + var returnToSender = new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()); var processor = new RetryProcessor(RetryStagingStore, MessageRedirectsDataStore, domainEvents, returnToSender, retryManager, new Lazy(() => sender), audit, NullLogger.Instance); await processor.ProcessBatches(); // stage (emits per-message audit) @@ -362,7 +362,7 @@ RetryProcessor CreateProcessor(IDomainEvents domainEvents, TestSender sender) => new(RetryStagingStore, MessageRedirectsDataStore, domainEvents, - new TestReturnToSenderDequeuer(new ReturnToSender(FailedMessageRetryStore, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()), + new TestReturnToSenderDequeuer(new ReturnToSender(BodyStorage, NullLogger.Instance), FailedMessageLifecycleStore, domainEvents, "TestEndpoint", new ErrorQueueNameCache(), new TestTransportCustomization()), new RetryingManager(domainEvents, NullLogger.Instance), new Lazy(() => sender), new RecordingMessageActionAuditLog(), diff --git a/src/ServiceControl.Persistence/IBodyStorage.cs b/src/ServiceControl.Persistence/IBodyStorage.cs index 4becd1ea2e..483e347206 100644 --- a/src/ServiceControl.Persistence/IBodyStorage.cs +++ b/src/ServiceControl.Persistence/IBodyStorage.cs @@ -1,20 +1,51 @@ namespace ServiceControl.Operations.BodyStorage { + using System; using System.IO; using System.Threading; using System.Threading.Tasks; public interface IBodyStorage { - Task TryFetch(string bodyId, CancellationToken cancellationToken = default); + Task TryFetch(string bodyId, CancellationToken cancellationToken = default); } - public class MessageBodyStreamResult + public enum MessageBodyState { - public bool HasResult; - public Stream Stream; // Intentional, other streams could require a context - public string ContentType; - public int BodySize; - public string Etag; + NotFound, + Empty, + Unavailable, + Available } + + public sealed class MessageBodyResult + { + MessageBodyResult(MessageBodyState state, MessageBodyStreamContent content = null) + { + State = state; + ContentValue = content; + } + + public MessageBodyState State { get; } + + public MessageBodyStreamContent Content => State == MessageBodyState.Available + ? ContentValue + : throw new InvalidOperationException($"Body content is not available when the state is {State}."); + + public static MessageBodyResult NotFound() => new(MessageBodyState.NotFound); + + public static MessageBodyResult Empty() => new(MessageBodyState.Empty); + + public static MessageBodyResult Unavailable() => new(MessageBodyState.Unavailable); + + public static MessageBodyResult Available(MessageBodyStreamContent content) + { + ArgumentNullException.ThrowIfNull(content); + return new MessageBodyResult(MessageBodyState.Available, content); + } + + MessageBodyStreamContent ContentValue { get; } + } + + public sealed record MessageBodyStreamContent(Stream Stream, string ContentType, int BodySize, string Etag); } \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/BodyStorage/MessageBodyResultTests.cs b/src/ServiceControl.UnitTests/BodyStorage/MessageBodyResultTests.cs new file mode 100644 index 0000000000..5c2b04f97a --- /dev/null +++ b/src/ServiceControl.UnitTests/BodyStorage/MessageBodyResultTests.cs @@ -0,0 +1,40 @@ +namespace ServiceControl.UnitTests.BodyStorage; + +using System; +using System.IO; +using NUnit.Framework; +using ServiceControl.Operations.BodyStorage; + +[TestFixture] +public class MessageBodyResultTests +{ + [TestCase(MessageBodyState.NotFound)] + [TestCase(MessageBodyState.Empty)] + [TestCase(MessageBodyState.Unavailable)] + public void Body_is_not_accessible_without_content(MessageBodyState state) + { + var result = state switch + { + MessageBodyState.NotFound => MessageBodyResult.NotFound(), + MessageBodyState.Empty => MessageBodyResult.Empty(), + MessageBodyState.Unavailable => MessageBodyResult.Unavailable(), + MessageBodyState.Available => throw new ArgumentOutOfRangeException(nameof(state), state, null), + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + + Assert.Throws(() => _ = result.Content); + } + + [Test] + public void Body_is_accessible_when_available() + { + var content = new MessageBodyStreamContent(Stream.Null, "text/plain", 1, "etag"); + var result = MessageBodyResult.Available(content); + + Assert.That(result.Content, Is.SameAs(content)); + } + + [Test] + public void Available_rejects_null() => + Assert.Throws(() => MessageBodyResult.Available(null)); +} \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs index a7d22eb027..e4de1674d7 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs @@ -90,18 +90,18 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") { var result = await bodyStorage.TryFetch(id, cancellationToken); - if (result == null) + if (result.State == MessageBodyState.NotFound) { return NotFound(); } - if (!result.HasResult) + if (result.State is MessageBodyState.Empty or MessageBodyState.Unavailable) { return NoContent(); } - Response.WithEtag(result.Etag); - return File(result.Stream, result.ContentType ?? "text/*"); + Response.WithEtag(result.Content.Etag); + return File(result.Content.Stream, result.Content.ContentType ?? "text/*"); } var remote = settings.RemoteInstances.SingleOrDefault(r => r.InstanceId == instanceId); diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs index 38737f4de1..e1cbd44e36 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs @@ -2,14 +2,15 @@ namespace ServiceControl.Recoverability { using System; using System.Collections.Generic; + using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NServiceBus.Routing; using NServiceBus.Transport; - using ServiceControl.Persistence; + using Operations.BodyStorage; - class ReturnToSender(IFailedMessageRetryDataStore errorMessageStore, ILogger logger) + class ReturnToSender(IBodyStorage bodyStorage, ILogger logger) { public virtual async Task HandleMessage(MessageContext message, IMessageDispatcher sender, string errorQueueTransportAddress, CancellationToken cancellationToken = default) { @@ -23,9 +24,9 @@ public virtual async Task HandleMessage(MessageContext message, IMessageDispatch logger.LogDebug("{MessageId}: Retrieving message body", messageId); - if (outgoingHeaders.TryGetValue("ServiceControl.Retry.Attempt.MessageId", out var attemptMessageId)) + if (outgoingHeaders.ContainsKey("ServiceControl.Retry.Attempt.MessageId")) { - body = await FetchFromFailedMessage(outgoingHeaders, messageId, attemptMessageId, cancellationToken); + body = await FetchFromFailedMessage(outgoingHeaders, messageId, cancellationToken); outgoingHeaders.Remove("ServiceControl.Retry.Attempt.MessageId"); } else @@ -56,21 +57,30 @@ public virtual async Task HandleMessage(MessageContext message, IMessageDispatch logger.LogDebug("{MessageId}: Forwarded message to {RetryTo}", messageId, retryTo); } - async Task FetchFromFailedMessage(Dictionary outgoingHeaders, string messageId, string attemptMessageId, CancellationToken cancellationToken) + async Task FetchFromFailedMessage(Dictionary outgoingHeaders, string messageId, CancellationToken cancellationToken) { var uniqueMessageId = outgoingHeaders["ServiceControl.Retry.UniqueMessageId"]; - byte[] body = await errorMessageStore.GetFailedMessageBody(uniqueMessageId, cancellationToken); + var result = await bodyStorage.TryFetch(uniqueMessageId, cancellationToken); - if (body == null) + if (result.State is MessageBodyState.NotFound or MessageBodyState.Unavailable) { - logger.LogWarning("{MessageId}: Message Body not found in failed message with unique id {UniqueMessageId} for attempt Id {AttemptMessageId}", messageId, uniqueMessageId, attemptMessageId); + throw new InvalidOperationException($"Cannot retry failed message {uniqueMessageId} because its body state is {result.State}."); } - else + + if (result.State == MessageBodyState.Empty) { - logger.LogDebug("{MessageId}: Body size: {MessageLength} bytes retrieved from failed message attachment", messageId, body.LongLength); + return EmptyBody; } - return body; + await using (result.Content.Stream) + { + using var memoryStream = new MemoryStream(); + await result.Content.Stream.CopyToAsync(memoryStream, cancellationToken); + var body = memoryStream.ToArray(); + + logger.LogDebug("{MessageId}: Body size: {MessageLength} bytes retrieved from failed message attachment", messageId, body.LongLength); + return body; + } } static readonly byte[] EmptyBody = Array.Empty(); From ec0b69ca0ac6093aecad0ea388a94879e4cbf38f Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 17:11:59 +1000 Subject: [PATCH 08/12] Propagate cancellation tokens through persistence dialects, licensing and acceptance test infrastructure Retires the cancellation analyzer debt blocks in six projects, which is Phase 7 of the propagation work. ServiceControl.Persistence.EFCore.SqlServer / .PostgreSql: the dialect implementations now match their interfaces, whose tokens are already optional. ServiceControl.RavenDB: EmbeddedDatabase.DeleteDatabase takes a token and forwards it, so deleting a database can be cancelled. Particular.LicensingComponent: IAuditQuery and IThroughputCollector and their implementations, plus BrokerThroughputCollectorHostedService's stoppingToken renamed to cancellationToken. Five catch blocks now catch OperationCanceledException filtered on the token before catching Exception, so shutdown cancellation is no longer reported as a failure to gather throughput or to reach the audit remotes. ServiceControl.AcceptanceTesting: the HTTP helpers and assertion helpers take and forward optional tokens. The scenario poll loop captured cancellation as a test failure on shutdown, so it now swallows its own cancellation and holds the linked token in a local rather than reading a disposed source. The project block is narrowed to the types that sit on NServiceBus.AcceptanceTesting extension points, which drive them without a token. ServiceControl.Audit.Persistence.RavenDB: the PS0020 carve-out on LicenseStatusCheck moves from the project editorconfig to an inline pragma. The filter is deliberate, since the try runs on a linked token that is always cancelled in the timeout case being mapped. --- .../.editorconfig | 8 --- .../AuditThroughput/AuditQuery.cs | 12 ++-- .../AuditThroughputCollectorHostedService.cs | 8 ++- .../AuditThroughput/IAuditQuery.cs | 8 +-- .../BrokerThroughputCollectorHostedService.cs | 34 ++++++---- .../IThroughputCollector.cs | 16 ++--- .../MonitoringThroughput/MonitoringService.cs | 4 +- .../MonitoringThroughputHostedService.cs | 8 ++- .../ThroughputCollector.cs | 16 ++--- .../WebApi/LicensingController.cs | 16 ++--- .../.editorconfig | 9 +-- .../Cors/CorsAssertions.cs | 11 +-- .../EndpointConfigurationExtensions.cs | 3 +- .../ForwardedHeadersAssertions.cs | 8 ++- .../HttpClientExtensions.cs | 5 +- .../HttpExtensions.cs | 67 ++++++++++--------- .../OpenIdConnect/OpenIdConnectAssertions.cs | 20 +++--- .../ScenarioWithEndpointBehaviorExtensions.cs | 11 ++- .../AcceptanceTestStorageConfiguration.cs | 2 +- .../.editorconfig | 6 -- .../LicenseStatusCheck.cs | 2 + .../.editorconfig | 5 -- .../PostgreSqlDialect.cs | 2 +- ...tgreSqlFailedMessageIngestionSqlDialect.cs | 6 +- .../PostgreSqlRetryBatchSqlDialect.cs | 2 +- .../.editorconfig | 5 -- .../SqlServerDialect.cs | 2 +- ...lServerFailedMessageIngestionSqlDialect.cs | 6 +- .../SqlServerRetryBatchSqlDialect.cs | 2 +- src/ServiceControl.RavenDB/.editorconfig | 6 -- .../EmbeddedDatabase.cs | 10 +-- 31 files changed, 165 insertions(+), 155 deletions(-) diff --git a/src/Particular.LicensingComponent/.editorconfig b/src/Particular.LicensingComponent/.editorconfig index 97adb8c6b7..423278de8e 100644 --- a/src/Particular.LicensingComponent/.editorconfig +++ b/src/Particular.LicensingComponent/.editorconfig @@ -12,11 +12,3 @@ dotnet_diagnostic.IDE0010.severity = suggestion csharp_style_var_elsewhere = true:error csharp_style_var_for_built_in_types = true:error - -# 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.PS0008.severity = none -dotnet_diagnostic.PS0017.severity = none -dotnet_diagnostic.PS0019.severity = none diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs index 7227159665..619a6ebae6 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs @@ -17,7 +17,7 @@ public class AuditQuery(ILogger logger, IEndpointsApi endpointsApi, r.SemanticVersion >= MinAuditCountsVersion && r.Retention >= TimeSpan.FromDays(2); - public async Task> GetKnownEndpoints(CancellationToken cancellationToken) + public async Task> GetKnownEndpoints(CancellationToken cancellationToken = default) { var endpoints = await endpointsApi.GetEndpoints(cancellationToken); @@ -36,9 +36,9 @@ public async Task> GetKnownEndpoints(Cancell return scEndpoints ?? []; } - public async Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken) => (await auditCountApi.GetEndpointAuditCounts(endpointUrlName, cancellationToken)).Select(s => new AuditCount { Count = s.Count, UtcDate = DateOnly.FromDateTime(s.UtcDate) }); + public async Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken = default) => (await auditCountApi.GetEndpointAuditCounts(endpointUrlName, cancellationToken)).Select(s => new AuditCount { Count = s.Count, UtcDate = DateOnly.FromDateTime(s.UtcDate) }); - public async Task> GetAuditRemotes(CancellationToken cancellationToken) + public async Task> GetAuditRemotes(CancellationToken cancellationToken = default) { try { @@ -101,6 +101,10 @@ public async Task> GetAuditRemotes(CancellationT return remotesInfo; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { logger.LogError(ex, "Failed to get Audit Remotes"); @@ -108,7 +112,7 @@ public async Task> GetAuditRemotes(CancellationT } } - public async Task TestAuditConnection(CancellationToken cancellationToken) + public async Task TestAuditConnection(CancellationToken cancellationToken = default) { var connectionTestResult = new ConnectionSettingsTestResult { ConnectionSuccessful = true }; diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs index 91c90dbb95..8b50f6cece 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs @@ -18,7 +18,7 @@ public class AuditThroughputCollectorHostedService( public TimeSpan DelayStart { get; set; } = TimeSpan.FromSeconds(40); public static List AuditQueues { get; set; } = []; - protected override async Task ExecuteAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { logger.LogInformation("Starting {ServiceName}", nameof(AuditThroughputCollectorHostedService)); @@ -34,7 +34,11 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await GatherThroughput(cancellationToken); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) { logger.LogError(ex, "Failed to gather throughput from audit"); } diff --git a/src/Particular.LicensingComponent/AuditThroughput/IAuditQuery.cs b/src/Particular.LicensingComponent/AuditThroughput/IAuditQuery.cs index ea63076356..66f03b7f35 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/IAuditQuery.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/IAuditQuery.cs @@ -8,11 +8,11 @@ public interface IAuditQuery SemanticVersion MinAuditCountsVersion { get; } Func ValidRemoteInstances { get; } - Task> GetKnownEndpoints(CancellationToken cancellationToken); + Task> GetKnownEndpoints(CancellationToken cancellationToken = default); - Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken); - Task> GetAuditRemotes(CancellationToken cancellationToken); - Task TestAuditConnection(CancellationToken cancellationToken); + Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken = default); + Task> GetAuditRemotes(CancellationToken cancellationToken = default); + Task TestAuditConnection(CancellationToken cancellationToken = default); } } diff --git a/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs b/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs index b8c37d8521..f62b770c1a 100644 --- a/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs +++ b/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs @@ -19,7 +19,7 @@ public class BrokerThroughputCollectorHostedService( { public TimeSpan DelayStart { get; set; } = TimeSpan.FromSeconds(40); - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { static ReadOnlyDictionary LoadBrokerSettingValues(IEnumerable brokerKeys) => new(brokerKeys.Select(pair => KeyValuePair.Create(pair.Key, SettingsReader.Read(ThroughputSettings.SettingsNamespace, pair.Key))) @@ -37,7 +37,7 @@ static ReadOnlyDictionary LoadBrokerSettingValues(IEnumerable LoadBrokerSettingValues(IEnumerable(); var postfixGenerator = new PostfixGenerator(); - await foreach (var queueName in brokerThroughputQuery.GetQueueNames(stoppingToken)) + await foreach (var queueName in brokerThroughputQuery.GetQueueNames(cancellationToken)) { if (PlatformEndpointHelper.IsPlatformEndpoint(queueName.SanitizedName, throughputSettings)) { @@ -78,13 +82,13 @@ async Task GatherThroughput(CancellationToken stoppingToken) } await Task.WhenAll(waitingTasks); - await dataStore.SaveBrokerMetadata(new BrokerMetadata(brokerThroughputQuery.ScopeType, brokerThroughputQuery.Data), stoppingToken); + await dataStore.SaveBrokerMetadata(new BrokerMetadata(brokerThroughputQuery.ScopeType, brokerThroughputQuery.Data), cancellationToken); return; async Task Exec(IBrokerQueue queueName, string postfix) { var endpointId = new EndpointIdentifier(queueName.QueueName, ThroughputSource.Broker); - var endpoint = await dataStore.GetEndpoint(endpointId, stoppingToken); + var endpoint = await dataStore.GetEndpoint(endpointId, cancellationToken); if (endpoint == null) { @@ -95,14 +99,18 @@ async Task Exec(IBrokerQueue queueName, string postfix) EndpointIndicators = [.. queueName.EndpointIndicators] }; - await dataStore.SaveEndpoint(endpoint, stoppingToken); + await dataStore.SaveEndpoint(endpoint, cancellationToken); } - await foreach (var queueThroughput in brokerThroughputQuery.GetThroughputPerDay(queueName, endpoint.LastCollectedDate.AddDays(1), stoppingToken)) + await foreach (var queueThroughput in brokerThroughputQuery.GetThroughputPerDay(queueName, endpoint.LastCollectedDate.AddDays(1), cancellationToken)) { try { - await dataStore.RecordEndpointThroughput(queueName.QueueName, ThroughputSource.Broker, queueThroughput.DateUTC, queueThroughput.TotalThroughput, stoppingToken); + await dataStore.RecordEndpointThroughput(queueName.QueueName, ThroughputSource.Broker, queueThroughput.DateUTC, queueThroughput.TotalThroughput, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception e) { diff --git a/src/Particular.LicensingComponent/IThroughputCollector.cs b/src/Particular.LicensingComponent/IThroughputCollector.cs index a880f1c37e..6e325dab79 100644 --- a/src/Particular.LicensingComponent/IThroughputCollector.cs +++ b/src/Particular.LicensingComponent/IThroughputCollector.cs @@ -5,13 +5,13 @@ public interface IThroughputCollector { - Task> GetThroughputSummary(CancellationToken cancellationToken); - Task UpdateUserIndicatorsOnEndpoints(List userIndicatorUpdates, CancellationToken cancellationToken); - Task GetThroughputConnectionSettingsInformation(CancellationToken cancellationToken); - Task TestConnectionSettings(CancellationToken cancellationToken); - Task GenerateThroughputReport(string spVersion, DateTime? reportEndDate, CancellationToken cancellationToken); - Task GetReportGenerationState(CancellationToken cancellationToken); - Task> GetReportMasks(CancellationToken cancellationToken); - Task UpdateReportMasks(List reportMaskUpdates, CancellationToken cancellationToken); + Task> GetThroughputSummary(CancellationToken cancellationToken = default); + Task UpdateUserIndicatorsOnEndpoints(List userIndicatorUpdates, CancellationToken cancellationToken = default); + Task GetThroughputConnectionSettingsInformation(CancellationToken cancellationToken = default); + Task TestConnectionSettings(CancellationToken cancellationToken = default); + Task GenerateThroughputReport(string spVersion, DateTime? reportEndDate, CancellationToken cancellationToken = default); + Task GetReportGenerationState(CancellationToken cancellationToken = default); + Task> GetReportMasks(CancellationToken cancellationToken = default); + Task UpdateReportMasks(List reportMaskUpdates, CancellationToken cancellationToken = default); } } diff --git a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs index bddab8ab3c..b0dd964a06 100644 --- a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs +++ b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs @@ -10,7 +10,7 @@ public class MonitoringService(ILicensingDataStore dataStore, IBrokerThroughputQuery? brokerThroughputQuery = null) { - public async Task RecordMonitoringThroughput(byte[] throughputMessage, CancellationToken cancellationToken) + public async Task RecordMonitoringThroughput(byte[] throughputMessage, CancellationToken cancellationToken = default) { RecordEndpointThroughputData? message; using (Stream stream = new MemoryStream(throughputMessage)) @@ -45,7 +45,7 @@ public async Task RecordMonitoringThroughput(byte[] throughputMessage, Cancellat } } - public async Task TestMonitoringConnection(CancellationToken cancellationToken) + public async Task TestMonitoringConnection(CancellationToken cancellationToken = default) { //NOTE can't actually test the monitoring connection apart from seeing if there has been any throughput recorded from Monitoring diff --git a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs index fc5e643bd2..ea8a299e39 100644 --- a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs +++ b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs @@ -16,13 +16,17 @@ async Task Handle(MessageContext message, CancellationToken cancellationToken) { await monitoringService.RecordMonitoringThroughput(message.Body.ToArray(), cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { logger.LogError(ex, "Error receiving throughput data from Monitoring"); } } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken = default) { logger.LogInformation("Starting {ServiceName}", nameof(MonitoringThroughputHostedService)); @@ -30,7 +34,7 @@ public async Task StartAsync(CancellationToken cancellationToken) await transportInfrastructure.Receivers[ServiceControlSettings.ServiceControlThroughputDataQueue].StartReceive(cancellationToken); } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { logger.LogInformation("Stopping {ServiceName}", nameof(MonitoringThroughputHostedService)); diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 32f70aaf40..03f11f2a06 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -16,7 +16,7 @@ public class ThroughputCollector(ILicensingDataStore dataStore, ThroughputSettings throughputSettings, IAuditQuery auditQuery, MonitoringService monitoringService, IEnumerable environmentDataProviders, IBrokerThroughputQuery? throughputQuery = null) : IThroughputCollector { - public async Task GetThroughputConnectionSettingsInformation(CancellationToken cancellationToken) + public async Task GetThroughputConnectionSettingsInformation(CancellationToken cancellationToken = default) { var throughputConnectionSettings = new ThroughputConnectionSettings { @@ -27,7 +27,7 @@ public async Task GetThroughputConnectionSettingsI return await Task.FromResult(throughputConnectionSettings); } - public async Task TestConnectionSettings(CancellationToken cancellationToken) + public async Task TestConnectionSettings(CancellationToken cancellationToken = default) { var tasks = new List(); var brokerTask = Task.FromResult(new ConnectionSettingsTestResult { ConnectionSuccessful = false, ConnectionErrorMessages = [] }); @@ -55,13 +55,13 @@ public async Task TestConnectionSettings(CancellationToke return await Task.FromResult(connectionTestResults); } - public async Task UpdateUserIndicatorsOnEndpoints(List userIndicatorUpdates, CancellationToken cancellationToken) => + public async Task UpdateUserIndicatorsOnEndpoints(List userIndicatorUpdates, CancellationToken cancellationToken = default) => await dataStore.UpdateUserIndicatorOnEndpoints(userIndicatorUpdates, cancellationToken); - public async Task> GetReportMasks(CancellationToken cancellationToken) => await dataStore.GetReportMasks(cancellationToken); - public async Task UpdateReportMasks(List reportMaskUpdates, CancellationToken cancellationToken) => await dataStore.SaveReportMasks(reportMaskUpdates, cancellationToken); + public async Task> GetReportMasks(CancellationToken cancellationToken = default) => await dataStore.GetReportMasks(cancellationToken); + public async Task UpdateReportMasks(List reportMaskUpdates, CancellationToken cancellationToken = default) => await dataStore.SaveReportMasks(reportMaskUpdates, cancellationToken); - public async Task> GetThroughputSummary(CancellationToken cancellationToken) + public async Task> GetThroughputSummary(CancellationToken cancellationToken = default) { var endpointSummaries = new List(); @@ -86,7 +86,7 @@ public async Task> GetThroughputSummary(Cancella return endpointSummaries; } - public async Task GetReportGenerationState(CancellationToken cancellationToken) => + public async Task GetReportGenerationState(CancellationToken cancellationToken = default) => throughputQuery == null ? await GetReportGenerationStateForNonBroker(cancellationToken) : await GetReportGenerationStateForBroker(cancellationToken); @@ -115,7 +115,7 @@ async Task GetReportGenerationStateForNonBroker(Cancellat }; } - public async Task GenerateThroughputReport(string spVersion, DateTime? reportEndDate, CancellationToken cancellationToken) + public async Task GenerateThroughputReport(string spVersion, DateTime? reportEndDate, CancellationToken cancellationToken = default) { var reportMasks = await dataStore.GetReportMasks(cancellationToken); var masker = new Masker([.. reportMasks]); diff --git a/src/Particular.LicensingComponent/WebApi/LicensingController.cs b/src/Particular.LicensingComponent/WebApi/LicensingController.cs index f898c0cc72..99019ae60c 100644 --- a/src/Particular.LicensingComponent/WebApi/LicensingController.cs +++ b/src/Particular.LicensingComponent/WebApi/LicensingController.cs @@ -24,7 +24,7 @@ public LicensingController(IThroughputCollector throughputCollector) [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("endpoints")] [HttpGet] - public async Task> GetEndpointThroughput(CancellationToken cancellationToken) + public async Task> GetEndpointThroughput(CancellationToken cancellationToken = default) { return await throughputCollector.GetThroughputSummary(cancellationToken); } @@ -32,7 +32,7 @@ public async Task> GetEndpointThroughput(Cancell [Authorize(Policy = Permissions.ErrorThroughputManage)] [Route("endpoints/update")] [HttpPost] - public async Task UpdateUserSelectionOnEndpointThroughput(List updateUserIndicators, CancellationToken cancellationToken) + public async Task UpdateUserSelectionOnEndpointThroughput(List updateUserIndicators, CancellationToken cancellationToken = default) { await throughputCollector.UpdateUserIndicatorsOnEndpoints(updateUserIndicators, cancellationToken); return Ok(); @@ -41,7 +41,7 @@ public async Task UpdateUserSelectionOnEndpointThroughput(List CanThroughputReportBeGenerated(CancellationToken cancellationToken) + public async Task CanThroughputReportBeGenerated(CancellationToken cancellationToken = default) { return await throughputCollector.GetReportGenerationState(cancellationToken); } @@ -49,7 +49,7 @@ public async Task CanThroughputReportBeGenerated(Cancella [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("report/file")] [HttpGet] - public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string? spVersion, CancellationToken cancellationToken) + public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string? spVersion, CancellationToken cancellationToken = default) { var reportStatus = await CanThroughputReportBeGenerated(cancellationToken); if (!reportStatus.ReportCanBeGenerated) @@ -86,7 +86,7 @@ public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/info")] [HttpGet] - public async Task GetThroughputSettingsInformation(CancellationToken cancellationToken) + public async Task GetThroughputSettingsInformation(CancellationToken cancellationToken = default) { return await throughputCollector.GetThroughputConnectionSettingsInformation(cancellationToken); } @@ -94,12 +94,12 @@ public async Task GetThroughputSettingsInformation [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/test")] [HttpGet] - public async Task TestThroughputConnectionSettings(CancellationToken cancellationToken) => await throughputCollector.TestConnectionSettings(cancellationToken); + public async Task TestThroughputConnectionSettings(CancellationToken cancellationToken = default) => await throughputCollector.TestConnectionSettings(cancellationToken); [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/masks")] [HttpGet] - public async Task> GetMasks(CancellationToken cancellationToken) + public async Task> GetMasks(CancellationToken cancellationToken = default) { return await throughputCollector.GetReportMasks(cancellationToken); } @@ -107,7 +107,7 @@ public async Task> GetMasks(CancellationToken cancellationToken) [Authorize(Policy = Permissions.ErrorThroughputManage)] [Route("settings/masks/update")] [HttpPost] - public async Task UpdateMasks(List updateMasks, CancellationToken cancellationToken) + public async Task UpdateMasks(List updateMasks, CancellationToken cancellationToken = default) { await throughputCollector.UpdateReportMasks(updateMasks, cancellationToken); return Ok(); diff --git a/src/ServiceControl.AcceptanceTesting/.editorconfig b/src/ServiceControl.AcceptanceTesting/.editorconfig index f3c943025f..cc18ee5f5b 100644 --- a/src/ServiceControl.AcceptanceTesting/.editorconfig +++ b/src/ServiceControl.AcceptanceTesting/.editorconfig @@ -3,9 +3,10 @@ # Justification: Usage is from test projects 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. +# These types sit on NServiceBus.AcceptanceTesting's extension points, which drive them without a +# CancellationToken: IEndpointSetupTemplate, IComponentBehavior/ComponentRunner, and the scenario +# Done/When callbacks whose delegate shapes the framework fixes. A token added here could only ever +# be CancellationToken.None. Everything else in this project takes and forwards one. +[{EndpointTemplates/*.cs,InfrastructureConfig/*.cs,ScenarioWithEndpointBehaviorExtensions.cs,Sequence.cs,DispatchRawMessages.cs}] dotnet_diagnostic.PS0013.severity = none dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none diff --git a/src/ServiceControl.AcceptanceTesting/Cors/CorsAssertions.cs b/src/ServiceControl.AcceptanceTesting/Cors/CorsAssertions.cs index d92d55af6d..2c9e4189dc 100644 --- a/src/ServiceControl.AcceptanceTesting/Cors/CorsAssertions.cs +++ b/src/ServiceControl.AcceptanceTesting/Cors/CorsAssertions.cs @@ -2,6 +2,7 @@ namespace ServiceControl.AcceptanceTesting.Cors { using System.Net; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -22,13 +23,14 @@ public static class CorsAssertions public static async Task SendPreflightRequest( HttpClient httpClient, string origin, - string requestMethod = "GET") + string requestMethod = "GET", + CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Options, "/api"); request.Headers.Add("Origin", origin); request.Headers.Add("Access-Control-Request-Method", requestMethod); - return await httpClient.SendAsync(request); + return await httpClient.SendAsync(request, cancellationToken); } /// @@ -37,12 +39,13 @@ public static async Task SendPreflightRequest( public static async Task SendRequestWithOrigin( HttpClient httpClient, string origin, - string endpoint = "/api") + string endpoint = "/api", + CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, endpoint); request.Headers.Add("Origin", origin); - return await httpClient.SendAsync(request); + return await httpClient.SendAsync(request, cancellationToken); } /// diff --git a/src/ServiceControl.AcceptanceTesting/EndpointConfigurationExtensions.cs b/src/ServiceControl.AcceptanceTesting/EndpointConfigurationExtensions.cs index 8998e768d7..bcce34bc4f 100644 --- a/src/ServiceControl.AcceptanceTesting/EndpointConfigurationExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/EndpointConfigurationExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; + using System.Threading; using System.Threading.Tasks; using InfrastructureConfig; using NServiceBus; @@ -54,7 +55,7 @@ IEnumerable GetNestedTypeRecursive(Type rootType, Type builderType) } } - public static async Task DefinePersistence(this EndpointConfiguration config, RunDescriptor runDescriptor, EndpointCustomizationConfiguration endpointCustomizationConfiguration) + public static async Task DefinePersistence(this EndpointConfiguration config, RunDescriptor runDescriptor, EndpointCustomizationConfiguration endpointCustomizationConfiguration, CancellationToken cancellationToken = default) { var persistenceConfiguration = new ConfigureEndpointInMemoryPersistence(); await persistenceConfiguration.Configure(endpointCustomizationConfiguration.EndpointName, config, runDescriptor.Settings, endpointCustomizationConfiguration.PublisherMetadata); diff --git a/src/ServiceControl.AcceptanceTesting/ForwardedHeaders/ForwardedHeadersAssertions.cs b/src/ServiceControl.AcceptanceTesting/ForwardedHeaders/ForwardedHeadersAssertions.cs index a8da121863..a4f90a49ba 100644 --- a/src/ServiceControl.AcceptanceTesting/ForwardedHeaders/ForwardedHeadersAssertions.cs +++ b/src/ServiceControl.AcceptanceTesting/ForwardedHeaders/ForwardedHeadersAssertions.cs @@ -2,6 +2,7 @@ namespace ServiceControl.AcceptanceTesting.ForwardedHeaders { using System.Net.Http; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -20,7 +21,8 @@ public static async Task GetRequestInfo( string xForwardedFor = null, string xForwardedProto = null, string xForwardedHost = null, - string testRemoteIp = null) + string testRemoteIp = null, + CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, "/debug/request-info"); @@ -41,10 +43,10 @@ public static async Task GetRequestInfo( request.Headers.Add(TestRemoteIpMiddleware.HeaderName, testRemoteIp); } - var response = await httpClient.SendAsync(request); + var response = await httpClient.SendAsync(request, cancellationToken); _ = response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); return JsonSerializer.Deserialize(content, serializerOptions); } diff --git a/src/ServiceControl.AcceptanceTesting/HttpClientExtensions.cs b/src/ServiceControl.AcceptanceTesting/HttpClientExtensions.cs index 861fbe4045..f30d41518b 100644 --- a/src/ServiceControl.AcceptanceTesting/HttpClientExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/HttpClientExtensions.cs @@ -1,11 +1,12 @@ namespace ServiceControl.AcceptanceTesting { using System.Net.Http; + using System.Threading; using System.Threading.Tasks; public static class HttpClientExtensions { - public static async Task PatchAsync(this HttpClient client, string requestUri, HttpContent iContent) + public static async Task PatchAsync(this HttpClient client, string requestUri, HttpContent iContent, CancellationToken cancellationToken = default) { var method = new HttpMethod("PATCH"); var request = new HttpRequestMessage(method, requestUri) @@ -13,7 +14,7 @@ public static async Task PatchAsync(this HttpClient client, Content = iContent }; - var response = await client.SendAsync(request); + var response = await client.SendAsync(request, cancellationToken); return response; } diff --git a/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs b/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs index 7e5ebde17f..9f8c8ee140 100644 --- a/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/HttpExtensions.cs @@ -6,16 +6,17 @@ namespace ServiceControl.AcceptanceTesting using System.Net; using System.Net.Http; using System.Net.Http.Json; + using System.Threading; using System.Threading.Tasks; public static class HttpExtensions { - public static async Task Put(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null, Func requestHasFailed = null) where T : class + public static async Task Put(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null, Func requestHasFailed = null, CancellationToken cancellationToken = default) where T : class { requestHasFailed ??= statusCode => statusCode is not HttpStatusCode.OK and not HttpStatusCode.Accepted; var httpClient = provider.HttpClient; - var response = await httpClient.PutAsJsonAsync(url, payload, provider.SerializerOptions); + var response = await httpClient.PutAsJsonAsync(url, payload, provider.SerializerOptions, cancellationToken); if (requestHasFailed(response.StatusCode)) { @@ -23,24 +24,24 @@ public static async Task Put(this IAcceptanceTestInfrastructureProvider provi } } - public static Task GetRaw(this IAcceptanceTestInfrastructureProvider provider, string url) + public static Task GetRaw(this IAcceptanceTestInfrastructureProvider provider, string url, CancellationToken cancellationToken = default) { var httpClient = provider.HttpClient; - return httpClient.GetAsync(url); + return httpClient.GetAsync(url, cancellationToken); } - public static Task Options(this IAcceptanceTestInfrastructureProvider provider, string url) + public static Task Options(this IAcceptanceTestInfrastructureProvider provider, string url, CancellationToken cancellationToken = default) { var httpClient = provider.HttpClient; var request = new HttpRequestMessage(HttpMethod.Options, url); - return httpClient.SendAsync(request); + return httpClient.SendAsync(request, cancellationToken); } - public static async Task> TryGetMany(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null) where T : class + public static async Task> TryGetMany(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null, CancellationToken cancellationToken = default) where T : class { condition ??= _ => true; - var response = await provider.GetInternal>(url); + var response = await provider.GetInternal>(url, cancellationToken); if (response == null || !response.Any(m => condition(m))) { @@ -50,25 +51,25 @@ public static async Task> TryGetMany(this IAcceptanceTestInfras return ManyResult.New(true, response.Where(m => condition(m)).ToList()); } - public static async Task Patch(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null) where T : class + public static async Task Patch(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null, CancellationToken cancellationToken = default) where T : class { var httpClient = provider.HttpClient; - var response = await httpClient.PatchAsJsonAsync(url, payload, provider.SerializerOptions); + var response = await httpClient.PatchAsJsonAsync(url, payload, provider.SerializerOptions, cancellationToken); if (!response.IsSuccessStatusCode) { - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(cancellationToken); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} - {body}"); } return response.StatusCode; } - public static async Task> TryGet(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null) where T : class + public static async Task> TryGet(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null, CancellationToken cancellationToken = default) where T : class { condition ??= _ => true; - var response = await provider.GetInternal(url); + var response = await provider.GetInternal(url, cancellationToken); if (response == null || !condition(response)) { @@ -78,11 +79,11 @@ public static async Task> TryGet(this IAcceptanceTestInfrastr return SingleResult.New(response); } - public static async Task> TryGet(this IAcceptanceTestInfrastructureProvider provider, string url, Func> condition) where T : class + public static async Task> TryGet(this IAcceptanceTestInfrastructureProvider provider, string url, Func> condition, CancellationToken cancellationToken = default) where T : class { - var response = await provider.GetInternal(url); + var response = await provider.GetInternal(url, cancellationToken); - if (response == null || !await condition(response)) + if (response == null || !await condition(response, cancellationToken)) { return SingleResult.Empty; } @@ -90,11 +91,11 @@ public static async Task> TryGet(this IAcceptanceTestInfrastr return SingleResult.New(response); } - public static async Task> TryGetSingle(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null) where T : class + public static async Task> TryGetSingle(this IAcceptanceTestInfrastructureProvider provider, string url, Predicate condition = null, CancellationToken cancellationToken = default) where T : class { condition ??= _ => true; - var response = await provider.GetInternal>(url); + var response = await provider.GetInternal>(url, cancellationToken); T item = null; if (response != null) { @@ -116,17 +117,17 @@ public static async Task> TryGetSingle(this IAcceptanceTestIn return SingleResult.Empty; } - public static async Task Get(this IAcceptanceTestInfrastructureProvider provider, string url) + public static async Task Get(this IAcceptanceTestInfrastructureProvider provider, string url, CancellationToken cancellationToken = default) { var httpClient = provider.HttpClient; - var response = await httpClient.GetAsync(url); + var response = await httpClient.GetAsync(url, cancellationToken); return response.StatusCode; } - public static async Task Post(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null, Func requestHasFailed = null) where T : class + public static async Task Post(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null, Func requestHasFailed = null, CancellationToken cancellationToken = default) where T : class { var httpClient = provider.HttpClient; - var response = await httpClient.PostAsJsonAsync(url, payload, provider.SerializerOptions); + var response = await httpClient.PostAsJsonAsync(url, payload, provider.SerializerOptions, cancellationToken); if (requestHasFailed != null) { @@ -140,39 +141,39 @@ public static async Task Post(this IAcceptanceTestInfrastructureProvider prov if (!response.IsSuccessStatusCode) { - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(cancellationToken); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} - {body}"); } } - public static async Task Delete(this IAcceptanceTestInfrastructureProvider provider, string url) + public static async Task Delete(this IAcceptanceTestInfrastructureProvider provider, string url, CancellationToken cancellationToken = default) { var httpClient = provider.HttpClient; - var response = await httpClient.DeleteAsync(url); + var response = await httpClient.DeleteAsync(url, cancellationToken); if (!response.IsSuccessStatusCode) { - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(cancellationToken); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} - {body}"); } } - public static async Task DownloadData(this IAcceptanceTestInfrastructureProvider provider, string url, HttpStatusCode successCode = HttpStatusCode.OK) + public static async Task DownloadData(this IAcceptanceTestInfrastructureProvider provider, string url, HttpStatusCode successCode = HttpStatusCode.OK, CancellationToken cancellationToken = default) { var httpClient = provider.HttpClient; - var response = await httpClient.GetAsync(url); + var response = await httpClient.GetAsync(url, cancellationToken); if (response.StatusCode != successCode) { throw new Exception($"Expected status code of {successCode}, but instead got {response.StatusCode}."); } - return await response.Content.ReadAsByteArrayAsync(); + return await response.Content.ReadAsByteArrayAsync(cancellationToken); } - static async Task GetInternal(this IAcceptanceTestInfrastructureProvider provider, string url) where T : class + static async Task GetInternal(this IAcceptanceTestInfrastructureProvider provider, string url, CancellationToken cancellationToken) where T : class { - var response = await provider.GetRaw(url); + var response = await provider.GetRaw(url, cancellationToken); //for now if (response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.NoContent or HttpStatusCode.ServiceUnavailable) @@ -183,12 +184,12 @@ static async Task GetInternal(this IAcceptanceTestInfrastructureProvider p if (response.StatusCode != HttpStatusCode.OK) { - var content = await response.Content.ReadAsStringAsync(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); LogRequest(response.ReasonPhrase + content); throw new InvalidOperationException($"Call failed: {(int)response.StatusCode} - {response.ReasonPhrase} {Environment.NewLine} {content}"); } - var payload = await response.Content.ReadFromJsonAsync(provider.SerializerOptions); + var payload = await response.Content.ReadFromJsonAsync(provider.SerializerOptions, cancellationToken); LogRequest(); return payload; diff --git a/src/ServiceControl.AcceptanceTesting/OpenIdConnect/OpenIdConnectAssertions.cs b/src/ServiceControl.AcceptanceTesting/OpenIdConnect/OpenIdConnectAssertions.cs index b48cc3ea87..54d36581b2 100644 --- a/src/ServiceControl.AcceptanceTesting/OpenIdConnect/OpenIdConnectAssertions.cs +++ b/src/ServiceControl.AcceptanceTesting/OpenIdConnect/OpenIdConnectAssertions.cs @@ -4,6 +4,7 @@ namespace ServiceControl.AcceptanceTesting.OpenIdConnect using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -74,9 +75,9 @@ public static void AssertWwwAuthenticateHeader(HttpResponseMessage response) /// /// Asserts that the response body contains the expected error response format. /// - public static async Task AssertAuthErrorResponse(HttpResponseMessage response, string expectedError = "unauthorized") + public static async Task AssertAuthErrorResponse(HttpResponseMessage response, string expectedError = "unauthorized", CancellationToken cancellationToken = default) { - var content = await response.Content.ReadAsStringAsync(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); Assert.That(content, Is.Not.Null.And.Not.Empty, "Response should have a body"); var jsonDoc = JsonDocument.Parse(content); @@ -107,12 +108,13 @@ public static async Task AssertAuthConfigurationResponse( string expectedAudience = null, string expectedApiScopes = null, string expectedScopes = null, - bool expectedRoleBasedAuthorizationEnabled = false) + bool expectedRoleBasedAuthorizationEnabled = false, + CancellationToken cancellationToken = default) { Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), "Authentication configuration endpoint should return 200 OK"); - var content = await response.Content.ReadAsStringAsync(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); var jsonDoc = JsonDocument.Parse(content); var root = jsonDoc.RootElement; @@ -201,11 +203,12 @@ public static async Task SendRequestWithBearerToken( HttpClient client, HttpMethod method, string path, - string token) + string token, + CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(method, path); request.Headers.Authorization = CreateBearerToken(token); - return await client.SendAsync(request); + return await client.SendAsync(request, cancellationToken); } /// @@ -214,10 +217,11 @@ public static async Task SendRequestWithBearerToken( public static async Task SendRequestWithoutAuth( HttpClient client, HttpMethod method, - string path) + string path, + CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(method, path); - return await client.SendAsync(request); + return await client.SendAsync(request, cancellationToken); } } } diff --git a/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs b/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs index 745bb3bcf5..8f02d3ea2e 100644 --- a/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs +++ b/src/ServiceControl.AcceptanceTesting/ScenarioWithEndpointBehaviorExtensions.cs @@ -104,11 +104,12 @@ class Runner( public override Task ComponentsStarted(CancellationToken cancellationToken = default) { tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var checkToken = tokenSource.Token; checkTask = Task.Run(async () => { try { - while (!tokenSource.IsCancellationRequested) + while (!checkToken.IsCancellationRequested) { if (await isDone(scenarioContext)) { @@ -116,14 +117,18 @@ public override Task ComponentsStarted(CancellationToken cancellationToken = def return; } - await Task.Delay(100, tokenSource.Token); + await Task.Delay(100, checkToken); } } + catch (OperationCanceledException) when (checkToken.IsCancellationRequested) + { + // Stopping the run cancels the poll; that is not a test failure + } catch (Exception e) { setException(ExceptionDispatchInfo.Capture(e)); } - }, tokenSource.Token); + }, checkToken); return Task.CompletedTask; } diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs index 462c2ab607..3157f0a53d 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -34,7 +34,7 @@ public async Task Cleanup(CancellationToken cancellationToken = default) return; } using var _ = await UseDatabaseLifecycleLock(cancellationToken); - await databaseInstance.DeleteDatabase(databaseName); + await databaseInstance.DeleteDatabase(databaseName, cancellationToken); } /// diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/.editorconfig b/src/ServiceControl.Audit.Persistence.RavenDB/.editorconfig index b9cef92e2f..ff993b49bb 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/.editorconfig +++ b/src/ServiceControl.Audit.Persistence.RavenDB/.editorconfig @@ -2,9 +2,3 @@ # Justification: ServiceControl app has no synchronization context dotnet_diagnostic.CA2007.severity = none - -# WaitForLicenseOrThrow passes only the linked cts.Token into its try, so PS0020 cannot recognise a -# filter on the caller's token. Removing this needs the license-check exception mapping reworked, not -# a token added, so it is deliberately scoped to the one file rather than the project. -[LicenseStatusCheck.cs] -dotnet_diagnostic.PS0020.severity = none diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/LicenseStatusCheck.cs b/src/ServiceControl.Audit.Persistence.RavenDB/LicenseStatusCheck.cs index 935e948875..9b8415ab9e 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/LicenseStatusCheck.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/LicenseStatusCheck.cs @@ -37,7 +37,9 @@ public static async Task WaitForLicenseOrThrow(IDocumentStore documentStore, Can await Task.Delay(200, cts.Token); } } +#pragma warning disable PS0020 // The try runs on the linked cts.Token, which is always cancelled in the timeout case this maps. Filtering on it instead of the caller's token would make the filter unreachable catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) +#pragma warning restore PS0020 { throw new InvalidOperationException("Cannot validate the current RavenDB license. Please, contact support"); } diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/.editorconfig b/src/ServiceControl.Persistence.EFCore.PostgreSql/.editorconfig index ea99d7cc09..fc68ac3228 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/.editorconfig +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/.editorconfig @@ -3,11 +3,6 @@ # Justification: ServiceControl app has no synchronization context 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 - # Disable style rules for auto-generated EF migrations [Migrations/**.cs] dotnet_diagnostic.IDE0065.severity = none diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDialect.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDialect.cs index d177ef3f59..385b61ddc5 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDialect.cs @@ -7,7 +7,7 @@ namespace ServiceControl.Persistence.EFCore.PostgreSql; abstract class PostgreSqlDialect { - protected static async Task Execute(ServiceControlDbContext dbContext, string sql, IEnumerable rows, CancellationToken cancellationToken) + protected static async Task Execute(ServiceControlDbContext dbContext, string sql, IEnumerable rows, CancellationToken cancellationToken = default) { await using var command = dbContext.Database.GetDbConnection().CreateCommand(); command.Transaction = (dbContext.Database.CurrentTransaction diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlFailedMessageIngestionSqlDialect.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlFailedMessageIngestionSqlDialect.cs index 17184e1aeb..f1c9f0a0be 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlFailedMessageIngestionSqlDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlFailedMessageIngestionSqlDialect.cs @@ -12,7 +12,7 @@ namespace ServiceControl.Persistence.EFCore.PostgreSql; // atomic statement. Rows are chunked to keep statement texts down to a few reusable shapes. class PostgreSqlFailedMessageIngestionSqlDialect : PostgreSqlDialect, IFailedMessageIngestionSqlDialect { - public async Task UpsertFailedMessages(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task UpsertFailedMessages(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { foreach (var chunk in rows.Chunk(MaxRowsPerStatement)) { @@ -29,7 +29,7 @@ INSERT INTO failed_messages ({FailedMessageColumnList}) } } - public async Task InsertGroups(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertGroups(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { foreach (var chunk in rows.Chunk(MaxRowsPerStatement)) { @@ -46,7 +46,7 @@ ON CONFLICT (failed_message_unique_id, group_id) DO NOTHING } } - public async Task InsertMissingKnownEndpoints(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertMissingKnownEndpoints(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { foreach (var chunk in rows.Chunk(MaxRowsPerStatement)) { diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlRetryBatchSqlDialect.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlRetryBatchSqlDialect.cs index a15cb17c58..3de74ce12a 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlRetryBatchSqlDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlRetryBatchSqlDialect.cs @@ -6,7 +6,7 @@ namespace ServiceControl.Persistence.EFCore.PostgreSql; class PostgreSqlRetryBatchSqlDialect : PostgreSqlDialect, IRetryBatchSqlDialect { - public async Task InsertMissingRetryClaims(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertMissingRetryClaims(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { foreach (var chunk in rows.Chunk(MaxRowsPerStatement)) { diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/.editorconfig b/src/ServiceControl.Persistence.EFCore.SqlServer/.editorconfig index ea99d7cc09..fc68ac3228 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/.editorconfig +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/.editorconfig @@ -3,11 +3,6 @@ # Justification: ServiceControl app has no synchronization context 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 - # Disable style rules for auto-generated EF migrations [Migrations/**.cs] dotnet_diagnostic.IDE0065.severity = none diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDialect.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDialect.cs index 6a2ff7de55..a053469147 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDialect.cs @@ -8,7 +8,7 @@ namespace ServiceControl.Persistence.EFCore.SqlServer; abstract class SqlServerDialect { - protected static async Task Execute(ServiceControlDbContext dbContext, string sql, IEnumerable rows, CancellationToken cancellationToken) + protected static async Task Execute(ServiceControlDbContext dbContext, string sql, IEnumerable rows, CancellationToken cancellationToken = default) { await using var command = dbContext.Database.GetDbConnection().CreateCommand(); command.Transaction = (dbContext.Database.CurrentTransaction diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerFailedMessageIngestionSqlDialect.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerFailedMessageIngestionSqlDialect.cs index 6206d3bfa4..cb8f4c9f53 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerFailedMessageIngestionSqlDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerFailedMessageIngestionSqlDialect.cs @@ -12,7 +12,7 @@ namespace ServiceControl.Persistence.EFCore.SqlServer; // statement texts down to a few reusable shapes. class SqlServerFailedMessageIngestionSqlDialect : SqlServerDialect, IFailedMessageIngestionSqlDialect { - public async Task UpsertFailedMessages(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task UpsertFailedMessages(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { var maxRowsPerStatement = MaxRowsPerStatement(FailedMessageColumns.Length); foreach (var chunk in rows.Chunk(maxRowsPerStatement)) @@ -34,7 +34,7 @@ WHEN NOT MATCHED THEN INSERT ({FailedMessageColumnList}) } } - public async Task InsertGroups(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertGroups(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { var maxRowsPerStatement = MaxRowsPerStatement(4); foreach (var chunk in rows.Chunk(maxRowsPerStatement)) @@ -55,7 +55,7 @@ WHEN NOT MATCHED THEN INSERT ([FailedMessageUniqueId], [GroupId], [Title], [Type } } - public async Task InsertMissingKnownEndpoints(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertMissingKnownEndpoints(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { var maxRowsPerStatement = MaxRowsPerStatement(5); foreach (var chunk in rows.Chunk(maxRowsPerStatement)) diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerRetryBatchSqlDialect.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerRetryBatchSqlDialect.cs index 0774e5c64b..1e8de7a9c5 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerRetryBatchSqlDialect.cs +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerRetryBatchSqlDialect.cs @@ -6,7 +6,7 @@ namespace ServiceControl.Persistence.EFCore.SqlServer; class SqlServerRetryBatchSqlDialect : SqlServerDialect, IRetryBatchSqlDialect { - public async Task InsertMissingRetryClaims(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken) + public async Task InsertMissingRetryClaims(ServiceControlDbContext dbContext, IReadOnlyList rows, CancellationToken cancellationToken = default) { var maxRowsPerStatement = MaxRowsPerStatement(3); foreach (var chunk in rows.Chunk(maxRowsPerStatement)) diff --git a/src/ServiceControl.RavenDB/.editorconfig b/src/ServiceControl.RavenDB/.editorconfig index 522a310bff..ff993b49bb 100644 --- a/src/ServiceControl.RavenDB/.editorconfig +++ b/src/ServiceControl.RavenDB/.editorconfig @@ -2,9 +2,3 @@ # Justification: ServiceControl app has no synchronization context 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.RavenDB/EmbeddedDatabase.cs b/src/ServiceControl.RavenDB/EmbeddedDatabase.cs index f0026a51d7..a2079816eb 100644 --- a/src/ServiceControl.RavenDB/EmbeddedDatabase.cs +++ b/src/ServiceControl.RavenDB/EmbeddedDatabase.cs @@ -165,7 +165,7 @@ void OnServerProcessExited(object? sender, ServerProcessExitedEventArgs _) } } - public async Task Connect(CancellationToken cancellationToken) + public async Task Connect(CancellationToken cancellationToken = default) { var dbOptions = new DatabaseOptions(configuration.Name) { @@ -180,16 +180,16 @@ public async Task Connect(CancellationToken cancellationToken) return store; } - public async Task DeleteDatabase(string dbName) + public async Task DeleteDatabase(string dbName, CancellationToken cancellationToken = default) { using var store = await EmbeddedServer.Instance.GetDocumentStoreAsync(new DatabaseOptions(dbName) { SkipCreatingDatabase = true - }); - await store.Maintenance.Server.SendAsync(new DeleteDatabasesOperation(dbName, true)); + }, cancellationToken); + await store.Maintenance.Server.SendAsync(new DeleteDatabasesOperation(dbName, true), cancellationToken); } - public async Task Stop(CancellationToken cancellationToken) + public async Task Stop(CancellationToken cancellationToken = default) { logger.LogDebug("Stopping RavenDB server"); EmbeddedServer.Instance.ServerProcessExited -= OnServerProcessExited; From 2cd5db786f6eb9b7c98fc99aecda0318efca8c6a Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 17:12:51 +1000 Subject: [PATCH 09/12] Drop redundant cancellation token arguments from the test projects Retires the PS0003, PS0006, PS0008, PS0017 and PS0019 debt across the test projects, and corrects the justification on what is left. Phase 7 of the propagation work. Stacked on the persistence and licensing change, whose optional parameters this depends on. Because the production tokens are now optional, 139 call sites that passed the default literal do not need CancellationToken.None in its place: they drop the argument entirely and read as ordinary calls. Tests that genuinely exercise cancellation keep using [Test, CancelAfter(...)] with the token NUnit injects. Test methods that take that injected token gain "= default", which is what PS0003 asks for on a non-private member and does not affect NUnit's injection. The remaining PS0018 and PS0013 blocks are reworded. The previous text claimed these were accepted exceptions because the rule would demand a token on every [Test] method. That is not what happens: Particular.Analyzers already exempts NUnit test methods, and measuring it showed 301 of the 306 sites are ordinary helper methods. The blocks now say what is actually left, which is threading tokens through the helper chain and the callback shapes the NServiceBus scenario API fixes, so it reads as scheduled work rather than a settled decision. Two shared container helpers rename ct to cancellationToken and rethrow their own cancellation before mapping container start failures. WatchdogTests does the same around its expected-exception assertion. --- .../.editorconfig | 3 - .../AuditQuery_Tests.cs | 24 +++--- ...tThroughputCollectorHostedService_Tests.cs | 36 ++++----- ...erThroughputCollectorHostedServiceTests.cs | 18 ++--- .../Infrastructure/DataStoreBuilder.cs | 5 +- .../Infrastructure/FakeAuditCountApi.cs | 2 +- .../FakeBrokerThroughputQuery.cs | 6 +- .../Infrastructure/FakeConfigurationApi.cs | 6 +- .../Infrastructure/FakeEndpointApi.cs | 2 +- .../MonitoringService_Tests.cs | 20 ++--- ...AdditionalEnvironmentDataProvider_Tests.cs | 3 +- ...oughputCollector_GenerationStatus_Tests.cs | 9 ++- .../ThroughputCollector_Report_Dates_Tests.cs | 3 +- ...tor_Report_EnvironmentInformation_Tests.cs | 17 ++-- ...oughputCollector_Report_Indicator_Tests.cs | 5 +- ...hroughputCollector_Report_Masking_Tests.cs | 7 +- ...ughputCollector_Report_Throughput_Tests.cs | 27 ++++--- ...utCollector_SanitizedNameGrouping_Tests.cs | 20 ++--- ...lector_ThroughputSumary_Indicator_Tests.cs | 9 ++- ...ughputCollector_ThroughputSummary_Tests.cs | 14 ++-- .../.editorconfig | 6 -- .../MessageFailures/FailedErrorsController.cs | 2 +- .../FailedMessageRetriesController.cs | 2 +- .../When_a_message_fails_to_import.cs | 2 +- .../.editorconfig | 16 ++-- ...KnownEndpointPersistenceQueryController.cs | 3 +- .../FailedMessageExtensions.cs | 5 +- .../Groups/When_a_group_is_archived.cs | 2 +- .../Groups/When_a_group_is_retried.cs | 2 +- ...e_fails_twice_with_different_exceptions.cs | 2 +- .../ErrorImportPerformanceTests.cs | 2 +- .../When_a_invalid_id_is_sent_to_retry.cs | 2 +- .../When_a_message_has_failed.cs | 2 +- .../When_a_retry_fails_to_be_sent.cs | 2 +- ..._for_a_empty_body_message_is_successful.cs | 2 +- ...When_a_retry_for_a_failed_message_fails.cs | 4 +- ...etry_for_a_failed_message_is_successful.cs | 10 +-- .../When_all_messages_are_retried.cs | 2 +- .../When_a_message_is_retried.cs | 2 +- ...ge_is_retried_and_succeeds_with_a_reply.cs | 4 +- .../.editorconfig | 1 - ...When_critical_storage_threshold_reached.cs | 2 +- .../.editorconfig | 1 - .../BodyStorage/BodyStorageEnricherTests.cs | 4 +- .../.editorconfig | 4 - .../WatchdogTests.cs | 4 + .../.editorconfig | 9 ++- .../When_requesting_a_message_body.cs | 2 +- .../When_remote_instance_is_not_reachable.cs | 2 +- .../Recoverability/WhenRetrying.cs | 8 +- .../WhenRetryingSameMessageMultipleTimes.cs | 2 +- .../Recoverability/WhenRetryingWithEdit.cs | 2 +- ...e_retry_audit_is_sent_to_audit_instance.cs | 2 +- ...issuing_retry_by_specifying_instance_id.cs | 2 +- .../HttpExtensionsMultiinstance.cs | 29 +++---- .../.editorconfig | 6 +- .../.editorconfig | 7 +- .../PostgreSqlSharedContainer.cs | 16 ++-- .../.editorconfig | 6 +- .../.editorconfig | 7 +- .../SqlServerSharedContainer.cs | 16 ++-- .../.editorconfig | 16 ++-- .../EFCore/EFCoreExtensionMethodTests.cs | 2 +- .../EFCore/LicensingDataStoreEFTests.cs | 49 +++++------ .../EndpointSettingsStoreTests.cs | 4 +- .../FakeDomainEvents.cs | 2 +- .../Recoverability/EditMessageTests.cs | 2 +- .../ReturnToSenderDequeuerTests.cs | 4 +- .../RetryStateTests.cs | 2 +- .../Throughput/AuditServiceMetadataTests.cs | 7 +- .../Throughput/BrokerMetadataTests.cs | 11 +-- .../Throughput/EndpointsTests.cs | 81 +++++++++---------- .../Throughput/ReportMasksTests.cs | 11 +-- .../TrialLicenseDataProviderTests.cs | 4 +- src/ServiceControl.UnitTests/.editorconfig | 2 - .../Licensing/ActiveLicenseTests.cs | 4 +- .../EndpointInstanceMonitoringTests.cs | 2 +- ...tEndpointSettingsSyncHostedServiceTests.cs | 6 +- 78 files changed, 328 insertions(+), 323 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/.editorconfig b/src/Particular.LicensingComponent.UnitTests/.editorconfig index 474aec5159..0a6ae3dffe 100644 --- a/src/Particular.LicensingComponent.UnitTests/.editorconfig +++ b/src/Particular.LicensingComponent.UnitTests/.editorconfig @@ -6,7 +6,4 @@ 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.PS0006.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none diff --git a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs index b9318ecc88..0c35b77398 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs @@ -34,7 +34,7 @@ public async Task Should_return_known_endpoints_if_any() var auditQuery = new AuditQuery(NullLogger.Instance, new EndpointsApi_ReturningTwoEndpoints(), new FakeAuditCountApi(), new FakeConfigurationApi()); //Act - var endpoints = (await auditQuery.GetKnownEndpoints(default)).ToList(); + var endpoints = (await auditQuery.GetKnownEndpoints()).ToList(); //Assert Assert.That(endpoints, Is.Not.Null, "Endpoints should be found"); @@ -53,7 +53,7 @@ public async Task Should_return_audit_remotes() var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), new ConfigurationApi_ReturningOneValidAuditConfig()); //Act - var remotes = await auditQuery.GetAuditRemotes(default); + var remotes = await auditQuery.GetAuditRemotes(); //Assert Assert.That(remotes, Is.Not.Null, "Remotes should be found"); @@ -81,7 +81,7 @@ public async Task Should_return_successful_audit_connection_if_instances_exist_a var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), new ConfigurationApi_ReturningOneValidAuditConfig()); //Act - var connectionSettingsResult = await auditQuery.TestAuditConnection(default); + var connectionSettingsResult = await auditQuery.TestAuditConnection(); //Assert Assert.That(connectionSettingsResult, Is.Not.Null, "connectionSettingsResult should be returned"); @@ -103,7 +103,7 @@ public async Task Should_return_diagnostics_and_no_errors_when_no_remotes_define var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), confiApi); //Act - var connectionSettingsResult = await auditQuery.TestAuditConnection(default); + var connectionSettingsResult = await auditQuery.TestAuditConnection(); //Assert Assert.That(connectionSettingsResult, Is.Not.Null, "connectionSettingsResult should be returned"); @@ -128,7 +128,7 @@ public async Task Should_always_return_diagnostics_and_relevant_errors_when_inva var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), confiApi); //Act - var connectionSettingsResult = await auditQuery.TestAuditConnection(default); + var connectionSettingsResult = await auditQuery.TestAuditConnection(); //Assert Assert.That(connectionSettingsResult, Is.Not.Null, "connectionSettingsResult should be returned"); @@ -148,7 +148,7 @@ public async Task Should_return_correct_audit_count() var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new AuditCountApi_ReturningThreeAuditCounts(), new FakeConfigurationApi()); //Act - var auditCount = await auditQuery.GetAuditCountForEndpoint("Endpoint1", default); + var auditCount = await auditQuery.GetAuditCountForEndpoint("Endpoint1"); Assert.That(auditCount, Is.Not.Null, "AuditCount should be returned"); Assert.That(auditCount.Count, Is.EqualTo(3), "Invalid number of audit counts"); @@ -156,7 +156,7 @@ public async Task Should_return_correct_audit_count() class ConfigurationApi_ReturningOneValidAuditConfig : IConfigurationApi { - public Task GetConfig(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task GetRemoteConfigs(CancellationToken cancellationToken = default) { @@ -165,12 +165,12 @@ public Task GetRemoteConfigs(CancellationToken cancellati return Task.FromResult([remote]); } - public Task GetUrls(string baseUrl, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } class ConfigurationApi_Configurable : IConfigurationApi { - public Task GetConfig(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task GetRemoteConfigs(CancellationToken cancellationToken = default) { @@ -184,7 +184,7 @@ public Task GetRemoteConfigs(CancellationToken cancellati return Task.FromResult([remote]); } - public Task GetUrls(string baseUrl, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool ReturnAuditConfig { get; set; } public string RemoteStatus { get; set; } @@ -194,7 +194,7 @@ public Task GetRemoteConfigs(CancellationToken cancellati class EndpointsApi_ReturningTwoEndpoints : IEndpointsApi { - public Task> GetEndpoints(CancellationToken cancellationToken) + public Task> GetEndpoints(CancellationToken cancellationToken = default) { return Task.FromResult>([ new Endpoint { Id = Guid.NewGuid(), Name = "Endpoint1" }, @@ -206,7 +206,7 @@ public Task> GetEndpoints(CancellationToken cancellationToken) class AuditCountApi_ReturningThreeAuditCounts : IAuditCountApi { - public async Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken) + public async Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken = default) { var auditCounts = new List { diff --git a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs index 88a22d2cfd..3c2fdb5474 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs @@ -133,7 +133,7 @@ await Task.Run(async () => } while (!token.IsCancellationRequested); }); - Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Audit, default); + Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Audit); //Assert Assert.That(foundEndpoint, Is.Not.Null, $"Expected endpoint {endpointName} not found."); @@ -181,9 +181,9 @@ await Task.Run(async () => }); await auditThroughputCollectorHostedService.StopAsync(token2); - Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Audit, default); + Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Audit); IDictionary> foundEndpointThroughput = - await DataStore.GetEndpointThroughputByQueueName([endpointName], default); + await DataStore.GetEndpointThroughputByQueueName([endpointName]); ThroughputData[] throughputData = foundEndpointThroughput[endpointName].ToArray(); // Assert @@ -210,19 +210,19 @@ class AuditQuery_NoAuditRemotes : IAuditQuery public Func ValidRemoteInstances => r => true; public Task> GetAuditCountForEndpoint(string endpointUrlName, - CancellationToken cancellationToken) => Task.FromResult>([]); + CancellationToken cancellationToken = default) => Task.FromResult>([]); - public Task> GetAuditRemotes(CancellationToken cancellationToken) => + public Task> GetAuditRemotes(CancellationToken cancellationToken = default) => Task.FromResult>([]); - public Task> GetKnownEndpoints(CancellationToken cancellationToken) + public Task> GetKnownEndpoints(CancellationToken cancellationToken = default) { InstanceParameter = true; return Task.FromResult>([]); } - public Task TestAuditConnection(CancellationToken cancellationToken) => + public Task TestAuditConnection(CancellationToken cancellationToken = default) => Task.FromResult( new ConnectionSettingsTestResult { ConnectionSuccessful = true, ConnectionErrorMessages = [] }); @@ -243,23 +243,23 @@ public AuditQuery_WithOneEndpoint(string endpointName, long throughputCount, Dat public Func ValidRemoteInstances => r => true; public Task> GetAuditCountForEndpoint(string endpointUrlName, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { var auditCount = new AuditCount { UtcDate = ThroughputDate, Count = ThroughputCount }; return Task.FromResult(new List { auditCount }.AsEnumerable()); } - public Task> GetAuditRemotes(CancellationToken cancellationToken) => + public Task> GetAuditRemotes(CancellationToken cancellationToken = default) => Task.FromResult>([]); - public Task> GetKnownEndpoints(CancellationToken cancellationToken) + public Task> GetKnownEndpoints(CancellationToken cancellationToken = default) { var scEndpoint = new ServiceControlEndpoint { Name = EndpointName, HeartbeatsEnabled = true }; return Task.FromResult>([scEndpoint]); } - public Task TestAuditConnection(CancellationToken cancellationToken) => + public Task TestAuditConnection(CancellationToken cancellationToken = default) => Task.FromResult( new ConnectionSettingsTestResult { ConnectionSuccessful = true, ConnectionErrorMessages = [] }); @@ -275,19 +275,19 @@ class AuditQuery_ThrowingAnExceptionOnKnownEndpointsCall : IAuditQuery public Func ValidRemoteInstances => r => true; public Task> GetAuditCountForEndpoint(string endpointUrlName, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task> GetAuditRemotes(CancellationToken cancellationToken) => + public Task> GetAuditRemotes(CancellationToken cancellationToken = default) => Task.FromResult>([]); - public Task> GetKnownEndpoints(CancellationToken cancellationToken) + public Task> GetKnownEndpoints(CancellationToken cancellationToken = default) { InstanceParameter = true; throw new Exception("Oops"); } - public Task TestAuditConnection(CancellationToken cancellationToken) => + public Task TestAuditConnection(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool InstanceParameter { get; set; } @@ -303,17 +303,17 @@ class BrokerThroughputQuery_WithSanitization : IBrokerThroughputQuery public KeyDescriptionPair[] Settings => throw new NotImplementedException(); - public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken) => + public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool HasInitialisationErrors(out string errorMessage) => throw new NotImplementedException(); public void Initialize(ReadOnlyDictionary settings) => throw new NotImplementedException(); public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) { diff --git a/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs b/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs index 2b5506a498..69ed2e4d8c 100644 --- a/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs +++ b/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs @@ -138,7 +138,7 @@ public void Initialize(ReadOnlyDictionary settings) } public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { GetGetThroughputPerDay++; @@ -148,7 +148,7 @@ public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue } public async IAsyncEnumerable GetQueueNames( - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (GetQueueNamesCalls++ % 2 == 0) { @@ -168,7 +168,7 @@ public async IAsyncEnumerable GetQueueNames( public KeyDescriptionPair[] Settings { get; } = []; public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; public string SanitizedEndpointNameCleanser(string endpointName) => endpointName; @@ -187,7 +187,7 @@ public void Initialize(ReadOnlyDictionary settings) } public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.CompletedTask; @@ -195,7 +195,7 @@ public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue } public async IAsyncEnumerable GetQueueNames( - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { yield return new DefaultBrokerQueue("sales@one") { SanitizedName = "sales" }; yield return new DefaultBrokerQueue("sales@two") { SanitizedName = "sales" }; @@ -211,7 +211,7 @@ public async IAsyncEnumerable GetQueueNames( public KeyDescriptionPair[] Settings { get; } = []; public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; public string SanitizedEndpointNameCleanser(string endpointName) => endpointName; @@ -232,7 +232,7 @@ public void Initialize(ReadOnlyDictionary settings) } public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.CompletedTask; @@ -242,7 +242,7 @@ public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue } public async IAsyncEnumerable GetQueueNames( - [EnumeratorCancellation] CancellationToken cancellationToken) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { yield return new DefaultBrokerQueue("marketing"); yield return new DefaultBrokerQueue("customer"); @@ -256,7 +256,7 @@ public async IAsyncEnumerable GetQueueNames( public KeyDescriptionPair[] Settings { get; } = []; public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; public string SanitizedEndpointNameCleanser(string endpointName) => endpointName; diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs index d2b1b1c93c..e579cf8f9c 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Contracts; using Persistence; @@ -122,7 +123,7 @@ public async Task Build() { foreach (Endpoint endpoint in endpoints) { - await store.SaveEndpoint(endpoint, default); + await store.SaveEndpoint(endpoint); } ; @@ -132,7 +133,7 @@ public async Task Build() foreach (ThroughputData throughput in throughputList) { await store.RecordEndpointThroughput(endpointId.Name, throughput.ThroughputSource, - throughput.Select(entry => new EndpointDailyThroughput(entry.Key, entry.Value)).ToList(), default); + throughput.Select(entry => new EndpointDailyThroughput(entry.Key, entry.Value)).ToList()); } } } diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeAuditCountApi.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeAuditCountApi.cs index a130d66264..e9c715e857 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeAuditCountApi.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeAuditCountApi.cs @@ -8,6 +8,6 @@ class FakeAuditCountApi : IAuditCountApi { - public Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeBrokerThroughputQuery.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeBrokerThroughputQuery.cs index 86e97d3c5b..6beb384f0a 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeBrokerThroughputQuery.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeBrokerThroughputQuery.cs @@ -17,17 +17,17 @@ class FakeBrokerThroughputQuery : IBrokerThroughputQuery public KeyDescriptionPair[] Settings => throw new NotImplementedException(); - public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken) => + public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool HasInitialisationErrors(out string errorMessage) => throw new NotImplementedException(); public void Initialize(ReadOnlyDictionary settings) => throw new NotImplementedException(); public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; public string SanitizedEndpointNameCleanser(string endpointName) => endpointName; diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeConfigurationApi.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeConfigurationApi.cs index 9f3a7fc3a0..d7d0893be2 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeConfigurationApi.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeConfigurationApi.cs @@ -8,8 +8,8 @@ class FakeConfigurationApi : IConfigurationApi { - public Task GetConfig(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task GetRemoteConfigs(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task GetUrls(string baseUrl, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task GetRemoteConfigs(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeEndpointApi.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeEndpointApi.cs index 3f2a66a6c0..802db6d5f2 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeEndpointApi.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/FakeEndpointApi.cs @@ -9,6 +9,6 @@ class FakeEndpointApi : IEndpointsApi { - public Task> GetEndpoints(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task> GetEndpoints(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } diff --git a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs index c59a7b63c9..aa13b9852f 100644 --- a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs @@ -37,12 +37,12 @@ public async Task Should_record_new_endpoint_and_throughput() }; byte[] messageBytes = JsonSerializer.SerializeToUtf8Bytes(message); - await configuration.MonitoringService.RecordMonitoringThroughput(messageBytes, default); + await configuration.MonitoringService.RecordMonitoringThroughput(messageBytes); // Act - Endpoint foundEndpoint = await DataStore.GetEndpoint("Endpoint1", ThroughputSource.Monitoring, default); + Endpoint foundEndpoint = await DataStore.GetEndpoint("Endpoint1", ThroughputSource.Monitoring); IDictionary> foundEndpointThroughput = - await DataStore.GetEndpointThroughputByQueueName(["Endpoint1"], default); + await DataStore.GetEndpointThroughputByQueueName(["Endpoint1"]); ThroughputData[] throughputData = foundEndpointThroughput["Endpoint1"].ToArray(); // Assert @@ -84,11 +84,11 @@ public async Task Should_sanitize_endpoint_name() var monitoringService = new MonitoringService(DataStore, new BrokerThroughputQuery_WithSanitization()); byte[] messageBytes = JsonSerializer.SerializeToUtf8Bytes(message); - await monitoringService.RecordMonitoringThroughput(messageBytes, default); + await monitoringService.RecordMonitoringThroughput(messageBytes); string endpointNameSanitized = "e-ndpoint-1"; // Act - Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Monitoring, default); + Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Monitoring); // Assert Assert.That(foundEndpoint, Is.Not.Null, $"Expected endpoint {endpointName} not found."); @@ -109,7 +109,7 @@ public async Task Should_return_successful_monitoring_connection_and_diagnostics // Act ConnectionSettingsTestResult connectionSettingsResult = - await configuration.MonitoringService.TestMonitoringConnection(default); + await configuration.MonitoringService.TestMonitoringConnection(); // Assert Assert.That(connectionSettingsResult, Is.Not.Null, "connectionSettingsResult should be returned"); @@ -139,7 +139,7 @@ public async Task Should_return_error_monitoring_connection_and_diagnostics_if_n // Act ConnectionSettingsTestResult connectionSettingsResult = - await configuration.MonitoringService.TestMonitoringConnection(default); + await configuration.MonitoringService.TestMonitoringConnection(); // Assert Assert.That(connectionSettingsResult, Is.Not.Null, "connectionSettingsResult should be returned"); @@ -169,17 +169,17 @@ class BrokerThroughputQuery_WithSanitization : IBrokerThroughputQuery public KeyDescriptionPair[] Settings => throw new NotImplementedException(); - public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken) => + public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool HasInitialisationErrors(out string errorMessage) => throw new NotImplementedException(); public void Initialize(ReadOnlyDictionary settings) => throw new NotImplementedException(); public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) { diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs index 8d42beea1d..732a6f8122 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs @@ -1,6 +1,7 @@ namespace Particular.LicensingComponent.UnitTests; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -22,7 +23,7 @@ public async Task Should_include_additional_environment_data_in_throughput_repor { // Arrange // Act - var report = await ThroughputCollector.GenerateThroughputReport(null, null, default); + var report = await ThroughputCollector.GenerateThroughputReport(null, null); // Assert Assert.That(report, Is.Not.Null); Assert.That(report.ReportData, Is.Not.Null); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_GenerationStatus_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_GenerationStatus_Tests.cs index 96173bddb8..dc753860ac 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_GenerationStatus_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_GenerationStatus_Tests.cs @@ -1,6 +1,7 @@ namespace Particular.LicensingComponent.UnitTests; using System; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -25,7 +26,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var reportGenerationState = await ThroughputCollector.GetReportGenerationState(default); + var reportGenerationState = await ThroughputCollector.GetReportGenerationState(); // Assert using (Assert.EnterMultipleScope()) @@ -45,7 +46,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var reportGenerationState = await ThroughputCollector.GetReportGenerationState(default); + var reportGenerationState = await ThroughputCollector.GetReportGenerationState(); // Assert using (Assert.EnterMultipleScope()) @@ -66,7 +67,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var reportGenerationState = await ThroughputCollector.GetReportGenerationState(default); + var reportGenerationState = await ThroughputCollector.GetReportGenerationState(); // Assert using (Assert.EnterMultipleScope()) @@ -87,7 +88,7 @@ await DataStore.CreateBuilder().AddEndpoint() .Build(); // Act - var reportGenerationState = await ThroughputCollector.GetReportGenerationState(default); + var reportGenerationState = await ThroughputCollector.GetReportGenerationState(); // Assert using (Assert.EnterMultipleScope()) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Dates_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Dates_Tests.cs index 096fed9bfe..6978ffa15b 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Dates_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Dates_Tests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -41,7 +42,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert var minDateInReport = new DateTimeOffset(minDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs index c79c987081..200ac65fa8 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs @@ -1,6 +1,7 @@ namespace Particular.LicensingComponent.UnitTests; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Contracts; @@ -27,7 +28,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -58,7 +59,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -82,7 +83,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -113,7 +114,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -145,7 +146,7 @@ await DataStore.CreateBuilder() // Act var spVersion = "5.1"; - var report = await ThroughputCollector.GenerateThroughputReport(spVersion, null, default); + var report = await ThroughputCollector.GenerateThroughputReport(spVersion, null); // Assert Assert.That(report, Is.Not.Null); @@ -170,14 +171,14 @@ await DataStore.CreateBuilder() var expectedBrokerVersion = "1.2"; var expectedScopeType = "testingScope"; - await DataStore.SaveBrokerMetadata(new BrokerMetadata(expectedScopeType, new Dictionary { [EnvironmentDataType.BrokerVersion.ToString()] = expectedBrokerVersion }), default); + await DataStore.SaveBrokerMetadata(new BrokerMetadata(expectedScopeType, new Dictionary { [EnvironmentDataType.BrokerVersion.ToString()] = expectedBrokerVersion })); var expectedAuditVersionSummary = new Dictionary { ["4.3.6"] = 2 }; var expectedAuditTransportSummary = new Dictionary { ["AzureServiceBus"] = 2 }; - await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata(expectedAuditVersionSummary, expectedAuditTransportSummary), default); + await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata(expectedAuditVersionSummary, expectedAuditTransportSummary)); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Indicator_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Indicator_Tests.cs index 68cab4dac4..9082429111 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Indicator_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Indicator_Tests.cs @@ -1,6 +1,7 @@ namespace Particular.LicensingComponent.UnitTests; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Contracts; using Infrastructure; @@ -31,7 +32,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -53,7 +54,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Masking_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Masking_Tests.cs index e3bdb7db7a..d589dcaab3 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Masking_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Masking_Tests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -28,10 +29,10 @@ await DataStore.CreateBuilder() .AddEndpoint("Endpoint3", sources: [ThroughputSource.Broker]).WithThroughput(days: 2) .Build(); var expectedReportMasks = new List { "Endpoint1" }; - await DataStore.SaveReportMasks(expectedReportMasks, default); + await DataStore.SaveReportMasks(expectedReportMasks); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -56,7 +57,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs index 6603abcad0..ac4503a4ca 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Contracts; using Infrastructure; @@ -37,7 +38,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -67,7 +68,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -94,7 +95,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -112,7 +113,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -132,7 +133,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -170,7 +171,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -197,7 +198,7 @@ public async Task Should_return_correct_throughput_in_report_when_endpoint_has_n await DataStore.CreateBuilder().AddEndpoint().Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -224,7 +225,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -258,7 +259,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + var report = await ThroughputCollector.GenerateThroughputReport("", null); // Assert Assert.That(report, Is.Not.Null); @@ -298,18 +299,18 @@ await DataStore.CreateBuilder() .Build(); var expectedReportMasks = new List { "Endpoint1" }; - await DataStore.SaveReportMasks(expectedReportMasks, default); + await DataStore.SaveReportMasks(expectedReportMasks); var expectedBrokerVersion = "1.2"; var expectedScopeType = "testingScope"; - await DataStore.SaveBrokerMetadata(new BrokerMetadata(expectedScopeType, new Dictionary { [EnvironmentDataType.BrokerVersion.ToString()] = expectedBrokerVersion }), default); + await DataStore.SaveBrokerMetadata(new BrokerMetadata(expectedScopeType, new Dictionary { [EnvironmentDataType.BrokerVersion.ToString()] = expectedBrokerVersion })); var expectedAuditVersionSummary = new Dictionary { ["4.3.6"] = 2 }; var expectedAuditTransportSummary = new Dictionary { ["AzureServiceBus"] = 2 }; - await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata(expectedAuditVersionSummary, expectedAuditTransportSummary), default); + await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata(expectedAuditVersionSummary, expectedAuditTransportSummary)); // Act - var report = await ThroughputCollector.GenerateThroughputReport("2.3.1", new DateTime(2024, 4, 25), default); + var report = await ThroughputCollector.GenerateThroughputReport("2.3.1", new DateTime(2024, 4, 25)); var reportString = System.Text.Json.JsonSerializer.Serialize(report, SerializationOptions.IndentedWithNoEscaping); // Assert diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs index 703e842cea..24cc1783b0 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs @@ -38,7 +38,7 @@ await DataStore.CreateBuilder() var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); // Act - var summary = await throughputCollector.GetThroughputSummary(default); + var summary = await throughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -64,7 +64,7 @@ await DataStore.CreateBuilder() var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); // Act - var report = await throughputCollector.GenerateThroughputReport(null, null, default); + var report = await throughputCollector.GenerateThroughputReport(null, null); // Assert Assert.That(report, Is.Not.Null); @@ -91,7 +91,7 @@ await DataStore.CreateBuilder() var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); // Act - var summary = await throughputCollector.GetThroughputSummary(default); + var summary = await throughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -117,7 +117,7 @@ await DataStore.CreateBuilder() var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); // Act - var report = await throughputCollector.GenerateThroughputReport(null, null, default); + var report = await throughputCollector.GenerateThroughputReport(null, null); // Assert Assert.That(report, Is.Not.Null); @@ -136,17 +136,17 @@ class BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse : IBrokerThroughpu public KeyDescriptionPair[] Settings => throw new NotImplementedException(); - public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken) => + public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool HasInitialisationErrors(out string errorMessage) => throw new NotImplementedException(); public void Initialize(ReadOnlyDictionary settings) => throw new NotImplementedException(); public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; @@ -163,17 +163,17 @@ class BrokerThroughputQuery_WithNoSanitizedNameCleanse : IBrokerThroughputQuery public KeyDescriptionPair[] Settings => throw new NotImplementedException(); - public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken) => + public IAsyncEnumerable GetQueueNames(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public IAsyncEnumerable GetThroughputPerDay(IBrokerQueue brokerQueue, DateOnly startDate, - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool HasInitialisationErrors(out string errorMessage) => throw new NotImplementedException(); public void Initialize(ReadOnlyDictionary settings) => throw new NotImplementedException(); public Task<(bool Success, List Errors, string Diagnostics)> TestConnection( - CancellationToken cancellationToken) => throw new NotImplementedException(); + CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string SanitizeEndpointName(string endpointName) => endpointName; diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSumary_Indicator_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSumary_Indicator_Tests.cs index ec54124799..a6922f683c 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSumary_Indicator_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSumary_Indicator_Tests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -36,7 +37,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -54,14 +55,14 @@ await DataStore.CreateBuilder() .AddEndpoint("Endpoint1", sources: [ThroughputSource.Broker]).WithThroughput(days: 2) .AddEndpoint("Endpoint1", sources: [ThroughputSource.Monitoring]).WithThroughput(days: 2) .Build(); - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Act List endpointsWithUpdates = [new UpdateUserIndicator() { Name = "Endpoint1", UserIndicator = userIndicator }]; - await ThroughputCollector.UpdateUserIndicatorsOnEndpoints(endpointsWithUpdates, default); + await ThroughputCollector.UpdateUserIndicatorsOnEndpoints(endpointsWithUpdates); // Assert - var updatedEndpoints = await DataStore.GetAllEndpoints(true, default); + var updatedEndpoints = await DataStore.GetAllEndpoints(true); Assert.That(updatedEndpoints, Is.Not.Null); Assert.That(updatedEndpoints.Count, Is.EqualTo(2)); diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index b3007fe41f..e012779408 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -35,7 +35,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -55,7 +55,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -73,7 +73,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -93,7 +93,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -125,7 +125,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -183,7 +183,7 @@ public async Task Should_return_correct_max_daily_throughput_in_summary_when_end await DataStore.CreateBuilder().AddEndpoint().Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); @@ -204,7 +204,7 @@ await DataStore.CreateBuilder() .Build(); // Act - var summary = await ThroughputCollector.GetThroughputSummary(default); + var summary = await ThroughputCollector.GetThroughputSummary(); // Assert Assert.That(summary, Is.Not.Null); diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/.editorconfig b/src/ServiceControl.AcceptanceTests.RavenDB/.editorconfig index bbcb303765..5f68a610b3 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/.editorconfig +++ b/src/ServiceControl.AcceptanceTests.RavenDB/.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.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs index c22a6f6e7e..2a6ce095f0 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedErrorsController.cs @@ -20,7 +20,7 @@ public class FailedErrorsController(IRavenSessionProvider sessionProvider, Impor { [Route("failederrors/count")] [HttpGet] - public async Task GetFailedErrorsCount(CancellationToken cancellationToken) + public async Task GetFailedErrorsCount(CancellationToken cancellationToken = default) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); var query = diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs index b511a9db28..705806daba 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs @@ -19,7 +19,7 @@ public class FailedMessageRetriesController(IRavenSessionProvider sessionProvide { [Route("failedmessageretries/count")] [HttpGet] - public async Task GetFailedMessageRetriesCount(CancellationToken cancellationToken) + public async Task GetFailedMessageRetriesCount(CancellationToken cancellationToken = default) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); await session.Query().Statistics(out var stats).ToListAsync(cancellationToken); diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs index 5780d4e716..e1749990c0 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs @@ -77,7 +77,7 @@ public async Task It_can_be_reimported() class MessageFailedHandler(MyContext scenarioContext) : IDomainHandler { - public Task Handle(MessageFailed domainEvent, CancellationToken cancellationToken) + public Task Handle(MessageFailed domainEvent, CancellationToken cancellationToken = default) { scenarioContext.MessageFailedEventPublished = true; return Task.CompletedTask; diff --git a/src/ServiceControl.AcceptanceTests/.editorconfig b/src/ServiceControl.AcceptanceTests/.editorconfig index 4fcb63e0be..a32084aea3 100644 --- a/src/ServiceControl.AcceptanceTests/.editorconfig +++ b/src/ServiceControl.AcceptanceTests/.editorconfig @@ -3,14 +3,14 @@ # 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.PS0013.severity = none -dotnet_diagnostic.PS0017.severity = none -dotnet_diagnostic.PS0018.severity = none - # Timezone debt: these DateTime values are Kind=Unspecified or Local, so the implicit cast to # DateTimeOffset uses the build agent's offset. Each needs individual review before removing. dotnet_diagnostic.PS0022.severity = none + +# Justification: these are the test helpers the NServiceBus.AcceptanceTesting scenario API drives. +# It calls Done/When callbacks and IComponentBehavior/ComponentRunner without a CancellationToken, +# so a token added here could only ever be CancellationToken.None at every call site, which tests +# nothing. Tests that genuinely exercise cancellation use [Test, CancelAfter(...)] with the token +# NUnit injects, and forward that. Helpers reachable with a real token do take one. +dotnet_diagnostic.PS0013.severity = none +dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/KnownEndpointPersistenceQueryController.cs b/src/ServiceControl.AcceptanceTests/Monitoring/KnownEndpointPersistenceQueryController.cs index 1790818a34..f8b25b5206 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/KnownEndpointPersistenceQueryController.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/KnownEndpointPersistenceQueryController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.AcceptanceTests.Monitoring { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using ServiceControl.Persistence; @@ -11,6 +12,6 @@ public class KnownEndpointPersistenceQueryController(IMonitoringDataStore dataSt { [Route("test/knownendpoints/query")] [HttpGet] - public async Task> GetKnownEndpoints() => await dataStore.GetAllKnownEndpoints(); + public async Task> GetKnownEndpoints(CancellationToken cancellationToken = default) => await dataStore.GetAllKnownEndpoints(cancellationToken); } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs index 262991a3b3..45ec710995 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ServiceControl.AcceptanceTesting; using ServiceControl.AcceptanceTests; @@ -7,10 +8,10 @@ public static class FailedMessageExtensions { - internal static async Task GetOnlyFailedUnresolvedMessageId(this AcceptanceTest test) + internal static async Task GetOnlyFailedUnresolvedMessageId(this AcceptanceTest test, CancellationToken cancellationToken = default) { var allFailedMessages = - await test.TryGet>($"/api/errors/?status=unresolved"); + await test.TryGet>($"/api/errors/?status=unresolved", cancellationToken: cancellationToken); if (!allFailedMessages.HasResult) { return null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs index 2e3455cc29..b1db1044bc 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_archived.cs @@ -165,7 +165,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Only_unresolved_issues_should_be_archived(CancellationToken cancellationToken) + public async Task Only_unresolved_issues_should_be_archived(CancellationToken cancellationToken = default) { await Define() .WithEndpoint(b => b.When(async bus => diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs index 3e32fba255..df2ba2a532 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_group_is_retried.cs @@ -17,7 +17,7 @@ class When_a_group_is_retried : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Only_unresolved_issues_should_be_retried(CancellationToken cancellationToken) + public async Task Only_unresolved_issues_should_be_retried(CancellationToken cancellationToken = default) { FailedMessage messageToBeRetriedAsPartOfGroupRetry = null; FailedMessage messageToBeArchived = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_message_fails_twice_with_different_exceptions.cs b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_message_fails_twice_with_different_exceptions.cs index 3668a8b7f4..ca914f59b2 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_message_fails_twice_with_different_exceptions.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/Groups/When_a_message_fails_twice_with_different_exceptions.cs @@ -19,7 +19,7 @@ class When_a_message_fails_twice_with_different_exceptions : AcceptanceTest { [Test] [CancelAfter(180_000)] - public async Task Only_the_second_groups_should_apply(CancellationToken cancellationToken) + public async Task Only_the_second_groups_should_apply(CancellationToken cancellationToken = default) { FailedMessage originalMessage = null; FailedMessage retriedMessage = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/ErrorImportPerformanceTests.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/ErrorImportPerformanceTests.cs index 72fe25eff0..a0a56c6336 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/ErrorImportPerformanceTests.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/ErrorImportPerformanceTests.cs @@ -16,7 +16,7 @@ class ErrorImportPerformanceTests : AcceptanceTest { [Test] [CancelAfter(180_000)] - public async Task Should_import_all_messages(CancellationToken cancellationToken) + public async Task Should_import_all_messages(CancellationToken cancellationToken = default) { await Define() .WithEndpoint(b => b.When(bus => Task.WhenAll(Enumerable.Repeat(0, 100).Select(i => bus.SendLocal(new MyMessage())))).DoNotFailOnErrorMessages()) diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs index 068f7b0dda..630e7b938c 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_invalid_id_is_sent_to_retry.cs @@ -15,7 +15,7 @@ class When_a_invalid_id_is_sent_to_retry : AcceptanceTest { [Test] [CancelAfter(180_000)] - public async Task SubsequentBatchesShouldBeProcessed(CancellationToken cancellationToken) + public async Task SubsequentBatchesShouldBeProcessed(CancellationToken cancellationToken = default) { var context = await Define() .WithEndpoint(cfg => cfg diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs index a26f067f09..1c6bd75b5b 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_message_has_failed.cs @@ -146,7 +146,7 @@ public async Task Should_be_listed_in_the_error_list() [Test] [CancelAfter(120_000)] - public async Task Should_be_listed_in_the_messages_list(CancellationToken cancellationToken) + public async Task Should_be_listed_in_the_messages_list(CancellationToken cancellationToken = default) { var failure = new MessagesView(); diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs index bbeea18019..ec575fb481 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_fails_to_be_sent.cs @@ -25,7 +25,7 @@ class When_a_retry_fails_to_be_sent : AcceptanceTest { [Test] [CancelAfter(180_000)] - public async Task SubsequentBatchesShouldBeProcessed(CancellationToken cancellationToken) + public async Task SubsequentBatchesShouldBeProcessed(CancellationToken cancellationToken = default) { FailedMessage decomissionedFailure = null, successfullyRetried = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs index 73ee896515..1cc7e3494b 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_empty_body_message_is_successful.cs @@ -22,7 +22,7 @@ class When_a_retry_for_a_empty_body_message_is_successful : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Should_show_up_as_resolved_when_doing_a_single_retry(CancellationToken cancellationToken) + public async Task Should_show_up_as_resolved_when_doing_a_single_retry(CancellationToken cancellationToken = default) { FailedMessage failure = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_fails.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_fails.cs index 716f707c4f..fc3bfbcdb7 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_fails.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_fails.cs @@ -17,7 +17,7 @@ class When_a_retry_for_a_failed_message_fails : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task It_should_be_marked_as_unresolved(CancellationToken cancellationToken) + public async Task It_should_be_marked_as_unresolved(CancellationToken cancellationToken = default) { var result = await Define(ctx => { ctx.Succeed = false; }) .WithEndpoint(b => @@ -49,7 +49,7 @@ public async Task It_should_be_marked_as_unresolved(CancellationToken cancellati [Test] [CancelAfter(120_000)] - public async Task It_should_be_able_to_be_retried_successfully(CancellationToken cancellationToken) + public async Task It_should_be_able_to_be_retried_successfully(CancellationToken cancellationToken = default) { var result = await Define(ctx => { ctx.Succeed = false; }) .WithEndpoint(b => diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs index 44a06395af..dd232ed5a0 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_a_retry_for_a_failed_message_is_successful.cs @@ -20,7 +20,7 @@ class When_a_retry_for_a_failed_message_is_successful : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Should_show_up_as_resolved_in_the_eventlog(CancellationToken cancellationToken) + public async Task Should_show_up_as_resolved_in_the_eventlog(CancellationToken cancellationToken = default) { FailedMessage failure = null; List eventLogItems = null; @@ -58,7 +58,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Should_show_up_as_resolved_when_doing_a_multi_retry(CancellationToken cancellationToken) + public async Task Should_show_up_as_resolved_when_doing_a_multi_retry(CancellationToken cancellationToken = default) { FailedMessage failure = null; @@ -89,7 +89,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Should_show_up_as_resolved_when_doing_a_retry_all(CancellationToken cancellationToken) + public async Task Should_show_up_as_resolved_when_doing_a_retry_all(CancellationToken cancellationToken = default) { FailedMessage failure = null; @@ -120,7 +120,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Acknowledging_the_retry_should_be_successful(CancellationToken cancellationToken) + public async Task Acknowledging_the_retry_should_be_successful(CancellationToken cancellationToken = default) { FailedMessage failure; @@ -149,7 +149,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Should_show_up_as_resolved_when_doing_a_retry_all_for_the_given_endpoint(CancellationToken cancellationToken) + public async Task Should_show_up_as_resolved_when_doing_a_retry_all_for_the_given_endpoint(CancellationToken cancellationToken = default) { FailedMessage failure = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_all_messages_are_retried.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_all_messages_are_retried.cs index d77fa5da40..c30c63f3ff 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_all_messages_are_retried.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_all_messages_are_retried.cs @@ -17,7 +17,7 @@ class When_all_messages_are_retried : AcceptanceTest { [Test] [CancelAfter(180_000)] - public async Task Only_unresolved_issues_should_be_retried(CancellationToken cancellationToken) + public async Task Only_unresolved_issues_should_be_retried(CancellationToken cancellationToken = default) { FailedMessage messageToBeRetriedAsPartOfRetryAll = null; FailedMessage messageToBeArchived = null; diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageRedirects/When_a_message_is_retried.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageRedirects/When_a_message_is_retried.cs index d373dcee4d..5235d39c36 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageRedirects/When_a_message_is_retried.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageRedirects/When_a_message_is_retried.cs @@ -18,7 +18,7 @@ class When_a_message_is_retried_with_a_redirect : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task It_should_be_sent_to_the_correct_endpoint(CancellationToken cancellationToken) + public async Task It_should_be_sent_to_the_correct_endpoint(CancellationToken cancellationToken = default) { var context = await Define() .WithEndpoint(b => b.When(bus => bus.SendLocal(new MessageToRetry())) diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/When_a_message_is_retried_and_succeeds_with_a_reply.cs b/src/ServiceControl.AcceptanceTests/Recoverability/When_a_message_is_retried_and_succeeds_with_a_reply.cs index 5b87a5aaf9..81711ab5b8 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/When_a_message_is_retried_and_succeeds_with_a_reply.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/When_a_message_is_retried_and_succeeds_with_a_reply.cs @@ -17,7 +17,7 @@ class When_a_message_is_retried_and_succeeds_with_a_reply : AcceptanceTest { [Test] [CancelAfter(60_000)] - public async Task The_reply_should_go_to_the_correct_endpoint(CancellationToken cancellation) + public async Task The_reply_should_go_to_the_correct_endpoint(CancellationToken cancellationToken = default) { var context = await Define() .WithEndpoint(c => c.When(bus => bus.Send(new OriginalMessage()))) @@ -43,7 +43,7 @@ public async Task The_reply_should_go_to_the_correct_endpoint(CancellationToken return !string.IsNullOrWhiteSpace(c.ReplyHandledBy); }) - .Run(cancellation); + .Run(cancellationToken); Assert.That(context.ReplyHandledBy, Is.EqualTo("Originating Endpoint"), "Reply handled by incorrect endpoint"); } diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig index bbcb303765..0a6ae3dffe 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/.editorconfig @@ -6,5 +6,4 @@ 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.Audit.AcceptanceTests.RavenDB/Auditing/When_critical_storage_threshold_reached.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/Auditing/When_critical_storage_threshold_reached.cs index 852e23c1e5..d87622ec54 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/Auditing/When_critical_storage_threshold_reached.cs +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/Auditing/When_critical_storage_threshold_reached.cs @@ -50,7 +50,7 @@ await Define() [Test] [CancelAfter(120_000)] - public async Task Should_stop_ingestion_and_resume_when_more_space_is_available(CancellationToken cancellationToken) + public async Task Should_stop_ingestion_and_resume_when_more_space_is_available(CancellationToken cancellationToken = default) { SetStorageConfiguration = static d => d.Add(RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey, "0"); diff --git a/src/ServiceControl.Audit.UnitTests/.editorconfig b/src/ServiceControl.Audit.UnitTests/.editorconfig index bbcb303765..0a6ae3dffe 100644 --- a/src/ServiceControl.Audit.UnitTests/.editorconfig +++ b/src/ServiceControl.Audit.UnitTests/.editorconfig @@ -6,5 +6,4 @@ 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.Audit.UnitTests/BodyStorage/BodyStorageEnricherTests.cs b/src/ServiceControl.Audit.UnitTests/BodyStorage/BodyStorageEnricherTests.cs index d0e7f00a2b..d559fdcf4e 100644 --- a/src/ServiceControl.Audit.UnitTests/BodyStorage/BodyStorageEnricherTests.cs +++ b/src/ServiceControl.Audit.UnitTests/BodyStorage/BodyStorageEnricherTests.cs @@ -262,13 +262,13 @@ class FakeBodyStorage : IBodyStorage { public int StoredBodySize { get; set; } - public Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream, CancellationToken cancellationToken) + public Task Store(string bodyId, string contentType, int bodySize, Stream bodyStream, CancellationToken cancellationToken = default) { StoredBodySize = bodySize; return Task.CompletedTask; } - public Task TryFetch(string bodyId, CancellationToken cancellationToken) + public Task TryFetch(string bodyId, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/src/ServiceControl.Infrastructure.Tests/.editorconfig b/src/ServiceControl.Infrastructure.Tests/.editorconfig index b09da988d4..07eabf2b62 100644 --- a/src/ServiceControl.Infrastructure.Tests/.editorconfig +++ b/src/ServiceControl.Infrastructure.Tests/.editorconfig @@ -3,7 +3,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.PS0019.severity = none diff --git a/src/ServiceControl.Infrastructure.Tests/WatchdogTests.cs b/src/ServiceControl.Infrastructure.Tests/WatchdogTests.cs index ea786ed314..c3d5c5fc8a 100644 --- a/src/ServiceControl.Infrastructure.Tests/WatchdogTests.cs +++ b/src/ServiceControl.Infrastructure.Tests/WatchdogTests.cs @@ -63,6 +63,10 @@ public async Task When_stop_fails_stop_should_throw_identifying_ungraceful_stop( await dog.Stop(TestContext.CurrentContext.CancellationToken); Assert.Fail("Should have thrown an exception"); } + catch (OperationCanceledException) when (TestContext.CurrentContext.CancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Assert.That(ex.Message, Is.EqualTo("Simulated")); diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/.editorconfig b/src/ServiceControl.MultiInstance.AcceptanceTests/.editorconfig index b0c4497648..3eca2918ad 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/.editorconfig +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/.editorconfig @@ -3,9 +3,10 @@ # 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 +# Justification: these are the test helpers the NServiceBus.AcceptanceTesting scenario API drives. +# It calls Done/When callbacks and IComponentBehavior/ComponentRunner without a CancellationToken, +# so a token added here could only ever be CancellationToken.None at every call site, which tests +# nothing. Tests that genuinely exercise cancellation use [Test, CancelAfter(...)] with the token +# NUnit injects, and forward that. Helpers reachable with a real token do take one. dotnet_diagnostic.PS0013.severity = none dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Auditing/When_requesting_a_message_body.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Auditing/When_requesting_a_message_body.cs index 7188e391a5..9766f79249 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Auditing/When_requesting_a_message_body.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Auditing/When_requesting_a_message_body.cs @@ -22,7 +22,7 @@ class When_requesting_a_message_body : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Should_be_forwarded_to_audit_instance(CancellationToken cancellationToken) + public async Task Should_be_forwarded_to_audit_instance(CancellationToken cancellationToken = default) { string addressOfAuditInstance = null; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs index 704dcb3220..e1fefebbc0 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs @@ -49,7 +49,7 @@ await Define() class RemoteNotAvailableHandler : HttpMessageHandler { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) => throw new HttpRequestException(HttpRequestError.ConnectionError); } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetrying.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetrying.cs index cdb7f575db..775f64cb6a 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetrying.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetrying.cs @@ -1,5 +1,7 @@ namespace ServiceControl.MultiInstance.AcceptanceTests.Recoverability; +using System.Threading; + using System.Threading.Tasks; using AcceptanceTesting; using MessageFailures; @@ -8,15 +10,15 @@ namespace ServiceControl.MultiInstance.AcceptanceTests.Recoverability; abstract class WhenRetrying : AcceptanceTest { - protected Task> GetFailedMessage(string uniqueMessageId, string instance, FailedMessageStatus expectedStatus) + protected Task> GetFailedMessage(string uniqueMessageId, string instance, FailedMessageStatus expectedStatus, CancellationToken cancellationToken = default) { if (uniqueMessageId == null) { return Task.FromResult(SingleResult.Empty); } - return this.TryGet($"/api/errors/{uniqueMessageId}", f => f.Status == expectedStatus, instance); + return this.TryGet($"/api/errors/{uniqueMessageId}", f => f.Status == expectedStatus, instance, cancellationToken); } - protected Task> GetAllFailedMessage(string instance, FailedMessageStatus expectedStatus) => this.TryGetMany("/api/errors", f => f.Status == expectedStatus, instance); + protected Task> GetAllFailedMessage(string instance, FailedMessageStatus expectedStatus, CancellationToken cancellationToken = default) => this.TryGetMany("/api/errors", f => f.Status == expectedStatus, instance, cancellationToken); } \ No newline at end of file diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs index 68b100316b..86e9d243fb 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs @@ -27,7 +27,7 @@ public enum RetryType //[TestCase(new[] { RetryType.NoEdit, RetryType.Edit, RetryType.NoEdit, RetryType.Edit })] [TestCase(new[] { RetryType.Edit, RetryType.Edit, RetryType.NoEdit })] [CancelAfter(30_000)] - public async Task WithMixOfRetryTypes(RetryType[] retryTypes, CancellationToken cancellationToken) + public async Task WithMixOfRetryTypes(RetryType[] retryTypes, CancellationToken cancellationToken = default) { CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; }; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs index c98ab98cf3..74abb83b2e 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs @@ -17,7 +17,7 @@ class WhenRetryingWithEdit : WhenRetrying { [Test] [CancelAfter(30_000)] - public async Task ShouldCreateNewMessageAndResolveEditedMessage(CancellationToken cancellationToken) + public async Task ShouldCreateNewMessageAndResolveEditedMessage(CancellationToken cancellationToken = default) { CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; }; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_is_sent_to_audit_instance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_is_sent_to_audit_instance.cs index d912716f26..67cd947d2e 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_is_sent_to_audit_instance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_a_message_retry_audit_is_sent_to_audit_instance.cs @@ -18,7 +18,7 @@ class When_a_message_retry_audit_is_sent_to_audit_instance : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Should_mark_as_resolved(CancellationToken cancellationToken) + public async Task Should_mark_as_resolved(CancellationToken cancellationToken = default) { FailedMessage failure; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs index 7159ca5348..658810bb4c 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs @@ -19,7 +19,7 @@ class When_issuing_retry_by_specifying_instance_id : AcceptanceTest { [Test] [CancelAfter(120_000)] - public async Task Should_be_work(CancellationToken cancellationToken) + public async Task Should_be_work(CancellationToken cancellationToken = default) { string addressOfItself = null; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs index d09497c90a..4f0512ed03 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs @@ -4,6 +4,7 @@ namespace ServiceControl.MultiInstance.AcceptanceTests.TestSupport using System.Net; using System.Net.Http; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using ServiceBus.Management.Infrastructure.Settings; @@ -17,39 +18,39 @@ static IAcceptanceTestInfrastructureProvider ToHttpExtension(this IAcceptanceTes SerializerOptions = providerMultiInstance.SerializerOptions[instanceName], }; - public static Task Put(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Put(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).Put(url, payload, requestHasFailed); + return providerMultiInstance.ToHttpExtension(instanceName).Put(url, payload, requestHasFailed, cancellationToken); } - public static Task GetRaw(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, string instanceName = Settings.DEFAULT_INSTANCE_NAME) + public static Task GetRaw(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) { - return providerMultiInstance.ToHttpExtension(instanceName).GetRaw(url); + return providerMultiInstance.ToHttpExtension(instanceName).GetRaw(url, cancellationToken); } - public static Task> TryGetMany(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGetMany(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).TryGetMany(url, condition); + return providerMultiInstance.ToHttpExtension(instanceName).TryGetMany(url, condition, cancellationToken); } - public static Task Patch(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Patch(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).Patch(url, payload); + return providerMultiInstance.ToHttpExtension(instanceName).Patch(url, payload, cancellationToken); } - public static Task> TryGet(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGet(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).TryGet(url, condition); + return providerMultiInstance.ToHttpExtension(instanceName).TryGet(url, condition, cancellationToken); } - public static Task> TryGetSingle(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGetSingle(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).TryGetSingle(url, condition); + return providerMultiInstance.ToHttpExtension(instanceName).TryGetSingle(url, condition, cancellationToken); } - public static Task Post(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Post(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME, CancellationToken cancellationToken = default) where T : class { - return providerMultiInstance.ToHttpExtension(instanceName).Post(url, payload, requestHasFailed); + return providerMultiInstance.ToHttpExtension(instanceName).Post(url, payload, requestHasFailed, cancellationToken); } } diff --git a/src/ServiceControl.Persistence.Tests.InMemory/.editorconfig b/src/ServiceControl.Persistence.Tests.InMemory/.editorconfig index 0a6ae3dffe..9bc5edd414 100644 --- a/src/ServiceControl.Persistence.Tests.InMemory/.editorconfig +++ b/src/ServiceControl.Persistence.Tests.InMemory/.editorconfig @@ -3,7 +3,7 @@ # 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 +# Cancellation analyzer debt. PS0018 here is the test helper chain, not [Test] methods +# (Particular.Analyzers already exempts those). Remove a line once this project has no # violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0018.severity = none +dotnet_diagnostic.PS0018.severity = none \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig b/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig index e88159f88e..82926b9419 100644 --- a/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig @@ -3,11 +3,8 @@ # Justification: ServiceControl app has no synchronization context 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 +# Cancellation analyzer debt. PS0018 here is the test helper chain, not [Test] methods +# (Particular.Analyzers already exempts those). Remove a line once this project has no # violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0008.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs b/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs index 8b4a659a1c..441c05a9e5 100644 --- a/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs @@ -9,7 +9,7 @@ static class PostgreSqlSharedContainer { const string docsPath = "docs/testing-persistence.md#postgresql"; - public static async Task GetConnectionStringAsync(CancellationToken ct = default) + public static async Task GetConnectionStringAsync(CancellationToken cancellationToken = default) { var envConnStr = Environment.GetEnvironmentVariable("ServiceControl_Persistence_PostgreSql_ConnectionString"); if (!string.IsNullOrEmpty(envConnStr)) @@ -22,10 +22,10 @@ public static async Task GetConnectionStringAsync(CancellationToken ct = return container.GetConnectionString(); } - await semaphore.WaitAsync(ct); + await semaphore.WaitAsync(cancellationToken); try { - container ??= await StartContainerAsync(ct); + container ??= await StartContainerAsync(cancellationToken); return container.GetConnectionString(); } finally @@ -34,15 +34,19 @@ public static async Task GetConnectionStringAsync(CancellationToken ct = } } - public static async Task Stop() => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); + public static async Task Stop(CancellationToken cancellationToken = default) => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); - static async Task StartContainerAsync(CancellationToken ct) + static async Task StartContainerAsync(CancellationToken cancellationToken) { var c = new PostgreSqlBuilder("postgres:16-alpine") .Build(); try { - await c.StartAsync(ct); + await c.StartAsync(cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/.editorconfig b/src/ServiceControl.Persistence.Tests.RavenDB/.editorconfig index 0a6ae3dffe..9bc5edd414 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/.editorconfig +++ b/src/ServiceControl.Persistence.Tests.RavenDB/.editorconfig @@ -3,7 +3,7 @@ # 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 +# Cancellation analyzer debt. PS0018 here is the test helper chain, not [Test] methods +# (Particular.Analyzers already exempts those). Remove a line once this project has no # violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0018.severity = none +dotnet_diagnostic.PS0018.severity = none \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig b/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig index e88159f88e..82926b9419 100644 --- a/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig +++ b/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig @@ -3,11 +3,8 @@ # Justification: ServiceControl app has no synchronization context 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 +# Cancellation analyzer debt. PS0018 here is the test helper chain, not [Test] methods +# (Particular.Analyzers already exempts those). Remove a line once this project has no # violations of that rule left, and never add a rule back to this list. -dotnet_diagnostic.PS0008.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs b/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs index 9494914c8f..aefc4ff667 100644 --- a/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs +++ b/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs @@ -10,7 +10,7 @@ static class SqlServerSharedContainer { const string docsPath = "docs/testing-persistence.md#sql-server"; - public static async Task GetConnectionStringAsync(CancellationToken ct = default) + public static async Task GetConnectionStringAsync(CancellationToken cancellationToken = default) { var envConnStr = Environment.GetEnvironmentVariable("ServiceControl_Persistence_SqlServer_ConnectionString"); if (!string.IsNullOrEmpty(envConnStr)) @@ -23,10 +23,10 @@ public static async Task GetConnectionStringAsync(CancellationToken ct = return container.GetConnectionString(); } - await semaphore.WaitAsync(ct); + await semaphore.WaitAsync(cancellationToken); try { - container ??= await StartContainerAsync(ct); + container ??= await StartContainerAsync(cancellationToken); return container.GetConnectionString(); } finally @@ -35,14 +35,18 @@ public static async Task GetConnectionStringAsync(CancellationToken ct = } } - public static async Task Stop() => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); + public static async Task Stop(CancellationToken cancellationToken = default) => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); - static async Task StartContainerAsync(CancellationToken ct) + static async Task StartContainerAsync(CancellationToken cancellationToken) { var c = new MsSqlBuilder("particular/servicecontrol-testing-sqlserver:latest").Build(); try { - await c.StartAsync(ct); + await c.StartAsync(cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { diff --git a/src/ServiceControl.Persistence.Tests/.editorconfig b/src/ServiceControl.Persistence.Tests/.editorconfig index b26fbddcce..fdb6174a57 100644 --- a/src/ServiceControl.Persistence.Tests/.editorconfig +++ b/src/ServiceControl.Persistence.Tests/.editorconfig @@ -3,12 +3,10 @@ # Justification: Test project dotnet_diagnostic.CA2007.severity = none -# Justification: Test project. Demanding a CancellationToken on every [Test] method, and on the -# helpers they call, buys nothing: a test that hangs fails the run either way, and [CancelAfter] -# only bites once the code under test honours the token. Persistence integration tests that need a -# real timeout use [Test, CancelAfter(...)] with an injected token instead, which is the pattern to -# follow for new tests. These are accepted exceptions, not scheduled work. -dotnet_diagnostic.PS0003.severity = none # A parameter of type CancellationToken on a non-private delegate or method should be optional -dotnet_diagnostic.PS0006.severity = none # Pass CancellationToken.None instead of the default literal -dotnet_diagnostic.PS0013.severity = none # A Func used as a method parameter with a Task return type argument should have a CancellationToken type argument -dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter +# Cancellation analyzer debt, still scheduled work rather than accepted exceptions. Measured +# Aug 2026: PS0018 here is NOT about [Test] methods (Particular.Analyzers already exempts those), +# it is the protected/private test helpers those tests call. Converting them means threading a +# token through the helper chain and the Func<...> callback shapes, which is why it is not done +# yet. Remove a line once this project has no violations of that rule left. +dotnet_diagnostic.PS0013.severity = none +dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.Persistence.Tests/EFCore/EFCoreExtensionMethodTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/EFCoreExtensionMethodTests.cs index 034ee9e51d..a6ea928931 100644 --- a/src/ServiceControl.Persistence.Tests/EFCore/EFCoreExtensionMethodTests.cs +++ b/src/ServiceControl.Persistence.Tests/EFCore/EFCoreExtensionMethodTests.cs @@ -13,7 +13,7 @@ namespace ServiceControl.Persistence.Tests; public class EFCoreExtensionMethodTests : PersistenceTestBase { [Test, CancelAfter(60_000)] - public async Task Insert_until_conflict_should_not_throw_errors(CancellationToken cancellationToken) + public async Task Insert_until_conflict_should_not_throw_errors(CancellationToken cancellationToken = default) { var i = 0; while (!cancellationToken.IsCancellationRequested) diff --git a/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs index fb266503a1..df7d63bc5c 100644 --- a/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs +++ b/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Persistence.Tests; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -15,8 +16,8 @@ public async Task Recording_the_same_day_twice_adds_to_that_day() { await SaveEndpoint("Endpoint", ThroughputSource.Monitoring); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 30, default); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 12, default); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 30); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 12); var throughput = await GetThroughput("Endpoint"); @@ -32,7 +33,7 @@ public async Task Concurrent_recordings_of_the_same_day_all_count() await SaveEndpoint("Endpoint", ThroughputSource.Monitoring); var recordings = Enumerable.Range(0, writers) - .Select(_ => LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 5, default)); + .Select(_ => LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Monitoring, Today, 5)); await Task.WhenAll(recordings); @@ -46,9 +47,9 @@ public async Task Endpoints_are_matched_regardless_of_name_casing() { await SaveEndpoint("SalesEndpoint", ThroughputSource.Broker); - await LicensingDataStore.RecordEndpointThroughput("salesendpoint", ThroughputSource.Broker, Today, 7, default); + await LicensingDataStore.RecordEndpointThroughput("salesendpoint", ThroughputSource.Broker, Today, 7); - var endpoint = await LicensingDataStore.GetEndpoint("SALESENDPOINT", ThroughputSource.Broker, default); + var endpoint = await LicensingDataStore.GetEndpoint("SALESENDPOINT", ThroughputSource.Broker); Assert.That(endpoint, Is.Not.Null); using (Assert.EnterMultipleScope()) @@ -63,11 +64,11 @@ public async Task Last_collected_date_is_the_newest_recorded_day() { await SaveEndpoint("Endpoint", ThroughputSource.Audit); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-5), 10, default); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-1), 10, default); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-3), 10, default); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-5), 10); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-1), 10); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Audit, Today.AddDays(-3), 10); - var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit, default); + var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit); Assert.That(endpoint, Is.Not.Null); Assert.That(endpoint.LastCollectedDate, Is.EqualTo(Today.AddDays(-1))); @@ -78,7 +79,7 @@ public async Task Last_collected_date_is_unset_when_nothing_was_recorded() { await SaveEndpoint("Endpoint", ThroughputSource.Audit); - var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit, default); + var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit); Assert.That(endpoint, Is.Not.Null); Assert.That(endpoint.LastCollectedDate, Is.EqualTo(default(DateOnly))); @@ -98,7 +99,7 @@ public async Task Endpoints_are_returned_for_every_requested_id_including_the_un new EndpointIdentifier("Unknown", ThroughputSource.Audit) }; - var results = (await LicensingDataStore.GetEndpoints(requested, default)).ToList(); + var results = (await LicensingDataStore.GetEndpoints(requested)).ToList(); using (Assert.EnterMultipleScope()) { @@ -118,11 +119,11 @@ await LicensingDataStore.SaveEndpoint( { SanitizedName = "Platform", EndpointIndicators = [EndpointIndicator.PlatformEndpoint.ToString()] - }, default); + }); await SaveEndpoint("Regular", ThroughputSource.Broker); - var withPlatform = await LicensingDataStore.GetAllEndpoints(true, default); - var withoutPlatform = await LicensingDataStore.GetAllEndpoints(false, default); + var withPlatform = await LicensingDataStore.GetAllEndpoints(true); + var withoutPlatform = await LicensingDataStore.GetAllEndpoints(false); using (Assert.EnterMultipleScope()) { @@ -143,9 +144,9 @@ await LicensingDataStore.SaveEndpoint( EndpointIndicators = indicators, Scope = "vhost", UserIndicator = UserIndicator.NServiceBusEndpoint.ToString() - }, default); + }); - var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker, default); + var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker); Assert.That(endpoint, Is.Not.Null); using (Assert.EnterMultipleScope()) @@ -163,8 +164,8 @@ public async Task Throughput_older_than_the_reported_window_is_not_returned() { await SaveEndpoint("Endpoint", ThroughputSource.Broker); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today.AddMonths(-15), 99, default); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today.AddDays(-1), 5, default); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today.AddMonths(-15), 99); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today.AddDays(-1), 5); var throughput = await GetThroughput("Endpoint"); @@ -178,7 +179,7 @@ public async Task Throughput_older_than_the_reported_window_is_not_returned() [Test] public async Task Recording_throughput_for_an_unknown_endpoint_throws() { - Assert.That(async () => await LicensingDataStore.RecordEndpointThroughput("Unknown", ThroughputSource.Broker, Today, 1, default), + Assert.That(async () => await LicensingDataStore.RecordEndpointThroughput("Unknown", ThroughputSource.Broker, Today, 1), Throws.InstanceOf()); } @@ -186,12 +187,12 @@ public async Task Recording_throughput_for_an_unknown_endpoint_throws() public async Task Saving_an_endpoint_again_replaces_it_and_keeps_its_throughput() { await SaveEndpoint("Endpoint", ThroughputSource.Broker); - await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today, 11, default); + await LicensingDataStore.RecordEndpointThroughput("Endpoint", ThroughputSource.Broker, Today, 11); await LicensingDataStore.SaveEndpoint( - new Endpoint("Endpoint", ThroughputSource.Broker) { SanitizedName = "Endpoint", Scope = "updated" }, default); + new Endpoint("Endpoint", ThroughputSource.Broker) { SanitizedName = "Endpoint", Scope = "updated" }); - var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker, default); + var endpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker); var throughput = await GetThroughput("Endpoint"); Assert.That(endpoint, Is.Not.Null); @@ -203,11 +204,11 @@ await LicensingDataStore.SaveEndpoint( } Task SaveEndpoint(string name, ThroughputSource source) => - LicensingDataStore.SaveEndpoint(new Endpoint(name, source) { SanitizedName = name }, default); + LicensingDataStore.SaveEndpoint(new Endpoint(name, source) { SanitizedName = name }); async Task GetThroughput(string queueName) { - var throughput = await LicensingDataStore.GetEndpointThroughputByQueueName([queueName], default); + var throughput = await LicensingDataStore.GetEndpointThroughputByQueueName([queueName]); return throughput[queueName].Single(); } diff --git a/src/ServiceControl.Persistence.Tests/EndpointSettingsStoreTests.cs b/src/ServiceControl.Persistence.Tests/EndpointSettingsStoreTests.cs index 8795db2217..2ecaa01c04 100644 --- a/src/ServiceControl.Persistence.Tests/EndpointSettingsStoreTests.cs +++ b/src/ServiceControl.Persistence.Tests/EndpointSettingsStoreTests.cs @@ -10,7 +10,7 @@ namespace ServiceControl.Persistence.Tests; class EndpointSettingsStoreTests : PersistenceTestBase { [Test, CancelAfter(30_000)] - public async Task UpdateEndpointSettings_stores_and_updates_existing_setting(CancellationToken cancellationToken) + public async Task UpdateEndpointSettings_stores_and_updates_existing_setting(CancellationToken cancellationToken = default) { await EndpointSettingsStore.UpdateEndpointSettings(new EndpointSettings { Name = "Sales", TrackInstances = false }, cancellationToken); await EndpointSettingsStore.UpdateEndpointSettings(new EndpointSettings { Name = "Sales", TrackInstances = true }, cancellationToken); @@ -26,7 +26,7 @@ public async Task UpdateEndpointSettings_stores_and_updates_existing_setting(Can } [Test, CancelAfter(30_000)] - public async Task Delete_removes_only_target_setting(CancellationToken cancellationToken) + public async Task Delete_removes_only_target_setting(CancellationToken cancellationToken = default) { await EndpointSettingsStore.UpdateEndpointSettings(new EndpointSettings { Name = "Sales", TrackInstances = false }, cancellationToken); await EndpointSettingsStore.UpdateEndpointSettings(new EndpointSettings { Name = "Shipping", TrackInstances = true }, cancellationToken); diff --git a/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs b/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs index 0718354ab6..e69e907b0a 100644 --- a/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs +++ b/src/ServiceControl.Persistence.Tests/FakeDomainEvents.cs @@ -13,7 +13,7 @@ class FakeDomainEvents : IDomainEvents { public List RaisedEvents { get; } = []; - public Task Raise(T domainEvent, CancellationToken cancellationToken) where T : IDomainEvent + public Task Raise(T domainEvent, CancellationToken cancellationToken = default) where T : IDomainEvent { RaisedEvents.Add(domainEvent); TestContext.Out.WriteLine($"Raised DomainEvent {typeof(T).Name}:"); diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs index 1e6a979032..afcb089bb4 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs @@ -269,7 +269,7 @@ public sealed class TestableUnicastDispatcher : IMessageDispatcher public Exception ThrowOnDispatch { get; set; } - public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken) + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken = default) { if (ThrowOnDispatch != null) { diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs index bc366dfcb4..c73e7bbeba 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs @@ -184,7 +184,7 @@ public async Task It_restores_body_id_and_target_addres_after_failure() class FaultySender : IMessageDispatcher { - public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken) + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken = default) { throw new Exception("Simulated"); } @@ -196,7 +196,7 @@ class FakeSender : IMessageDispatcher public string Destination { get; private set; } - public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken) + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken = default) { var operation = outgoingMessages.UnicastTransportOperations.Single(); Message = operation.Message; diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs index 5cdac0108c..56df79608c 100644 --- a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -515,7 +515,7 @@ public class TestSender : IMessageDispatcher { public Action Callback { get; set; } = m => { }; - public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken) + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken = default) { foreach (var operation in outgoingMessages.UnicastTransportOperations) { diff --git a/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs index 50a8830326..5707e68d53 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Persistence.Tests.Throughput; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -15,7 +16,7 @@ public async Task Should_retrieve_saved_audit_service_metadata() var expectedAuditServiceMetadata = new AuditServiceMetadata( new Dictionary { ["Some version"] = 2 }, new Dictionary { ["Some transport"] = 3 }); - await LicensingDataStore.SaveAuditServiceMetadata(expectedAuditServiceMetadata, default); + await LicensingDataStore.SaveAuditServiceMetadata(expectedAuditServiceMetadata); //Act var retrievedAuditServiceMetadata = await LicensingDataStore.GetAuditServiceMetadata(); @@ -36,13 +37,13 @@ public async Task Should_update_existing_audit_service_metadata_if_already_exist var oldAuditServiceMetadata = new AuditServiceMetadata( new Dictionary { ["Some version"] = 2 }, new Dictionary { ["Some transport"] = 3 }); - await LicensingDataStore.SaveAuditServiceMetadata(oldAuditServiceMetadata, default); + await LicensingDataStore.SaveAuditServiceMetadata(oldAuditServiceMetadata); // Act var expectedAuditServiceMetadata = new AuditServiceMetadata( new Dictionary { ["Some version"] = 2, ["New version"] = 1 }, new Dictionary { ["Some transport"] = 4 }); - await LicensingDataStore.SaveAuditServiceMetadata(expectedAuditServiceMetadata, default); + await LicensingDataStore.SaveAuditServiceMetadata(expectedAuditServiceMetadata); var retrievedAuditServiceMetadata = await LicensingDataStore.GetAuditServiceMetadata(); // Assert diff --git a/src/ServiceControl.Persistence.Tests/Throughput/BrokerMetadataTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/BrokerMetadataTests.cs index 90b957a751..e2653207a7 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/BrokerMetadataTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/BrokerMetadataTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Persistence.Tests.Throughput; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -13,10 +14,10 @@ public async Task Should_retrieve_saved_broker_metadata() { //Arrange var expectedBrokerMetadata = new BrokerMetadata("Some scope", new Dictionary { ["Some key"] = "Some value" }); - await LicensingDataStore.SaveBrokerMetadata(expectedBrokerMetadata, default); + await LicensingDataStore.SaveBrokerMetadata(expectedBrokerMetadata); //Act - var retrievedBrokerMetadata = await LicensingDataStore.GetBrokerMetadata(default); + var retrievedBrokerMetadata = await LicensingDataStore.GetBrokerMetadata(); //Assert Assert.That(retrievedBrokerMetadata, Is.Not.Null); @@ -32,12 +33,12 @@ public async Task Should_update_existing_broker_metadata_if_already_exists() { // Arrange var oldBrokerMetadata = new BrokerMetadata("Some scope", new Dictionary { ["Some key"] = "Some value" }); - await LicensingDataStore.SaveBrokerMetadata(oldBrokerMetadata, default); + await LicensingDataStore.SaveBrokerMetadata(oldBrokerMetadata); // Act var expectedBrokerMetadata = new BrokerMetadata("New scope", new Dictionary { ["New key"] = "New value" }); - await LicensingDataStore.SaveBrokerMetadata(expectedBrokerMetadata, default); - var retrievedBrokerMetadata = await LicensingDataStore.GetBrokerMetadata(default); + await LicensingDataStore.SaveBrokerMetadata(expectedBrokerMetadata); + var retrievedBrokerMetadata = await LicensingDataStore.GetBrokerMetadata(); // Assert Assert.That(retrievedBrokerMetadata, Is.Not.Null); diff --git a/src/ServiceControl.Persistence.Tests/Throughput/EndpointsTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/EndpointsTests.cs index 897a80a25d..9d258ce21d 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/EndpointsTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/EndpointsTests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -16,10 +17,10 @@ public async Task Should_add_new_endpoint_when_no_endpoints() var endpoint = new Endpoint("Endpoint", ThroughputSource.Audit); // Act - await LicensingDataStore.SaveEndpoint(endpoint, default); + await LicensingDataStore.SaveEndpoint(endpoint); // Assert - var endpoints = await LicensingDataStore.GetAllEndpoints(true, default); + var endpoints = await LicensingDataStore.GetAllEndpoints(true); var foundEndpoint = endpoints.Single(); using (Assert.EnterMultipleScope()) @@ -37,11 +38,11 @@ public async Task Should_add_new_endpoint_when_name_is_the_same_but_source_diffe var endpoint2 = new Endpoint("Endpoint1", ThroughputSource.Broker); // Act - await LicensingDataStore.SaveEndpoint(endpoint1, default); - await LicensingDataStore.SaveEndpoint(endpoint2, default); + await LicensingDataStore.SaveEndpoint(endpoint1); + await LicensingDataStore.SaveEndpoint(endpoint2); // Assert - var endpoints = await LicensingDataStore.GetAllEndpoints(true, default); + var endpoints = await LicensingDataStore.GetAllEndpoints(true); Assert.That(endpoints.Count(), Is.EqualTo(2)); } @@ -51,15 +52,15 @@ public async Task Should_update_endpoint_that_already_has_throughput_with_new_th { // Arrange var endpoint1 = new Endpoint("Endpoint1", ThroughputSource.Audit) { SanitizedName = "Endpoint1" }; - await LicensingDataStore.SaveEndpoint(endpoint1, default); - await LicensingDataStore.RecordEndpointThroughput(endpoint1.Id.Name, ThroughputSource.Audit, DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-1), 50, default); + await LicensingDataStore.SaveEndpoint(endpoint1); + await LicensingDataStore.RecordEndpointThroughput(endpoint1.Id.Name, ThroughputSource.Audit, DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-1), 50); // Act - await LicensingDataStore.RecordEndpointThroughput(endpoint1.Id.Name, ThroughputSource.Audit, DateOnly.FromDateTime(DateTime.UtcNow), 100, default); + await LicensingDataStore.RecordEndpointThroughput(endpoint1.Id.Name, ThroughputSource.Audit, DateOnly.FromDateTime(DateTime.UtcNow), 100); // Assert - var endpoints = await LicensingDataStore.GetAllEndpoints(true, default); - var throughput = await LicensingDataStore.GetEndpointThroughputByQueueName([endpoint1.SanitizedName], default); + var endpoints = await LicensingDataStore.GetAllEndpoints(true); + var throughput = await LicensingDataStore.GetEndpointThroughputByQueueName([endpoint1.SanitizedName]); var foundEndpoint = endpoints.Single(); using (Assert.EnterMultipleScope()) @@ -83,10 +84,10 @@ public async Task Should_retrieve_matching_endpoint_when_same_source() { // Arrange var endpoint = new Endpoint("Endpoint", ThroughputSource.Audit); - await LicensingDataStore.SaveEndpoint(endpoint, default); + await LicensingDataStore.SaveEndpoint(endpoint); // Act - var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit, default); + var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit); // Assert Assert.That(foundEndpoint, Is.Not.Null); @@ -97,10 +98,10 @@ public async Task Should_not_retrieve_matching_endpoint_when_different_source() { // Arrange var endpoint = new Endpoint("Endpoint", ThroughputSource.Audit); - await LicensingDataStore.SaveEndpoint(endpoint, default); + await LicensingDataStore.SaveEndpoint(endpoint); // Act - var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker, default); + var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Broker); // Assert Assert.That(foundEndpoint, Is.Null); @@ -116,13 +117,13 @@ public async Task Should_update_user_indicators_and_nothing_else() { SanitizedName = "Endpoint" }; - await LicensingDataStore.SaveEndpoint(endpoint, default); + await LicensingDataStore.SaveEndpoint(endpoint); // Act - await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "Endpoint", UserIndicator = userIndicator }], default); + await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "Endpoint", UserIndicator = userIndicator }]); // Assert - var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit, default); + var foundEndpoint = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit); Assert.That(foundEndpoint, Is.Not.Null); Assert.That(foundEndpoint.UserIndicator, Is.EqualTo(userIndicator)); @@ -139,10 +140,10 @@ public async Task Should_not_add_endpoint_when_updating_user_indication() }; // Act - await LicensingDataStore.UpdateUserIndicatorOnEndpoints([userIndicatorUpate], default); + await LicensingDataStore.UpdateUserIndicatorOnEndpoints([userIndicatorUpate]); // Assert - var allEndpoints = await LicensingDataStore.GetAllEndpoints(true, default); + var allEndpoints = await LicensingDataStore.GetAllEndpoints(true); Assert.That(allEndpoints.Count, Is.EqualTo(0)); } @@ -156,15 +157,15 @@ public async Task Should_update_indicators_on_all_endpoint_sources_when_updated_ var endpointAudit = new Endpoint("Endpoint", ThroughputSource.Audit) { SanitizedName = "Endpoint" }; var endpointMonitoring = new Endpoint("Endpoint", ThroughputSource.Monitoring) { SanitizedName = "Endpoint" }; - await LicensingDataStore.SaveEndpoint(endpointAudit, default); - await LicensingDataStore.SaveEndpoint(endpointMonitoring, default); + await LicensingDataStore.SaveEndpoint(endpointAudit); + await LicensingDataStore.SaveEndpoint(endpointMonitoring); // Act - await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "Endpoint", UserIndicator = userIndicator }], default); + await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "Endpoint", UserIndicator = userIndicator }]); // Assert - var foundEndpointAudit = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit, default); - var foundEndpointMonitoring = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Monitoring, default); + var foundEndpointAudit = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Audit); + var foundEndpointMonitoring = await LicensingDataStore.GetEndpoint("Endpoint", ThroughputSource.Monitoring); Assert.That(foundEndpointAudit, Is.Not.Null); Assert.That(foundEndpointAudit.UserIndicator, Is.EqualTo(userIndicator)); @@ -182,15 +183,15 @@ public async Task Should_update_indicators_on_all_endpoint_sources_when_updated_ var endpointAudit = new Endpoint("Endpoint1", ThroughputSource.Audit) { SanitizedName = "Endpoint1" }; var endpointMonitoring = new Endpoint("\"public\".\"Endpoint1\"", ThroughputSource.Monitoring) { SanitizedName = "Endpoint1" }; - await LicensingDataStore.SaveEndpoint(endpointAudit, default); - await LicensingDataStore.SaveEndpoint(endpointMonitoring, default); + await LicensingDataStore.SaveEndpoint(endpointAudit); + await LicensingDataStore.SaveEndpoint(endpointMonitoring); // Act - await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "\"public\".\"Endpoint1\"", UserIndicator = userIndicator }], default); + await LicensingDataStore.UpdateUserIndicatorOnEndpoints([new UpdateUserIndicator { Name = "\"public\".\"Endpoint1\"", UserIndicator = userIndicator }]); // Assert - var foundEndpointAudit = await LicensingDataStore.GetEndpoint("Endpoint1", ThroughputSource.Audit, default); - var foundEndpointMonitoring = await LicensingDataStore.GetEndpoint("\"public\".\"Endpoint1\"", ThroughputSource.Monitoring, default); + var foundEndpointAudit = await LicensingDataStore.GetEndpoint("Endpoint1", ThroughputSource.Audit); + var foundEndpointMonitoring = await LicensingDataStore.GetEndpoint("\"public\".\"Endpoint1\"", ThroughputSource.Monitoring); Assert.That(foundEndpointAudit, Is.Not.Null); Assert.That(foundEndpointAudit.UserIndicator, Is.EqualTo(userIndicator)); @@ -213,8 +214,8 @@ public async Task Should_update_user_indicators_on_more_than_30_endpoints_withou for (var i = 0; i < endpointCount; i++) { var sanitizedName = $"Endpoint{i}"; - await LicensingDataStore.SaveEndpoint(new Endpoint(sanitizedName, ThroughputSource.Audit) { SanitizedName = sanitizedName }, default); - await LicensingDataStore.SaveEndpoint(new Endpoint($"schema.{sanitizedName}", ThroughputSource.Monitoring) { SanitizedName = sanitizedName }, default); + await LicensingDataStore.SaveEndpoint(new Endpoint(sanitizedName, ThroughputSource.Audit) { SanitizedName = sanitizedName }); + await LicensingDataStore.SaveEndpoint(new Endpoint($"schema.{sanitizedName}", ThroughputSource.Monitoring) { SanitizedName = sanitizedName }); } var updates = Enumerable.Range(0, endpointCount) @@ -222,10 +223,10 @@ public async Task Should_update_user_indicators_on_more_than_30_endpoints_withou .ToList(); // Act - must not throw InvalidOperationException due to exceeding session request limit - await LicensingDataStore.UpdateUserIndicatorOnEndpoints(updates, default); + await LicensingDataStore.UpdateUserIndicatorOnEndpoints(updates); // Assert - var allEndpoints = (await LicensingDataStore.GetAllEndpoints(true, default)).ToList(); + var allEndpoints = (await LicensingDataStore.GetAllEndpoints(true)).ToList(); Assert.That(allEndpoints, Has.Count.EqualTo(endpointCount * 2)); Assert.That(allEndpoints, Has.All.Matches(e => e.UserIndicator == userIndicator)); @@ -237,16 +238,15 @@ public async Task Should_correctly_report_throughput_existence_for_X_days(int da { // Arrange var endpointAudit = new Endpoint("Endpoint", ThroughputSource.Audit); - await LicensingDataStore.SaveEndpoint(endpointAudit, default); + await LicensingDataStore.SaveEndpoint(endpointAudit); await LicensingDataStore.RecordEndpointThroughput( endpointAudit.Id.Name, ThroughputSource.Audit, DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-daysSinceLastThroughputEntry), - 50, - default); + 50); - Assert.That(await LicensingDataStore.IsThereThroughputForLastXDays(timeFrameToCheck, default), expectedValue ? Is.True : Is.False); + Assert.That(await LicensingDataStore.IsThereThroughputForLastXDays(timeFrameToCheck), expectedValue ? Is.True : Is.False); } [TestCase(10, 5, ThroughputSource.Monitoring, ThroughputSource.Monitoring, false, false)] @@ -258,15 +258,14 @@ public async Task Should_correctly_report_throughput_existence_for_X_days_for_sp { // Arrange var endpointAudit = new Endpoint("Endpoint", throughputSourceToRecord); - await LicensingDataStore.SaveEndpoint(endpointAudit, default); + await LicensingDataStore.SaveEndpoint(endpointAudit); await LicensingDataStore.RecordEndpointThroughput( endpointAudit.Id.Name, throughputSourceToRecord, DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-daysSinceLastThroughputEntry), - 50, - default); + 50); - Assert.That(await LicensingDataStore.IsThereThroughputForLastXDaysForSource(timeFrameToCheck, throughputSourceToCheck, includeToday, default), expectedValue ? Is.True : Is.False); + Assert.That(await LicensingDataStore.IsThereThroughputForLastXDaysForSource(timeFrameToCheck, throughputSourceToCheck, includeToday), expectedValue ? Is.True : Is.False); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/Throughput/ReportMasksTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/ReportMasksTests.cs index 9c0aca7645..209aa699da 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/ReportMasksTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/ReportMasksTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Persistence.Tests.Throughput; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -12,10 +13,10 @@ public async Task Should_retrieve_saved_report_masks() { //Arrange var expectedReportMasks = new List { "secret", "boo" }; - await LicensingDataStore.SaveReportMasks(expectedReportMasks, default); + await LicensingDataStore.SaveReportMasks(expectedReportMasks); //Act - var retrievedReportMasks = await LicensingDataStore.GetReportMasks(default); + var retrievedReportMasks = await LicensingDataStore.GetReportMasks(); //Assert Assert.That(retrievedReportMasks, Is.Not.Null); @@ -27,12 +28,12 @@ public async Task Should_update_existing_report_masks_if_already_exists() { // Arrange var oldReportMasks = new List { "secret", "boo" }; - await LicensingDataStore.SaveReportMasks(oldReportMasks, default); + await LicensingDataStore.SaveReportMasks(oldReportMasks); // Act var expectedReportMasks = new List { "secret", "hello" }; - await LicensingDataStore.SaveReportMasks(expectedReportMasks, default); - var retrievedReportMasks = await LicensingDataStore.GetReportMasks(default); + await LicensingDataStore.SaveReportMasks(expectedReportMasks); + var retrievedReportMasks = await LicensingDataStore.GetReportMasks(); // Assert Assert.That(retrievedReportMasks, Is.Not.Null); diff --git a/src/ServiceControl.Persistence.Tests/TrialLicenseDataProviderTests.cs b/src/ServiceControl.Persistence.Tests/TrialLicenseDataProviderTests.cs index ca48e29042..88270cb5ef 100644 --- a/src/ServiceControl.Persistence.Tests/TrialLicenseDataProviderTests.cs +++ b/src/ServiceControl.Persistence.Tests/TrialLicenseDataProviderTests.cs @@ -10,7 +10,7 @@ namespace ServiceControl.Persistence.Tests; class TrialLicenseDataProviderTests : PersistenceTestBase { [Test, CancelAfter(30_000)] - public async Task GetTrialEndDate_returns_null_by_default(CancellationToken cancellationToken) + public async Task GetTrialEndDate_returns_null_by_default(CancellationToken cancellationToken = default) { var trialLicenseDataProvider = ServiceProvider.GetRequiredService(); @@ -20,7 +20,7 @@ public async Task GetTrialEndDate_returns_null_by_default(CancellationToken canc } [Test, CancelAfter(30_000)] - public async Task StoreTrialEndDate_persists_value(CancellationToken cancellationToken) + public async Task StoreTrialEndDate_persists_value(CancellationToken cancellationToken = default) { var trialLicenseDataProvider = ServiceProvider.GetRequiredService(); var expectedEndDate = DateOnly.FromDateTime(DateTime.UtcNow.Date.AddDays(13)); diff --git a/src/ServiceControl.UnitTests/.editorconfig b/src/ServiceControl.UnitTests/.editorconfig index d9b7048273..062de7d0db 100644 --- a/src/ServiceControl.UnitTests/.editorconfig +++ b/src/ServiceControl.UnitTests/.editorconfig @@ -6,7 +6,5 @@ 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.PS0013.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.UnitTests/Licensing/ActiveLicenseTests.cs b/src/ServiceControl.UnitTests/Licensing/ActiveLicenseTests.cs index b41dd6c800..00a93ffc04 100644 --- a/src/ServiceControl.UnitTests/Licensing/ActiveLicenseTests.cs +++ b/src/ServiceControl.UnitTests/Licensing/ActiveLicenseTests.cs @@ -70,9 +70,9 @@ public FakeDataProvider() : this(null) public FakeDataProvider(TrialMetadata metadata) => this.metadata = metadata; - public Task GetTrialEndDate(CancellationToken cancellationToken) => Task.FromResult(metadata?.TrialEndDate); + public Task GetTrialEndDate(CancellationToken cancellationToken = default) => Task.FromResult(metadata?.TrialEndDate); - public Task StoreTrialEndDate(DateOnly trialEndDate, CancellationToken cancellationToken) + public Task StoreTrialEndDate(DateOnly trialEndDate, CancellationToken cancellationToken = default) { metadata ??= new TrialMetadata(); metadata.TrialEndDate = trialEndDate; diff --git a/src/ServiceControl.UnitTests/Monitoring/EndpointInstanceMonitoringTests.cs b/src/ServiceControl.UnitTests/Monitoring/EndpointInstanceMonitoringTests.cs index 9ab5eac338..da66c2de3a 100644 --- a/src/ServiceControl.UnitTests/Monitoring/EndpointInstanceMonitoringTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/EndpointInstanceMonitoringTests.cs @@ -35,7 +35,7 @@ public async Task When_endpoint_removed_should_stay_removed() class FakeDomainEvents : IDomainEvents { - public Task Raise(T domainEvent, CancellationToken cancellationToken) where T : IDomainEvent + public Task Raise(T domainEvent, CancellationToken cancellationToken = default) where T : IDomainEvent { return Task.CompletedTask; } diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 6b8b1ca1c8..2f2252afe2 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -242,15 +242,15 @@ public void RecordHeartbeat(EndpointInstanceId endpointInstanceId, DateTime time class MockEndpointSettingsStore(EndpointSettings[] settings) : IEndpointSettingsStore { - public IAsyncEnumerable GetAllEndpointSettings(CancellationToken token) => settings.ToAsyncEnumerable(); + public IAsyncEnumerable GetAllEndpointSettings(CancellationToken cancellationToken = default) => settings.ToAsyncEnumerable(); - public Task UpdateEndpointSettings(EndpointSettings settings, CancellationToken token) + public Task UpdateEndpointSettings(EndpointSettings settings, CancellationToken cancellationToken = default) { Updated.Add(settings); return Task.CompletedTask; } - public Task Delete(string name, CancellationToken cancellationToken) + public Task Delete(string name, CancellationToken cancellationToken = default) { Deleted.Add(name); return Task.CompletedTask; From 64c70a5c3ce31938af967a3139d292cbce485444 Mon Sep 17 00:00:00 2001 From: John Simons Date: Wed, 12 Aug 2026 17:50:13 +1000 Subject: [PATCH 10/12] Await the deployed instances refresh instead of firing it as async void ListInstancesViewModel.AddAndRemoveInstances was a genuine async void, not an event handler. It returned at its first await, so HandleAsync(RefreshInstances) published PostRefreshInstances while the removals were still in flight. That is the exact ordering the method's own remarks say must not happen: deleting an instance in PowerShell could then error out a deleted instance viewmodel trying to refresh itself. It is now async Task and awaited. The only thing that kept it async void was the constructor call site, which cannot await; that call moves to RxScreen.OnInitialize, which takes a token and is awaited by IActivate.ActivateAsync. The list therefore populates on activation rather than construction. The CorruptInstanceConfiguration specs construct the viewmodel directly, so they now activate it before asserting. Deferred since Phase 0 of the cancellation work, and unblocked by OnInitialize gaining a CancellationToken. --- .../CorruptInstanceConfiguration.cs | 24 ++++++++++++------- .../ListInstances/ListInstancesViewModel.cs | 14 ++++------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ServiceControl.Config.Tests/InstanceDetails/CorruptInstanceConfiguration.cs b/src/ServiceControl.Config.Tests/InstanceDetails/CorruptInstanceConfiguration.cs index 5245eac65a..c96a82ac60 100644 --- a/src/ServiceControl.Config.Tests/InstanceDetails/CorruptInstanceConfiguration.cs +++ b/src/ServiceControl.Config.Tests/InstanceDetails/CorruptInstanceConfiguration.cs @@ -253,6 +253,7 @@ public async Task The_one_where_the_fix_is_picked_up_through_the_deployed_instan { EventAggregator = new EventAggregator() }; + await ((IActivate)list).ActivateAsync(TestContext.CurrentContext.CancellationToken); Assert.That(list.HasConfigurationErrors, Is.True, "Precondition: the list starts out with a corrupt instance"); // The operator fixes the file, then triggers the refresh the UI uses @@ -294,12 +295,12 @@ public void The_one_where_a_refresh_tries_to_apply_data_from_an_instance_of_a_di public class Rule_5_Must_summarize_configuration_errors_above_the_instance_list : CorruptInstanceConfigurationFixture { [Test] - public void The_one_where_a_single_instance_is_corrupt_and_the_banner_names_it() + public async Task The_one_where_a_single_instance_is_corrupt_and_the_banner_names_it() { WriteErrorInstanceConfig(CorruptXml); WriteAuditInstanceConfig(ValidAuditInstanceXml); - var list = ListFor(LoadErrorInstance(), LoadAuditInstance("Particular.ServiceControl.Audit")); + var list = await ListFor(LoadErrorInstance(), LoadAuditInstance("Particular.ServiceControl.Audit")); using (Assert.EnterMultipleScope()) { @@ -310,23 +311,23 @@ public void The_one_where_a_single_instance_is_corrupt_and_the_banner_names_it() } [Test] - public void The_one_where_multiple_instances_are_corrupt_and_the_banner_lists_all_of_them() + public async Task The_one_where_multiple_instances_are_corrupt_and_the_banner_lists_all_of_them() { WriteErrorInstanceConfig(CorruptXml); WriteAuditInstanceConfig(CorruptXml); - var list = ListFor(LoadErrorInstance(), LoadAuditInstance("Particular.ServiceControl.Audit")); + var list = await ListFor(LoadErrorInstance(), LoadAuditInstance("Particular.ServiceControl.Audit")); Assert.That(list.ConfigurationErrorMessage, Is.EqualTo("Multiple instances (Particular.ServiceControl, Particular.ServiceControl.Audit) cannot be loaded due to XML configuration errors.")); } [Test] - public void The_one_where_all_configurations_are_valid_and_no_banner_is_shown() + public async Task The_one_where_all_configurations_are_valid_and_no_banner_is_shown() { WriteErrorInstanceConfig(ValidErrorInstanceXml); - var list = ListFor(LoadErrorInstance()); + var list = await ListFor(LoadErrorInstance()); using (Assert.EnterMultipleScope()) { @@ -400,8 +401,15 @@ protected MonitoringInstance LoadMonitoringInstance(string serviceName = Service internal static InstanceDetailsViewModel DetailsFor(BaseService instance) => new(instance, null, null, null, null, null, null, null, null); - internal static ListInstancesViewModel ListFor(params BaseService[] instances) => - new(DetailsFor, () => instances); + // The list populates in OnInitialize, so it has to be activated before it has anything in it +#pragma warning disable PS0018 // A params array must be the last parameter, so a trailing CancellationToken cannot be added + internal static async Task ListFor(params BaseService[] instances) +#pragma warning restore PS0018 + { + var list = new ListInstancesViewModel(DetailsFor, () => instances); + await ((IActivate)list).ActivateAsync(TestContext.CurrentContext.CancellationToken); + return list; + } class FakeWindowsServiceController(string exePath, string serviceName) : IWindowsServiceController { diff --git a/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs b/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs index bb89720cd6..6ec76f53f2 100644 --- a/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs +++ b/src/ServiceControl.Config/UI/ListInstances/ListInstancesViewModel.cs @@ -32,10 +32,10 @@ internal ListInstancesViewModel(Func inst CopyToClipboard = new CopyToClipboardCommand(); Instances = []; - - AddAndRemoveInstances(); } + protected override Task OnInitialize(CancellationToken cancellationToken = default) => AddAndRemoveInstances(cancellationToken); + public CopyToClipboardCommand CopyToClipboard { get; } public BindableCollection OrderedInstances => [.. Instances.OrderBy(x => x.Name)]; @@ -107,7 +107,7 @@ public Task HandleAsync(LicenseUpdated licenseUpdatedEvent, CancellationToken ca /// public async Task HandleAsync(RefreshInstances message, CancellationToken cancellationToken = default) { - AddAndRemoveInstances(); + await AddAndRemoveInstances(cancellationToken); await EventAggregator.PublishOnUIThreadAsync(new PostRefreshInstances(), cancellationToken); } @@ -127,12 +127,7 @@ public async Task HandleAsync(ResetInstances message, CancellationToken cancella NotifyOfPropertyChange(nameof(Instances)); } - // TODO: this is a genuine async void, not an event handler. Because it returns at the first - // await, HandleAsync(RefreshInstances) publishes PostRefreshInstances before the removals have - // finished, which is the ordering that method's own remarks say must not happen. Converting it - // to async Task needs the constructor call site at the top of this class restructured first. -#pragma warning disable PS0027 - async void AddAndRemoveInstances() + async Task AddAndRemoveInstances(CancellationToken cancellationToken) { // Remove instances that no longer exist on disk var toRemove = Instances.Where(instance => !instance.Exists()).ToList(); @@ -169,7 +164,6 @@ async void AddAndRemoveInstances() NotifyOfPropertyChange(nameof(HasConfigurationErrors)); NotifyOfPropertyChange(nameof(ConfigurationErrorMessage)); } -#pragma warning restore PS0027 readonly Func instanceDetailsFunc; readonly Func> getAllInstances; From 1287f375ca87c9bc4a45b2e70907373dd2ecc0a7 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 15:44:10 -0700 Subject: [PATCH 11/12] fix flaky HeartbeatEndpointSettingsSyncHostedService tests by polling instead of fixed delay --- ...tEndpointSettingsSyncHostedServiceTests.cs | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 2f2252afe2..17b6177033 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,23 +17,37 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { + // Background service work happens on the thread pool asynchronously. Waiting a fixed + // wall-clock duration before asserting is flaky on slower/loaded CI machines because the + // work may not have completed yet. Instead, poll for the expected condition until it is + // met or a generous timeout elapses. + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10)); + while (!condition() && DateTime.UtcNow < deadline) + { + await Task.Delay(20); + } + } + [Test] public async Task Should_handle_cancellation_token_gracefully() { using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore([]), new MockEndpointSettingsStore([]), - new MockEndpointInstanceMonitoring([]), new Settings { TrackInstancesInitialValue = true }, + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = true }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(service.ExecuteTask?.IsCompletedSuccessfully, Is.True); @@ -59,7 +73,7 @@ public async Task Should_delete_settings_from_endpoints_that_are_no_longer_live( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Deleted.Count, Is.EqualTo(2)); @@ -86,7 +100,7 @@ public async Task Should_set_the_default_for_settings_if_does_not_exist_already( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Updated.Count >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(1)); @@ -106,11 +120,12 @@ public async Task Should_not_set_the_default_if_already_exists() var mockEndpointSettingsStore = new MockEndpointSettingsStore([ new EndpointSettings { Name = string.Empty, TrackInstances = expectedTrackInstancesInitialValue } ]); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore( []), mockEndpointSettingsStore, - new MockEndpointInstanceMonitoring([]), + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = expectedTrackInstancesInitialValue }, fakeTimeProvider, NullLogger.Instance) { @@ -118,7 +133,7 @@ public async Task Should_not_set_the_default_if_already_exists() }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(0)); @@ -158,7 +173,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockMonitoringDataStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(2)); @@ -197,7 +212,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(0)); @@ -220,7 +235,16 @@ public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, b public Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public EndpointsView[] GetEndpoints() => endpointsViews; + public EndpointsView[] GetEndpoints() + { + GetEndpointsCallCount++; + return endpointsViews; + } + + // GetEndpoints() is called by PurgeMonitoringDataThatDoesNotNeedToBeTracked, which runs + // at the end of each sync cycle. Waiting for this to be invoked gives a deterministic + // signal that a full sync cycle has completed, without relying on a fixed wall-clock delay. + public int GetEndpointsCallCount { get; private set; } public List GetKnownEndpoints() => throw new NotImplementedException(); From fa50be24666d30c8edb96733254d14b729e8e9c6 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 15:46:51 -0700 Subject: [PATCH 12/12] remove comment --- .../HeartbeatEndpointSettingsSyncHostedServiceTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 17b6177033..29acd797af 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,10 +17,7 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { - // Background service work happens on the thread pool asynchronously. Waiting a fixed - // wall-clock duration before asserting is flaky on slower/loaded CI machines because the - // work may not have completed yet. Instead, poll for the expected condition until it is - // met or a generous timeout elapses. + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) { var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10));