diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs index ef2d140575..eb7871618b 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs @@ -3,6 +3,7 @@ namespace ServiceControl.AcceptanceTests.Recoverability.ExternalIntegration using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using AcceptanceTesting.EndpointTemplates; @@ -79,7 +80,7 @@ class FaultyPublisher(MyContext context) : IEventPublisher public object CreateDispatchContext(IDomainEvent @event) => null; - public Task> PublishEventsForOwnContexts(IEnumerable allContexts) + public Task> PublishEventsForOwnContexts(IEnumerable allContexts, CancellationToken cancellationToken = default) { if (!failed) { diff --git a/src/ServiceControl.Audit/.editorconfig b/src/ServiceControl.Audit/.editorconfig index c83c803ef4..d5e4367c81 100644 --- a/src/ServiceControl.Audit/.editorconfig +++ b/src/ServiceControl.Audit/.editorconfig @@ -3,13 +3,14 @@ # 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 +# Blocked on the transport abstraction. TransportCustomization.CreateTransportInfrastructure takes +# Func for onCriticalError, so the critical-error path here cannot carry a +# token until that signature gains one in the transports phase. +[{Auditing/AuditIngestion.cs,Auditing/AuditIngestionFaultPolicy.cs,Auditing/ImportFailureCircuitBreaker.cs}] dotnet_diagnostic.PS0013.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none + +# Blocked on IFailedAuditStorage.ProcessFailedMessages, whose callback is handed its own token by the +# store. The catch filter here correctly tests that one, which PS0020 cannot see. +[Auditing/ImportFailedAudits.cs] dotnet_diagnostic.PS0020.severity = none -dotnet_diagnostic.PS0021.severity = none diff --git a/src/ServiceControl.Audit/Auditing/AuditIngestion.cs b/src/ServiceControl.Audit/Auditing/AuditIngestion.cs index ea417c25a8..0b14b2724d 100644 --- a/src/ServiceControl.Audit/Auditing/AuditIngestion.cs +++ b/src/ServiceControl.Audit/Auditing/AuditIngestion.cs @@ -93,12 +93,20 @@ async Task EnsureStarted(CancellationToken cancellationToken) await StopAndTeardownInfrastructure(cancellationToken); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { try { await StopAndTeardownInfrastructure(cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception teardownException) { throw new AggregateException(e, teardownException); @@ -139,6 +147,10 @@ async Task SetUpAndStartInfrastructure(CancellationToken cancellationToken) logger.LogInformation(LogMessages.StartedInfrastructure); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogError(e, "Failed to start infrastructure"); @@ -174,6 +186,10 @@ async Task StopAndTeardownInfrastructure(CancellationToken cancellationToken) logger.LogInformation(LogMessages.StoppedInfrastructure); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogError(e, "Failed to stop infrastructure"); @@ -216,19 +232,19 @@ async Task OnMessage(MessageContext messageContext, CancellationToken cancellati messageIngestionMetrics.Success(); } - public override async Task StartAsync(CancellationToken cancellationToken) + public override async Task StartAsync(CancellationToken cancellationToken = default) { await watchdog.Start(() => applicationLifetime.StopApplication(), cancellationToken); await base.StartAsync(cancellationToken); } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { try { var contexts = new List(MaxBatchSize); - while (await channel.Reader.WaitToReadAsync(stoppingToken)) + while (await channel.Reader.WaitToReadAsync(cancellationToken)) { // will only enter here if there is something to read. try @@ -241,11 +257,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) contexts.Add(context); } - await auditIngestor.Ingest(contexts, stoppingToken); + await auditIngestor.Ingest(contexts, cancellationToken); batchMetrics.Complete(contexts.Count); } - catch (Exception e) + catch (OperationCanceledException e) when (cancellationToken.IsCancellationRequested) { // signal all message handling tasks to terminate foreach (var context in contexts) @@ -253,10 +269,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _ = context.GetTaskCompletionSource().TrySetException(e); } - if (e is OperationCanceledException && stoppingToken.IsCancellationRequested) + logger.LogInformation(e, "Batch cancelled"); + break; + } + catch (Exception e) + { + // signal all message handling tasks to terminate + foreach (var context in contexts) { - logger.LogInformation(e, "Batch cancelled"); - break; + _ = context.GetTaskCompletionSource().TrySetException(e); } logger.LogInformation(e, "Ingesting messages failed"); @@ -268,13 +289,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } // will fall out here when writer is completed } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // ExecuteAsync cancelled } } - public override async Task StopAsync(CancellationToken cancellationToken) + public override async Task StopAsync(CancellationToken cancellationToken = default) { try { diff --git a/src/ServiceControl.Audit/Auditing/AuditIngestor.cs b/src/ServiceControl.Audit/Auditing/AuditIngestor.cs index af5bbdac58..784fd15a21 100644 --- a/src/ServiceControl.Audit/Auditing/AuditIngestor.cs +++ b/src/ServiceControl.Audit/Auditing/AuditIngestor.cs @@ -45,7 +45,7 @@ ILogger logger ); } - public async Task Ingest(List contexts, CancellationToken cancellationToken) + public async Task Ingest(List contexts, CancellationToken cancellationToken = default) { var stored = await auditPersister.Persist(contexts, cancellationToken); @@ -61,6 +61,10 @@ public async Task Ingest(List contexts, CancellationToken cancel context.GetTaskCompletionSource().TrySetResult(true); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogWarning(e, "Forwarding messages failed"); @@ -101,7 +105,7 @@ Task Forward(IReadOnlyCollection messageContexts, string forward : Task.CompletedTask; } - public async Task VerifyCanReachForwardingAddress(CancellationToken cancellationToken) + public async Task VerifyCanReachForwardingAddress(CancellationToken cancellationToken = default) { if (!settings.ForwardAuditMessages) { @@ -120,6 +124,10 @@ public async Task VerifyCanReachForwardingAddress(CancellationToken cancellation await messageDispatcher.Value.Dispatch(transportOperations, new TransportTransaction(), cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { throw new Exception($"Unable to write to forwarding queue {settings.AuditLogQueue}", e); diff --git a/src/ServiceControl.Audit/Auditing/AuditPersister.cs b/src/ServiceControl.Audit/Auditing/AuditPersister.cs index e9421a27aa..fdd1c64dd4 100644 --- a/src/ServiceControl.Audit/Auditing/AuditPersister.cs +++ b/src/ServiceControl.Audit/Auditing/AuditPersister.cs @@ -22,7 +22,7 @@ class AuditPersister(IAuditIngestionUnitOfWorkFactory unitOfWorkFactory, Lazy messageDispatcher, ILogger logger) { - public async Task> Persist(IReadOnlyList contexts, CancellationToken cancellationToken) + public async Task> Persist(IReadOnlyList contexts, CancellationToken cancellationToken = default) { var storedContexts = new List(contexts.Count); IAuditIngestionUnitOfWork unitOfWork = null; @@ -33,7 +33,7 @@ public async Task> Persist(IReadOnlyList(contexts.Count); foreach (var context in contexts) { - inserts.Add(ProcessMessage(context)); + inserts.Add(ProcessMessage(context, cancellationToken)); } await Task.WhenAll(inserts); @@ -58,6 +58,10 @@ public async Task> Persist(IReadOnlyList> Persist(IReadOnlyList { @@ -65,6 +68,7 @@ await failedAuditStore.ProcessFailedMessages( } }, cancellationToken); +#pragma warning restore PS0021 logger.LogInformation("Done re-importing failed audits. Successfully re-imported {SuccessCount} messages. Failed re-importing {FailureCount} messages", succeeded, failed); diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs index 5027f7f43f..0267bb3779 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs @@ -24,7 +24,7 @@ public async Task> GetAllMessages( [FromQuery(Name = "from")] string from, [FromQuery(Name = "to")] string to, string q, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { QueryResult> result; var pagingInfo = new PagingInfo(pageSize: pageSize); diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs index 14357f0b7f..66031fcd9c 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs @@ -18,7 +18,7 @@ public class GetMessagesController(IAuditDataStore dataStore) : ControllerBase [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages")] [HttpGet] - public async Task> GetAllMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, CancellationToken cancellationToken) + public async Task> GetAllMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, CancellationToken cancellationToken = default) { var result = await dataStore.GetMessages(includeSystemMessages, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -28,7 +28,7 @@ public async Task> GetAllMessages([FromQuery] PagingInfo pag [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages")] [HttpGet] - public async Task> GetEndpointMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, string endpoint, CancellationToken cancellationToken) + public async Task> GetEndpointMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, string endpoint, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessagesByReceivingEndpoint(includeSystemMessages, endpoint, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -39,7 +39,7 @@ public async Task> GetEndpointMessages([FromQuery] PagingInf [AllowAnonymous] [Route("endpoints/{endpoint}/audit-count")] [HttpGet] - public async Task> GetEndpointAuditCounts([FromQuery] PagingInfo pagingInfo, string endpoint, CancellationToken cancellationToken) + public async Task> GetEndpointAuditCounts([FromQuery] PagingInfo pagingInfo, string endpoint, CancellationToken cancellationToken = default) { var result = await dataStore.QueryAuditCounts(endpoint, cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -49,7 +49,7 @@ public async Task> GetEndpointAuditCounts([FromQuery] PagingIn [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/{id}/body")] [HttpGet] - public async Task Get(string id, CancellationToken cancellationToken) + public async Task Get(string id, CancellationToken cancellationToken = default) { var result = await dataStore.GetMessageBody(id, cancellationToken); @@ -76,7 +76,7 @@ public async Task Get(string id, CancellationToken cancellationTo [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/search")] [HttpGet] - public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string q, CancellationToken cancellationToken) + public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string q, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessages(q, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -86,7 +86,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/search/{keyword}")] [HttpGet] - public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken) + public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessages(keyword, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -96,7 +96,7 @@ public async Task> SearchByKeyWord([FromQuery] PagingInfo pa [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages/search")] [HttpGet] - public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string q, CancellationToken cancellationToken) + public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string q, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, q, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); @@ -106,7 +106,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages/search/{keyword}")] [HttpGet] - public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken) + public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, keyword, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs index 7ba20c1a11..3b232318a2 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs @@ -17,7 +17,7 @@ public class MessagesConversationController(IAuditDataStore dataStore) : Control [Authorize(Policy = Permissions.AuditMessageView)] [Route("conversations/{conversationId}")] [HttpGet] - public async Task> Get([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string conversationId, CancellationToken cancellationToken) + public async Task> Get([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string conversationId, CancellationToken cancellationToken = default) { var result = await dataStore.QueryMessagesByConversationId(conversationId, pagingInfo, sortInfo, cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/AbstractCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/AbstractCommand.cs index 5f6de0478e..a89ce2221e 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/AbstractCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/AbstractCommand.cs @@ -1,10 +1,11 @@ namespace ServiceControl.Audit.Infrastructure.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using ServiceControl.Audit.Infrastructure.Settings; 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.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs index bff386d02e..70f5911de8 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/CommandRunner.cs @@ -1,15 +1,16 @@ namespace ServiceControl.Audit.Infrastructure.Hosting.Commands { using System; + using System.Threading; using System.Threading.Tasks; using ServiceControl.Audit.Infrastructure.Settings; 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.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs index c2b5926d58..40de68768a 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/ImportFailedAuditsCommand.cs @@ -13,7 +13,7 @@ class ImportFailedAuditsCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { settings.IngestAuditMessages = false; @@ -39,7 +39,7 @@ public override async Task Execute(HostArguments args, Settings settings) { await importer.Run(tokenSource.Token); } - catch (OperationCanceledException e) when (tokenSource.IsCancellationRequested) + catch (OperationCanceledException e) when (tokenSource.Token.IsCancellationRequested) { LoggerUtil.CreateStaticLogger().LogInformation(e, "Cancelled"); } diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs index 4a92ce6b41..7dadf6a98e 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Audit.Infrastructure.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.WindowsServices; @@ -9,7 +10,7 @@ class MaintenanceModeCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings); var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); @@ -26,7 +27,7 @@ public override async Task Execute(HostArguments args, Settings settings) } var host = hostBuilder.Build(); - await host.RunAsync(); + await host.RunAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs index 5d5d8753eb..ce2167ccbd 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Audit.Infrastructure.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using NServiceBus; @@ -10,7 +11,7 @@ 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); var assemblyScanner = endpointConfiguration.AssemblyScanner(); diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs index 73480e72e2..3260fa5add 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/SetupCommand.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using System.Runtime.InteropServices; + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -11,7 +12,7 @@ class SetupCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { if (settings.IngestAuditMessages) { @@ -45,8 +46,8 @@ public override async Task Execute(HostArguments args, Settings settings) hostBuilder.AddServiceControlAuditInstallers(settings); using var host = hostBuilder.Build(); - await host.StartAsync(); - await host.StopAsync(); + await host.StartAsync(cancellationToken); + await host.StopAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/BodyUrlRouteFix.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/BodyUrlRouteFix.cs index 52afcb4d75..b2db172507 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/BodyUrlRouteFix.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/BodyUrlRouteFix.cs @@ -8,6 +8,7 @@ namespace ServiceControl.Audit.Infrastructure.WebApi // This owin module rewrites incoming urls starting with // to just one /. This avoids breaking existing clients when removing NancyFX. class BodyUrlRouteFix(RequestDelegate next) { +#pragma warning disable PS0018 // ASP.NET Core middleware convention takes only the HttpContext public Task InvokeAsync(HttpContext context) { if (context.Request.Path.HasValue) @@ -21,5 +22,6 @@ public Task InvokeAsync(HttpContext context) return next(context); } +#pragma warning restore PS0018 } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/PagingInfoModelBinder.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/PagingInfoModelBinder.cs index 217ad1eeb6..302b2ac2f6 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/PagingInfoModelBinder.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/PagingInfoModelBinder.cs @@ -7,6 +7,7 @@ namespace ServiceControl.Audit.Infrastructure.WebApi; public class PagingInfoModelBinder : IModelBinder { +#pragma warning disable PS0018 // IModelBinder declares this without a CancellationToken public Task BindModelAsync(ModelBindingContext bindingContext) { ArgumentNullException.ThrowIfNull(bindingContext); @@ -28,4 +29,5 @@ public Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.Result = ModelBindingResult.Success(new PagingInfo(page, maxResultsPerPage)); return Task.CompletedTask; } -} \ No newline at end of file +#pragma warning restore PS0018 +} diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/SortInfoModelBinder.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/SortInfoModelBinder.cs index de8b6c0290..57923a06bf 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/SortInfoModelBinder.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/SortInfoModelBinder.cs @@ -6,6 +6,7 @@ namespace ServiceControl.Audit.Infrastructure.WebApi; public class SortInfoModelBinder : IModelBinder { +#pragma warning disable PS0018 // IModelBinder declares this without a CancellationToken public Task BindModelAsync(ModelBindingContext bindingContext) { ArgumentNullException.ThrowIfNull(bindingContext); @@ -27,4 +28,5 @@ public Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.Result = ModelBindingResult.Success(new SortInfo(sort, direction)); return Task.CompletedTask; } -} \ No newline at end of file +#pragma warning restore PS0018 +} diff --git a/src/ServiceControl.Audit/SagaAudit/SagasController.cs b/src/ServiceControl.Audit/SagaAudit/SagasController.cs index 3d92d5f913..ed3612652e 100644 --- a/src/ServiceControl.Audit/SagaAudit/SagasController.cs +++ b/src/ServiceControl.Audit/SagaAudit/SagasController.cs @@ -18,7 +18,7 @@ public class SagasController(IAuditDataStore dataStore) : ControllerBase [Authorize(Policy = Permissions.AuditSagaView)] [Route("sagas/{id}")] [HttpGet] - public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken) + public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken = default) { var result = await dataStore.QuerySagaHistoryById(id, cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); diff --git a/src/ServiceControl.Infrastructure/.editorconfig b/src/ServiceControl.Infrastructure/.editorconfig deleted file mode 100644 index 1ca766dd45..0000000000 --- a/src/ServiceControl.Infrastructure/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -# Cancellation analyzer debt, narrowed to the one file still owed. MessageActionAuditLogExtensions -# takes a Func callback that has to become Func, which changes the -# lambda at a dozen controller call sites, so it moves with the controllers in Phase 2. -[Auth/MessageActionAuditLogExtensions.cs] -dotnet_diagnostic.PS0013.severity = none -dotnet_diagnostic.PS0018.severity = none diff --git a/src/ServiceControl.Infrastructure/Auth/MessageActionAuditLogExtensions.cs b/src/ServiceControl.Infrastructure/Auth/MessageActionAuditLogExtensions.cs index 90990bafc0..f91aa3fb46 100644 --- a/src/ServiceControl.Infrastructure/Auth/MessageActionAuditLogExtensions.cs +++ b/src/ServiceControl.Infrastructure/Auth/MessageActionAuditLogExtensions.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Infrastructure.Auth; using System; +using System.Threading; using System.Threading.Tasks; public static class MessageActionAuditLogExtensions @@ -14,12 +15,19 @@ public static class MessageActionAuditLogExtensions /// public static async Task AuditedOperation(this IMessageActionAuditLog auditLog, AuditUser user, MessageActionKind kind, string permission, MessageActionScope scope, string? resource, - int? count, string operationId, Func action) + int? count, string operationId, Func action, + CancellationToken cancellationToken = default) { var success = true; try { - await action().ConfigureAwait(false); + await action(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // A cancelled operation did not complete, so it is recorded as a failure like any other. + success = false; + throw; } catch { diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs index c2ee36ffa9..5277ccf869 100644 --- a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -431,11 +431,11 @@ public CustomRetriesGateway(bool progressToStaged, IRetryBatchStore store, Retry this.progressToStaged = progressToStaged; } - protected override Task MoveBatchToStaging(string batchId) + protected override Task MoveBatchToStaging(string batchId, CancellationToken cancellationToken = default) { if (progressToStaged) { - return base.MoveBatchToStaging(batchId); + return base.MoveBatchToStaging(batchId, cancellationToken); } return Task.CompletedTask; @@ -453,11 +453,11 @@ public CustomRetryDocumentManager(bool progressToStaged, IRetryBatchStore retryS this.progressToStaged = progressToStaged; } - public override Task MoveBatchToStaging(string batchId) + public override Task MoveBatchToStaging(string batchId, CancellationToken cancellationToken = default) { if (progressToStaged) { - return base.MoveBatchToStaging(batchId); + return base.MoveBatchToStaging(batchId, cancellationToken); } return Task.CompletedTask; diff --git a/src/ServiceControl.Persistence/IEndpointInstanceMonitoring.cs b/src/ServiceControl.Persistence/IEndpointInstanceMonitoring.cs index 63b6788289..2ea93fc5fa 100644 --- a/src/ServiceControl.Persistence/IEndpointInstanceMonitoring.cs +++ b/src/ServiceControl.Persistence/IEndpointInstanceMonitoring.cs @@ -2,17 +2,18 @@ { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using ServiceControl.Operations; public interface IEndpointInstanceMonitoring { - Task CheckEndpoints(DateTime threshold); - Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt); + Task CheckEndpoints(DateTime threshold, CancellationToken cancellationToken = default); + Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt, CancellationToken cancellationToken = default); void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, bool monitored); - Task DisableMonitoring(Guid id); - Task EnableMonitoring(Guid id); - Task EndpointDetected(EndpointDetails newEndpointDetails); + Task DisableMonitoring(Guid id, CancellationToken cancellationToken = default); + Task EnableMonitoring(Guid id, CancellationToken cancellationToken = default); + Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default); EndpointsView[] GetEndpoints(); List GetKnownEndpoints(); EndpointMonitoringStats GetStats(); diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index 6ffe825713..264be98787 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -1,77 +1,77 @@ -GET / => ServiceControl.Infrastructure.WebApi.RootController:Urls() -GET /archive/groups/id/{groupId:required:minlength(1)} => ServiceControl.MessageFailures.Api.ArchiveMessagesController:GetGroup(String groupId, String status, String modified) -GET /configuration => ServiceControl.Infrastructure.WebApi.RootController:Config() +GET / => ServiceControl.Infrastructure.WebApi.RootController:Urls(CancellationToken cancellationToken) +GET /archive/groups/id/{groupId:required:minlength(1)} => ServiceControl.MessageFailures.Api.ArchiveMessagesController:GetGroup(String groupId, String status, String modified, CancellationToken cancellationToken) +GET /configuration => ServiceControl.Infrastructure.WebApi.RootController:Config(CancellationToken cancellationToken) GET /configuration => ServiceControl.Authentication.AuthenticationController:Configuration() GET /configuration/remotes => ServiceControl.Infrastructure.WebApi.RootController:RemoteConfig(CancellationToken cancellationToken) -GET /connection => ServiceControl.Connection.ConnectionController:GetConnectionDetails() -GET /conversations/{conversationId:required:minlength(1)} => ServiceControl.CompositeViews.Messages.GetMessagesByConversationController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, String conversationId) -GET /customchecks => ServiceControl.CustomChecks.CustomCheckController:CustomChecks(PagingInfo pagingInfo, String status) -DELETE /customchecks/{id} => ServiceControl.CustomChecks.CustomCheckController:Delete(Guid id) -POST /edit/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.EditFailedMessagesController:Edit(String failedMessageId, EditMessageModel edit) +GET /connection => ServiceControl.Connection.ConnectionController:GetConnectionDetails(CancellationToken cancellationToken) +GET /conversations/{conversationId:required:minlength(1)} => ServiceControl.CompositeViews.Messages.GetMessagesByConversationController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, String conversationId, CancellationToken cancellationToken) +GET /customchecks => ServiceControl.CustomChecks.CustomCheckController:CustomChecks(PagingInfo pagingInfo, String status, CancellationToken cancellationToken) +DELETE /customchecks/{id} => ServiceControl.CustomChecks.CustomCheckController:Delete(Guid id, CancellationToken cancellationToken) +POST /edit/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.EditFailedMessagesController:Edit(String failedMessageId, EditMessageModel edit, CancellationToken cancellationToken) GET /edit/config => ServiceControl.MessageFailures.Api.EditFailedMessagesController:Config() GET /endpoints => ServiceControl.Monitoring.EndpointsMonitoringController:Endpoints() OPTIONS /endpoints => ServiceControl.Monitoring.EndpointsMonitoringController:GetSupportedOperations() -GET /endpoints/{endpoint}/audit-count => ServiceControl.CompositeViews.Messages.GetMessagesController:GetEndpointAuditCounts(PagingInfo pagingInfo, String endpoint) -GET /endpoints/{endpoint}/messages => ServiceControl.CompositeViews.Messages.GetMessagesController:MessagesForEndpoint(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, String endpoint) -GET /endpoints/{endpoint}/messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String endpoint, String q) -GET /endpoints/{endpoint}/messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyword(PagingInfo pagingInfo, SortInfo sortInfo, String endpoint, String keyword) -DELETE /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:DeleteEndpoint(Guid endpointId) -PATCH /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:Monitoring(Guid endpointId, EndpointUpdateModel data) -GET /endpoints/{endpointname}/errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsByEndpointName(PagingInfo pagingInfo, SortInfo sortInfo, String status, String modified, String endpointName) +GET /endpoints/{endpoint}/audit-count => ServiceControl.CompositeViews.Messages.GetMessagesController:GetEndpointAuditCounts(PagingInfo pagingInfo, String endpoint, CancellationToken cancellationToken) +GET /endpoints/{endpoint}/messages => ServiceControl.CompositeViews.Messages.GetMessagesController:MessagesForEndpoint(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, String endpoint, CancellationToken cancellationToken) +GET /endpoints/{endpoint}/messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String endpoint, String q, CancellationToken cancellationToken) +GET /endpoints/{endpoint}/messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyword(PagingInfo pagingInfo, SortInfo sortInfo, String endpoint, String keyword, CancellationToken cancellationToken) +DELETE /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:DeleteEndpoint(Guid endpointId, CancellationToken cancellationToken) +PATCH /endpoints/{endpointId} => ServiceControl.Monitoring.EndpointsMonitoringController:Monitoring(Guid endpointId, EndpointUpdateModel data, CancellationToken cancellationToken) +GET /endpoints/{endpointname}/errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsByEndpointName(PagingInfo pagingInfo, SortInfo sortInfo, String status, String modified, String endpointName, CancellationToken cancellationToken) GET /endpoints/known => ServiceControl.Monitoring.EndpointsMonitoringController:KnownEndpoints(PagingInfo pagingInfo) -GET /endpointssettings => ServiceControl.Monitoring.EndpointsSettingsController:Endpoints(CancellationToken token) -PATCH /endpointssettings/{endpointName?} => ServiceControl.Monitoring.EndpointsSettingsController:UpdateTrackingSetting(String endpointName, EndpointSettingsUpdateModel data, CancellationToken token) -GET /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsGet(PagingInfo pagingInfo, SortInfo sortInfo, String status, String modified, String queueAddress) -HEAD /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsHead(String status, String modified, String queueAddress) -POST /errors/{endpointName:required:minlength(1)}/retry/all => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllByEndpoint(String endpointName) -GET /errors/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.GetErrorByIdController:ErrorBy(String failedMessageId) -POST /errors/{failedMessageId:required:minlength(1)}/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryMessageBy(String instanceId, String failedMessageId) -PATCH /errors/{from}...{to}/unarchive => ServiceControl.MessageFailures.Api.UnArchiveMessagesController:Unarchive(String from, String to) -PATCH/POST /errors/{messageId:required:minlength(1)}/archive => ServiceControl.MessageFailures.Api.ArchiveMessagesController:Archive(String messageId) -PATCH/POST /errors/archive => ServiceControl.MessageFailures.Api.ArchiveMessagesController:ArchiveBatch(String[] messageIds) -GET /errors/groups/{classifier?} => ServiceControl.MessageFailures.Api.ArchiveMessagesController:GetArchiveMessageGroups(String classifier) -GET /errors/last/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.GetErrorByIdController:ErrorLastBy(String failedMessageId) -POST /errors/queues/{queueAddress:required:minlength(1)}/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllBy(String queueAddress) -GET /errors/queues/addresses => ServiceControl.MessageFailures.Api.QueueAddressController:GetAddresses(PagingInfo pagingInfo) -POST /errors/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllBy(List messageIds) -POST /errors/retry/all => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAll() -GET /errors/summary => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsSummary() -PATCH /errors/unarchive => ServiceControl.MessageFailures.Api.UnArchiveMessagesController:Unarchive(String[] ids) -GET /eventlogitems => ServiceControl.EventLog.EventLogApiController:Items(PagingInfo pagingInfo) +GET /endpointssettings => ServiceControl.Monitoring.EndpointsSettingsController:Endpoints(CancellationToken cancellationToken) +PATCH /endpointssettings/{endpointName?} => ServiceControl.Monitoring.EndpointsSettingsController:UpdateTrackingSetting(String endpointName, EndpointSettingsUpdateModel data, CancellationToken cancellationToken) +GET /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsGet(PagingInfo pagingInfo, SortInfo sortInfo, String status, String modified, String queueAddress, CancellationToken cancellationToken) +HEAD /errors => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsHead(String status, String modified, String queueAddress, CancellationToken cancellationToken) +POST /errors/{endpointName:required:minlength(1)}/retry/all => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllByEndpoint(String endpointName, CancellationToken cancellationToken) +GET /errors/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.GetErrorByIdController:ErrorBy(String failedMessageId, CancellationToken cancellationToken) +POST /errors/{failedMessageId:required:minlength(1)}/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryMessageBy(String instanceId, String failedMessageId, CancellationToken cancellationToken) +PATCH /errors/{from}...{to}/unarchive => ServiceControl.MessageFailures.Api.UnArchiveMessagesController:Unarchive(String from, String to, CancellationToken cancellationToken) +PATCH/POST /errors/{messageId:required:minlength(1)}/archive => ServiceControl.MessageFailures.Api.ArchiveMessagesController:Archive(String messageId, CancellationToken cancellationToken) +PATCH/POST /errors/archive => ServiceControl.MessageFailures.Api.ArchiveMessagesController:ArchiveBatch(String[] messageIds, CancellationToken cancellationToken) +GET /errors/groups/{classifier?} => ServiceControl.MessageFailures.Api.ArchiveMessagesController:GetArchiveMessageGroups(String classifier, CancellationToken cancellationToken) +GET /errors/last/{failedMessageId:required:minlength(1)} => ServiceControl.MessageFailures.Api.GetErrorByIdController:ErrorLastBy(String failedMessageId, CancellationToken cancellationToken) +POST /errors/queues/{queueAddress:required:minlength(1)}/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllBy(String queueAddress, CancellationToken cancellationToken) +GET /errors/queues/addresses => ServiceControl.MessageFailures.Api.QueueAddressController:GetAddresses(PagingInfo pagingInfo, CancellationToken cancellationToken) +POST /errors/retry => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAllBy(List messageIds, CancellationToken cancellationToken) +POST /errors/retry/all => ServiceControl.MessageFailures.Api.RetryMessagesController:RetryAll(CancellationToken cancellationToken) +GET /errors/summary => ServiceControl.MessageFailures.Api.GetAllErrorsController:ErrorsSummary(CancellationToken cancellationToken) +PATCH /errors/unarchive => ServiceControl.MessageFailures.Api.UnArchiveMessagesController:Unarchive(String[] ids, CancellationToken cancellationToken) +GET /eventlogitems => ServiceControl.EventLog.EventLogApiController:Items(PagingInfo pagingInfo, CancellationToken cancellationToken) GET /heartbeats/stats => ServiceControl.Monitoring.EndpointsMonitoringController:HeartbeatStats() -GET /instance-info => ServiceControl.Infrastructure.WebApi.RootController:Config() +GET /instance-info => ServiceControl.Infrastructure.WebApi.RootController:Config(CancellationToken cancellationToken) GET /license => ServiceControl.Licensing.LicenseController:License(Boolean refresh, String clientName, CancellationToken cancellationToken) GET /license/details => ServiceControl.Licensing.LicenseController:LicenseDetails(CancellationToken cancellationToken) POST /license/detailsUpload => ServiceControl.Licensing.LicenseController:UploadLicenseDetails(IFormFile file, CancellationToken cancellationToken) -GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages) -GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId) -GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q) -GET /messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyWord(PagingInfo pagingInfo, SortInfo sortInfo, String keyword) -GET /messages2 => ServiceControl.CompositeViews.Messages.GetMessages2Controller:Messages(SortInfo sortInfo, Int32 pageSize, String endpointName, String from, String to, String q) +GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, CancellationToken cancellationToken) +GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId, CancellationToken cancellationToken) +GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q, CancellationToken cancellationToken) +GET /messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyWord(PagingInfo pagingInfo, SortInfo sortInfo, String keyword, CancellationToken cancellationToken) +GET /messages2 => ServiceControl.CompositeViews.Messages.GetMessages2Controller:Messages(SortInfo sortInfo, Int32 pageSize, String endpointName, String from, String to, String q, CancellationToken cancellationToken) GET /my/routes => ServiceControl.Hosting.Auth.MyRoutesController:GetMyRoutes() -GET /notifications/email => ServiceControl.Notifications.Api.NotificationsController:GetEmailNotificationsSettings() -POST /notifications/email => ServiceControl.Notifications.Api.NotificationsController:UpdateSettings(UpdateEmailNotificationsSettingsRequest request) -POST /notifications/email/test => ServiceControl.Notifications.Api.NotificationsController:SendTestEmail() -POST /notifications/email/toggle => ServiceControl.Notifications.Api.NotificationsController:ToggleEmailNotifications(ToggleEmailNotifications request) -PATCH /pendingretries/queues/resolve => ServiceControl.MessageFailures.Api.ResolveMessagesController:ResolveBy(QueueModel queueModel) -POST /pendingretries/queues/retry => ServiceControl.MessageFailures.Api.PendingRetryMessagesController:RetryBy(PendingRetryRequest request) -PATCH /pendingretries/resolve => ServiceControl.MessageFailures.Api.ResolveMessagesController:ResolveBy(UniqueMessageIdsModel request) -POST /pendingretries/retry => ServiceControl.MessageFailures.Api.PendingRetryMessagesController:RetryBy(String[] ids) +GET /notifications/email => ServiceControl.Notifications.Api.NotificationsController:GetEmailNotificationsSettings(CancellationToken cancellationToken) +POST /notifications/email => ServiceControl.Notifications.Api.NotificationsController:UpdateSettings(UpdateEmailNotificationsSettingsRequest request, CancellationToken cancellationToken) +POST /notifications/email/test => ServiceControl.Notifications.Api.NotificationsController:SendTestEmail(CancellationToken cancellationToken) +POST /notifications/email/toggle => ServiceControl.Notifications.Api.NotificationsController:ToggleEmailNotifications(ToggleEmailNotifications request, CancellationToken cancellationToken) +PATCH /pendingretries/queues/resolve => ServiceControl.MessageFailures.Api.ResolveMessagesController:ResolveBy(QueueModel queueModel, CancellationToken cancellationToken) +POST /pendingretries/queues/retry => ServiceControl.MessageFailures.Api.PendingRetryMessagesController:RetryBy(PendingRetryRequest request, CancellationToken cancellationToken) +PATCH /pendingretries/resolve => ServiceControl.MessageFailures.Api.ResolveMessagesController:ResolveBy(UniqueMessageIdsModel request, CancellationToken cancellationToken) +POST /pendingretries/retry => ServiceControl.MessageFailures.Api.PendingRetryMessagesController:RetryBy(String[] ids, CancellationToken cancellationToken) GET /recoverability/classifiers => ServiceControl.Recoverability.API.FailureGroupsController:GetSupportedClassifiers() -GET /recoverability/groups/{classifier?} => ServiceControl.Recoverability.API.FailureGroupsController:GetAllGroups(String classifier, String classifierFilter) -DELETE /recoverability/groups/{groupId:required:minlength(1)}/comment => ServiceControl.Recoverability.API.FailureGroupsController:DeleteComment(String groupId) -POST /recoverability/groups/{groupId:required:minlength(1)}/comment => ServiceControl.Recoverability.API.FailureGroupsController:EditComment(String groupId, String comment) -GET /recoverability/groups/{groupId:required:minlength(1)}/errors => ServiceControl.Recoverability.API.FailureGroupsController:GetGroupErrors(String groupId, SortInfo sortInfo, PagingInfo pagingInfo, String status, String modified) -HEAD /recoverability/groups/{groupId:required:minlength(1)}/errors => ServiceControl.Recoverability.API.FailureGroupsController:GetGroupErrorsCount(String groupId, String status, String modified) -POST /recoverability/groups/{groupId:required:minlength(1)}/errors/archive => ServiceControl.Recoverability.API.FailureGroupsArchiveController:ArchiveGroupErrors(String groupId) -POST /recoverability/groups/{groupId:required:minlength(1)}/errors/retry => ServiceControl.Recoverability.API.FailureGroupsRetryController:ArchiveGroupErrors(String groupId) -POST /recoverability/groups/{groupId:required:minlength(1)}/errors/unarchive => ServiceControl.Recoverability.API.FailureGroupsUnarchiveController:UnarchiveGroupErrors(String groupId) -GET /recoverability/groups/id/{groupId:required:minlength(1)} => ServiceControl.Recoverability.API.FailureGroupsController:GetGroup(String groupId, String status, String modified) -GET /recoverability/history => ServiceControl.Recoverability.API.FailureGroupsController:GetRetryHistory() -DELETE /recoverability/unacknowledgedgroups/{groupId:required:minlength(1)} => ServiceControl.Recoverability.API.UnacknowledgedGroupsController:AcknowledgeOperation(String groupId) -HEAD /redirect => ServiceControl.MessageRedirects.Api.MessageRedirectsController:CountRedirects() -GET /redirects => ServiceControl.MessageRedirects.Api.MessageRedirectsController:Redirects(String sort, String direction, PagingInfo pagingInfo) -POST /redirects => ServiceControl.MessageRedirects.Api.MessageRedirectsController:NewRedirects(MessageRedirectRequest request) -DELETE /redirects/{messageRedirectId:guid} => ServiceControl.MessageRedirects.Api.MessageRedirectsController:DeleteRedirect(Guid messageRedirectId) -PUT /redirects/{messageRedirectId:guid} => ServiceControl.MessageRedirects.Api.MessageRedirectsController:UpdateRedirect(Guid messageRedirectId, MessageRedirectRequest request) -GET /sagas/{id} => ServiceControl.SagaAudit.SagasController:Sagas(PagingInfo pagingInfo, Guid id) +GET /recoverability/groups/{classifier?} => ServiceControl.Recoverability.API.FailureGroupsController:GetAllGroups(String classifier, String classifierFilter, CancellationToken cancellationToken) +DELETE /recoverability/groups/{groupId:required:minlength(1)}/comment => ServiceControl.Recoverability.API.FailureGroupsController:DeleteComment(String groupId, CancellationToken cancellationToken) +POST /recoverability/groups/{groupId:required:minlength(1)}/comment => ServiceControl.Recoverability.API.FailureGroupsController:EditComment(String groupId, String comment, CancellationToken cancellationToken) +GET /recoverability/groups/{groupId:required:minlength(1)}/errors => ServiceControl.Recoverability.API.FailureGroupsController:GetGroupErrors(String groupId, SortInfo sortInfo, PagingInfo pagingInfo, String status, String modified, CancellationToken cancellationToken) +HEAD /recoverability/groups/{groupId:required:minlength(1)}/errors => ServiceControl.Recoverability.API.FailureGroupsController:GetGroupErrorsCount(String groupId, String status, String modified, CancellationToken cancellationToken) +POST /recoverability/groups/{groupId:required:minlength(1)}/errors/archive => ServiceControl.Recoverability.API.FailureGroupsArchiveController:ArchiveGroupErrors(String groupId, CancellationToken cancellationToken) +POST /recoverability/groups/{groupId:required:minlength(1)}/errors/retry => ServiceControl.Recoverability.API.FailureGroupsRetryController:ArchiveGroupErrors(String groupId, CancellationToken cancellationToken) +POST /recoverability/groups/{groupId:required:minlength(1)}/errors/unarchive => ServiceControl.Recoverability.API.FailureGroupsUnarchiveController:UnarchiveGroupErrors(String groupId, CancellationToken cancellationToken) +GET /recoverability/groups/id/{groupId:required:minlength(1)} => ServiceControl.Recoverability.API.FailureGroupsController:GetGroup(String groupId, String status, String modified, CancellationToken cancellationToken) +GET /recoverability/history => ServiceControl.Recoverability.API.FailureGroupsController:GetRetryHistory(CancellationToken cancellationToken) +DELETE /recoverability/unacknowledgedgroups/{groupId:required:minlength(1)} => ServiceControl.Recoverability.API.UnacknowledgedGroupsController:AcknowledgeOperation(String groupId, CancellationToken cancellationToken) +HEAD /redirect => ServiceControl.MessageRedirects.Api.MessageRedirectsController:CountRedirects(CancellationToken cancellationToken) +GET /redirects => ServiceControl.MessageRedirects.Api.MessageRedirectsController:Redirects(String sort, String direction, PagingInfo pagingInfo, CancellationToken cancellationToken) +POST /redirects => ServiceControl.MessageRedirects.Api.MessageRedirectsController:NewRedirects(MessageRedirectRequest request, CancellationToken cancellationToken) +DELETE /redirects/{messageRedirectId:guid} => ServiceControl.MessageRedirects.Api.MessageRedirectsController:DeleteRedirect(Guid messageRedirectId, CancellationToken cancellationToken) +PUT /redirects/{messageRedirectId:guid} => ServiceControl.MessageRedirects.Api.MessageRedirectsController:UpdateRedirect(Guid messageRedirectId, MessageRedirectRequest request, CancellationToken cancellationToken) +GET /sagas/{id} => ServiceControl.SagaAudit.SagasController:Sagas(PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 238021bf98..4317f3ad17 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -206,19 +206,19 @@ public async Task class MockEndpointInstanceMonitoring(EndpointsView[] endpointsViews) : IEndpointInstanceMonitoring { - public Task CheckEndpoints(DateTime threshold) => throw new NotImplementedException(); + public Task CheckEndpoints(DateTime threshold, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt) => + public Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, bool monitored) => throw new NotImplementedException(); - public Task DisableMonitoring(Guid id) => throw new NotImplementedException(); + public Task DisableMonitoring(Guid id, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task EnableMonitoring(Guid id) => throw new NotImplementedException(); + public Task EnableMonitoring(Guid id, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task EndpointDetected(EndpointDetails newEndpointDetails) => throw new NotImplementedException(); + public Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public EndpointsView[] GetEndpoints() => endpointsViews; diff --git a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs index 4c327da5c8..7f0c779e38 100644 --- a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs +++ b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using CompositeViews.Messages; using Microsoft.AspNetCore.Http; @@ -74,7 +75,7 @@ public TestApi(object dataStore, Settings settings, IHttpClientFactory httpClien { } - protected override Task>> LocalQuery(ScatterGatherApiMessageViewContext input) => throw new NotImplementedException(); + protected override Task>> LocalQuery(ScatterGatherApiMessageViewContext input, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/src/ServiceControl/.editorconfig b/src/ServiceControl/.editorconfig index 935fb04254..e2f03528c2 100644 --- a/src/ServiceControl/.editorconfig +++ b/src/ServiceControl/.editorconfig @@ -3,14 +3,23 @@ # 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 +# Blocked on the transport abstraction. TransportCustomization.CreateTransportInfrastructure takes +# Func for onCriticalError, so the critical-error path here cannot carry a +# token until that signature gains one in the transports phase. +[{Operations/ErrorIngestion.cs,Operations/ErrorIngestionFaultPolicy.cs,Operations/ImportFailureCircuitBreaker.cs}] +dotnet_diagnostic.PS0013.severity = none +dotnet_diagnostic.PS0018.severity = none + +# Blocked on IExternalIntegrationRequestsDataStore.Subscribe, which takes Func. +# The dispatch callback gains a token when that interface does, in the persistence phase. +[ExternalIntegrations/EventDispatcherHostedService.cs] +dotnet_diagnostic.PS0018.severity = none + +# Blocked on IRetryBatchStore. BulkRetryRequest.Invoke forwards a Func straight +# into ForEachUnresolvedMessage and friends, so the callback gains a token when that interface does. +# PS0004 is a separate matter: AssignMessagesToBatch is private and would take a required token, but its +# trailing optional parameters make that illegal, so the default stays until the parameters are reordered. +[Recoverability/Retrying/RetriesGateway.cs] dotnet_diagnostic.PS0004.severity = none -dotnet_diagnostic.PS0006.severity = none dotnet_diagnostic.PS0013.severity = none -dotnet_diagnostic.PS0017.severity = none dotnet_diagnostic.PS0018.severity = none -dotnet_diagnostic.PS0019.severity = none -dotnet_diagnostic.PS0020.severity = none diff --git a/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs b/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs index 73899eff73..dbf265b0b6 100644 --- a/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/AuditCounts/GetAuditCountsForEndpointApi.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Api.Contracts; using Messages; @@ -28,7 +29,7 @@ public class GetAuditCountsForEndpointApi( { static readonly IList Empty = new List(0).AsReadOnly(); - protected override Task>> LocalQuery(AuditCountsForEndpointContext input) => + protected override Task>> LocalQuery(AuditCountsForEndpointContext input, CancellationToken cancellationToken = default) => // Will never be implemented on the primary instance Task.FromResult(new QueryResult>(Empty, QueryStatsInfo.Zero)); diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs index 726d4f7365..51a9c16b78 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs @@ -2,6 +2,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -16,7 +17,7 @@ public GetAllMessagesApi(IMessagesViewDataStore dataStore, Settings settings, IH { } - protected override Task>> LocalQuery(ScatterGatherApiMessageViewWithSystemMessagesContext input) + protected override Task>> LocalQuery(ScatterGatherApiMessageViewWithSystemMessagesContext input, CancellationToken cancellationToken = default) { return DataStore.GetAllMessages(input.PagingInfo, input.SortInfo, input.IncludeSystemMessages, input.TimeSentRange); } diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs index 05e43e0e93..c7d9cfcc29 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs @@ -2,6 +2,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -24,6 +25,6 @@ public GetAllMessagesForEndpointApi(IMessagesViewDataStore dataStore, Settings s { } - protected override Task>> LocalQuery(AllMessagesForEndpointContext input) => DataStore.GetAllMessagesForEndpoint(input.EndpointName, input.PagingInfo, input.SortInfo, input.IncludeSystemMessages, input.TimeSentRange); + protected override Task>> LocalQuery(AllMessagesForEndpointContext input, CancellationToken cancellationToken = default) => DataStore.GetAllMessagesForEndpoint(input.EndpointName, input.PagingInfo, input.SortInfo, input.IncludeSystemMessages, input.TimeSentRange); } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs index eb72656fb1..df6dec1155 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs @@ -1,6 +1,7 @@ namespace ServiceControl.CompositeViews.Messages; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -27,7 +28,8 @@ public async Task> Messages( [FromQuery(Name = "endpoint_name")] string endpointName, [FromQuery(Name = "from")] string from, [FromQuery(Name = "to")] string to, - string q) + string q, + CancellationToken cancellationToken = default) { QueryResult> result; var pagingInfo = new PagingInfo(pageSize: pageSize); @@ -38,13 +40,13 @@ public async Task> Messages( result = await allMessagesApi.Execute( new ScatterGatherApiMessageViewWithSystemMessagesContext(pagingInfo, sortInfo, false, new DateTimeRange(from, to)), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); } else { result = await searchApi.Execute( new SearchApiContext(pagingInfo, sortInfo, q, new DateTimeRange(from, to)), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); } } else @@ -54,12 +56,12 @@ public async Task> Messages( result = await allMessagesEndpointApi.Execute( new AllMessagesForEndpointContext(pagingInfo, sortInfo, false, endpointName, new DateTimeRange(from, to)), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); } else { result = await searchEndpointApi.Execute(new SearchEndpointContext(pagingInfo, sortInfo, q, endpointName, new DateTimeRange(from, to)), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); } } diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs index ab18a6da9e..5b21ae3246 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -20,11 +21,11 @@ public class GetMessagesByConversationController(MessagesByConversationApi byCon public async Task> Messages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] - bool includeSystemMessages, string conversationId) + bool includeSystemMessages, string conversationId, CancellationToken cancellationToken = default) { QueryResult> result = await byConversationApi.Execute( new MessagesByConversationContext(pagingInfo, sortInfo, includeSystemMessages, conversationId), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs index 5ee7542b6d..bef095821d 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs @@ -3,6 +3,7 @@ namespace ServiceControl.CompositeViews.Messages using System.Collections.Generic; using System.Linq; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Api.Contracts; using Infrastructure.Auth; @@ -41,11 +42,11 @@ public class GetMessagesController( public async Task> Messages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] - bool includeSystemMessages) + bool includeSystemMessages, CancellationToken cancellationToken = default) { QueryResult> result = await allMessagesApi.Execute( new ScatterGatherApiMessageViewWithSystemMessagesContext(pagingInfo, sortInfo, includeSystemMessages), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -57,11 +58,11 @@ public async Task> Messages([FromQuery] PagingInfo pagingInf public async Task> MessagesForEndpoint([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] - bool includeSystemMessages, string endpoint) + bool includeSystemMessages, string endpoint, CancellationToken cancellationToken = default) { QueryResult> result = await allMessagesForEndpointApi.Execute( new AllMessagesForEndpointContext(pagingInfo, sortInfo, includeSystemMessages, endpoint), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -71,10 +72,10 @@ public async Task> MessagesForEndpoint([FromQuery] PagingInf [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpoint}/audit-count")] [HttpGet] - public async Task> GetEndpointAuditCounts([FromQuery] PagingInfo pagingInfo, string endpoint) + public async Task> GetEndpointAuditCounts([FromQuery] PagingInfo pagingInfo, string endpoint, CancellationToken cancellationToken = default) { QueryResult> result = await auditCountsForEndpointApi.Execute( - new AuditCountsForEndpointContext(pagingInfo, endpoint), Request.GetEncodedPathAndQuery()); + new AuditCountsForEndpointContext(pagingInfo, endpoint), Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -83,7 +84,7 @@ public async Task> GetEndpointAuditCounts([FromQuery] PagingIn [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages/{id}/body")] [HttpGet] - public async Task Get(string id, [FromQuery(Name = "instance_id")] string instanceId) + public async Task Get(string id, [FromQuery(Name = "instance_id")] string instanceId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.InstanceId) { @@ -114,7 +115,7 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") if (forwarderError != ForwarderError.None && HttpContext.GetForwarderErrorFeature()?.Exception is { } exception) { logger.LogWarning(exception, "Failed to forward the request to remote instance at {RemoteInstanceUrl}", - remote.BaseAddress + HttpContext.Request.GetEncodedPathAndQuery()); + remote.BaseAddress + HttpContext.Request.GetEncodedPathAndQuery(), cancellationToken); } return Empty; @@ -124,10 +125,10 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") [Route("messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, - string q) + string q, CancellationToken cancellationToken = default) { QueryResult> result = await api.Execute(new SearchApiContext(pagingInfo, sortInfo, q), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -137,11 +138,11 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [Route("messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, - [FromQuery] SortInfo sortInfo, string keyword) + [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken = default) { QueryResult> result = await api.Execute( new SearchApiContext(pagingInfo, sortInfo, keyword?.Replace("/", @"\")), - Request.GetEncodedPathAndQuery()); + Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -151,10 +152,10 @@ public async Task> SearchByKeyWord([FromQuery] PagingInfo pa [Route("endpoints/{endpoint}/messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, - string endpoint, string q) + string endpoint, string q, CancellationToken cancellationToken = default) { QueryResult> result = await endpointApi.Execute( - new SearchEndpointContext(pagingInfo, sortInfo, endpoint, q), Request.GetEncodedPathAndQuery()); + new SearchEndpointContext(pagingInfo, sortInfo, endpoint, q), Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; @@ -164,10 +165,10 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [Route("endpoints/{endpoint}/messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, - [FromQuery] SortInfo sortInfo, string endpoint, string keyword) + [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken = default) { QueryResult> result = await endpointApi.Execute( - new SearchEndpointContext(pagingInfo, sortInfo, endpoint, keyword), Request.GetEncodedPathAndQuery()); + new SearchEndpointContext(pagingInfo, sortInfo, endpoint, keyword), Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; diff --git a/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs b/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs index 95ce512413..22c6323adb 100644 --- a/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/MessagesByConversationApi.cs @@ -2,6 +2,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -23,7 +24,7 @@ public MessagesByConversationApi(IMessagesViewDataStore dataStore, Settings sett { } - protected override Task>> LocalQuery(MessagesByConversationContext input) => + protected override Task>> LocalQuery(MessagesByConversationContext input, CancellationToken cancellationToken = default) => DataStore.GetAllMessagesByConversation(input.ConversationId, input.PagingInfo, input.SortInfo, input.IncludeSystemMessages); } diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs index 1b4f167667..d0444a2068 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs @@ -6,6 +6,7 @@ namespace ServiceControl.CompositeViews.Messages using System.Net; using System.Net.Http; using System.Net.Http.Headers; + using System.Threading; using System.Threading.Tasks; using Infrastructure.WebApi; using Microsoft.AspNetCore.Http; @@ -54,7 +55,7 @@ protected ScatterGatherApi(TDataStore store, Settings settings, IHttpClientFacto IHttpClientFactory HttpClientFactory { get; } IHttpContextAccessor HttpContextAccessor { get; } - public async Task> Execute(TIn input, string pathAndQuery) + public async Task> Execute(TIn input, string pathAndQuery, CancellationToken cancellationToken = default) { var remotes = Settings.RemoteInstances; var instanceId = Settings.InstanceId; @@ -62,7 +63,7 @@ public async Task> Execute(TIn input, string pathAndQuery) var tasks = new List>>(remotes.Length + 1) { - LocalCall(input, instanceId) + LocalCall(input, instanceId, cancellationToken) }; foreach (var remote in remotes) @@ -72,7 +73,7 @@ public async Task> Execute(TIn input, string pathAndQuery) continue; } - tasks.Add(RemoteCall(HttpClientFactory.CreateClient(remote.InstanceId), pathAndQuery, remote, authorizationHeader)); + tasks.Add(RemoteCall(HttpClientFactory.CreateClient(remote.InstanceId), pathAndQuery, remote, authorizationHeader, cancellationToken)); } var results = await Task.WhenAll(tasks); @@ -81,14 +82,14 @@ public async Task> Execute(TIn input, string pathAndQuery) return response; } - async Task> LocalCall(TIn input, string instanceId) + async Task> LocalCall(TIn input, string instanceId, CancellationToken cancellationToken) { - var result = await LocalQuery(input); + var result = await LocalQuery(input, cancellationToken); result.InstanceId = instanceId; return result; } - protected abstract Task> LocalQuery(TIn input); + protected abstract Task> LocalQuery(TIn input, CancellationToken cancellationToken = default); internal QueryResult AggregateResults(TIn input, QueryResult[] results) { @@ -114,14 +115,14 @@ protected virtual QueryStatsInfo AggregateStats(TIn input, IEnumerable> RemoteCall(HttpClient client, string pathAndQuery, RemoteInstanceSetting remoteInstanceSetting, string authorizationHeader) + async Task> RemoteCall(HttpClient client, string pathAndQuery, RemoteInstanceSetting remoteInstanceSetting, string authorizationHeader, CancellationToken cancellationToken) { - var fetched = await FetchAndParse(client, pathAndQuery, remoteInstanceSetting, authorizationHeader); + var fetched = await FetchAndParse(client, pathAndQuery, remoteInstanceSetting, authorizationHeader, cancellationToken); fetched.InstanceId = remoteInstanceSetting.InstanceId; return fetched; } - async Task> FetchAndParse(HttpClient httpClient, string pathAndQuery, RemoteInstanceSetting remoteInstanceSetting, string authorizationHeader) + async Task> FetchAndParse(HttpClient httpClient, string pathAndQuery, RemoteInstanceSetting remoteInstanceSetting, string authorizationHeader, CancellationToken cancellationToken) { try { @@ -135,7 +136,7 @@ async Task> FetchAndParse(HttpClient httpClient, string pathAn } // Assuming SendAsync returns uncompressed response and the AutomaticDecompression is enabled on the http client. - var rawResponse = await httpClient.SendAsync(request); + var rawResponse = await httpClient.SendAsync(request, cancellationToken); // special case - queried by conversation ID and nothing was found if (rawResponse.StatusCode == HttpStatusCode.NotFound) @@ -150,7 +151,7 @@ async Task> FetchAndParse(HttpClient httpClient, string pathAn return QueryResult.Empty(); } - return await ParseResult(rawResponse); + return await ParseResult(rawResponse, cancellationToken); } catch (HttpRequestException httpRequestException) { @@ -161,6 +162,12 @@ async Task> FetchAndParse(HttpClient httpClient, string pathAn remoteInstanceSetting.BaseAddress); return QueryResult.Empty(); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The caller gave up on the whole scatter-gather, so this is not a per-remote timeout + // to be absorbed into an empty result. + throw; + } catch (OperationCanceledException) // Intentional, used to gracefully handle timeout { logger.LogWarning("Failed to query remote instance at {RemoteInstanceBaseAddress} due to a timeout", remoteInstanceSetting.BaseAddress); @@ -173,10 +180,10 @@ async Task> FetchAndParse(HttpClient httpClient, string pathAn } } - static async Task> ParseResult(HttpResponseMessage responseMessage) + static async Task> ParseResult(HttpResponseMessage responseMessage, CancellationToken cancellationToken) { - await using var responseStream = await responseMessage.Content.ReadAsStreamAsync(); - var remoteResults = await JsonSerializer.DeserializeAsync(responseStream, SerializerOptions.Default); + await using var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken); + var remoteResults = await JsonSerializer.DeserializeAsync(responseStream, SerializerOptions.Default, cancellationToken); var totalCount = 0; if (responseMessage.Headers.TryGetValues("Total-Count", out var totalCounts)) diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherRemoteOnly.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherRemoteOnly.cs index 26f2af4914..379070101b 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherRemoteOnly.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherRemoteOnly.cs @@ -1,6 +1,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,7 +13,7 @@ public abstract class ScatterGatherRemoteOnly(Settings settings, IHtt where TIn : ScatterGatherContext where TOut : class { - protected sealed override Task> LocalQuery(TIn input) => QueryResult.Empty(); + protected sealed override Task> LocalQuery(TIn input, CancellationToken cancellationToken = default) => QueryResult.Empty(); } public sealed class NoOpStore diff --git a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs index 8f9f1e8dd5..e9fedc48a3 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs @@ -2,6 +2,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -23,7 +24,7 @@ public SearchApi(IMessagesViewDataStore dataStore, Settings settings, IHttpClien { } - protected override Task>> LocalQuery(SearchApiContext input) => + protected override Task>> LocalQuery(SearchApiContext input, CancellationToken cancellationToken = default) => DataStore.GetAllMessagesForSearch(input.SearchQuery, input.PagingInfo, input.SortInfo, input.TimeSentRange); } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs index 6acd23b653..499879c2b2 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs @@ -2,6 +2,7 @@ namespace ServiceControl.CompositeViews.Messages { using System.Collections.Generic; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -24,7 +25,7 @@ public SearchEndpointApi(IMessagesViewDataStore dataStore, Settings settings, IH { } - protected override Task>> LocalQuery(SearchEndpointContext input) => + protected override Task>> LocalQuery(SearchEndpointContext input, CancellationToken cancellationToken = default) => DataStore.SearchEndpointMessages(input.Endpoint, input.Keyword, input.PagingInfo, input.SortInfo, input.TimeSentRange); } } \ No newline at end of file diff --git a/src/ServiceControl/Connection/ConnectionController.cs b/src/ServiceControl/Connection/ConnectionController.cs index b87d4d8dc7..4183c2679f 100644 --- a/src/ServiceControl/Connection/ConnectionController.cs +++ b/src/ServiceControl/Connection/ConnectionController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Microsoft.AspNetCore.Authorization; @@ -18,9 +19,9 @@ public class ConnectionController(IPlatformConnectionBuilder builder) : Controll [Authorize(Policy = Permissions.ErrorConnectionsView)] [Route("connection")] [HttpGet] - public async Task GetConnectionDetails() + public async Task GetConnectionDetails(CancellationToken cancellationToken = default) { - var platformConnectionDetails = await builder.BuildPlatformConnection(); + var platformConnectionDetails = await builder.BuildPlatformConnection(cancellationToken); return new JsonResult(new ConnectionDetails(new Dictionary(platformConnectionDetails.ToDictionary()), [.. platformConnectionDetails.Errors]), JsonSerializerOptions.Default); } diff --git a/src/ServiceControl/Connection/IPlatformConnectionBuilder.cs b/src/ServiceControl/Connection/IPlatformConnectionBuilder.cs index 62149747aa..28202a0e10 100644 --- a/src/ServiceControl/Connection/IPlatformConnectionBuilder.cs +++ b/src/ServiceControl/Connection/IPlatformConnectionBuilder.cs @@ -1,9 +1,10 @@ namespace ServiceControl.Connection { + using System.Threading; using System.Threading.Tasks; public interface IPlatformConnectionBuilder { - Task BuildPlatformConnection(); + Task BuildPlatformConnection(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ServiceControl/Connection/IProvidePlatformConnectionDetails.cs b/src/ServiceControl/Connection/IProvidePlatformConnectionDetails.cs index 65fbf8fc94..2a260a0cb5 100644 --- a/src/ServiceControl/Connection/IProvidePlatformConnectionDetails.cs +++ b/src/ServiceControl/Connection/IProvidePlatformConnectionDetails.cs @@ -1,9 +1,10 @@ namespace ServiceControl.Connection { + using System.Threading; using System.Threading.Tasks; interface IProvidePlatformConnectionDetails { - Task ProvideConnectionDetails(PlatformConnectionDetails connection); + Task ProvideConnectionDetails(PlatformConnectionDetails connection, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ServiceControl/Connection/PlatformConnectionBuilder.cs b/src/ServiceControl/Connection/PlatformConnectionBuilder.cs index 0cabfb2abe..3e2b38b3d8 100644 --- a/src/ServiceControl/Connection/PlatformConnectionBuilder.cs +++ b/src/ServiceControl/Connection/PlatformConnectionBuilder.cs @@ -2,18 +2,19 @@ { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; class PlatformConnectionBuilder(IEnumerable platformConnectionProviders) : IPlatformConnectionBuilder { - public async Task BuildPlatformConnection() + public async Task BuildPlatformConnection(CancellationToken cancellationToken = default) { var connectionDetails = new PlatformConnectionDetails(); await Task.WhenAll( from provider in platformConnectionProviders - select provider.ProvideConnectionDetails(connectionDetails) + select provider.ProvideConnectionDetails(connectionDetails, cancellationToken) ); return connectionDetails; diff --git a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs index fe0ee35258..0a1c349059 100644 --- a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,17 +13,17 @@ class RemotePlatformConnectionDetailsProvider(Settings settings, IHttpClientFactory clientFactory, IHttpContextAccessor httpContextAccessor, ILogger logger) : IProvidePlatformConnectionDetails { - public Task ProvideConnectionDetails(PlatformConnectionDetails connection) + public Task ProvideConnectionDetails(PlatformConnectionDetails connection, CancellationToken cancellationToken = default) { var authorizationHeader = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); return Task.WhenAll( settings.RemoteInstances - .Select(remote => UpdateFromRemote(remote, connection, authorizationHeader)) + .Select(remote => UpdateFromRemote(remote, connection, authorizationHeader, cancellationToken)) ); } - async Task UpdateFromRemote(RemoteInstanceSetting remote, PlatformConnectionDetails connection, string authorizationHeader) + async Task UpdateFromRemote(RemoteInstanceSetting remote, PlatformConnectionDetails connection, string authorizationHeader, CancellationToken cancellationToken) { var client = clientFactory.CreateClient(remote.InstanceId); try @@ -35,16 +36,20 @@ async Task UpdateFromRemote(RemoteInstanceSetting remote, PlatformConnectionDeta request.Headers.TryAddWithoutValidation("Authorization", authorizationHeader); } - var response = await client.SendAsync(request); + var response = await client.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(); - var document = await JsonDocument.ParseAsync(stream); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); foreach (var property in document.RootElement.EnumerateObject()) { connection.Add(property.Name, property.Value); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { var remoteConnectionUri = $"{remote.BaseAddress.TrimEnd('/')}/connection"; diff --git a/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs b/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs index ed104b7760..9153ca42f2 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckFailedPublisher.cs @@ -3,6 +3,7 @@ namespace ServiceControl.CustomChecks using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.CustomChecks; using ExternalIntegrations; @@ -23,7 +24,7 @@ protected override DispatchContext CreateDispatchRequest(CustomCheckFailed @even }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.CustomCheckFailed { diff --git a/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs b/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs index 1e7a3d6727..92679885cf 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckResultProcessor.cs @@ -1,6 +1,7 @@ namespace ServiceControl.CustomChecks { using System; + using System.Threading; using System.Threading.Tasks; using Contracts.CustomChecks; using Infrastructure.DomainEvents; @@ -16,12 +17,12 @@ public CustomCheckResultProcessor(IDomainEvents domainEvents, ICustomChecksDataS this.logger = logger; } - public async Task ProcessResult(CustomCheckDetail checkDetail) + public async Task ProcessResult(CustomCheckDetail checkDetail, CancellationToken cancellationToken = default) { try { var statusChange = await store.UpdateCustomCheckStatus(checkDetail); - await RaiseEvents(statusChange, checkDetail); + await RaiseEvents(statusChange, checkDetail, cancellationToken); var numberOfFailedChecks = await store.GetNumberOfFailedChecks(); @@ -34,7 +35,11 @@ public async Task ProcessResult(CustomCheckDetail checkDetail) await domainEvents.Raise(new CustomChecksUpdated { Failed = numberOfFailedChecks - }); + }, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { @@ -42,7 +47,7 @@ await domainEvents.Raise(new CustomChecksUpdated } } - async Task RaiseEvents(CheckStateChange state, CustomCheckDetail detail) + async Task RaiseEvents(CheckStateChange state, CustomCheckDetail detail, CancellationToken cancellationToken) { var id = detail.GetDeterministicId(); @@ -58,7 +63,7 @@ await domainEvents.Raise(new CustomCheckFailed FailedAt = detail.ReportedAt, FailureReason = detail.FailureReason, OriginatingEndpoint = detail.OriginatingEndpoint - }); + }, cancellationToken); } else { @@ -69,7 +74,7 @@ await domainEvents.Raise(new CustomCheckSucceeded Category = detail.Category, SucceededAt = detail.ReportedAt, OriginatingEndpoint = detail.OriginatingEndpoint - }); + }, cancellationToken); } } } diff --git a/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs b/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs index 6ea417b2ef..caa3bea162 100644 --- a/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs +++ b/src/ServiceControl/CustomChecks/CustomCheckSucceededPublisher.cs @@ -3,6 +3,7 @@ namespace ServiceControl.CustomChecks using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.CustomChecks; using ExternalIntegrations; @@ -22,7 +23,7 @@ protected override DispatchContext CreateDispatchRequest(CustomCheckSucceeded @e }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.CustomCheckSucceeded { diff --git a/src/ServiceControl/CustomChecks/CustomChecksConnectionDetailsProvider.cs b/src/ServiceControl/CustomChecks/CustomChecksConnectionDetailsProvider.cs index e38e837804..0ce902791d 100644 --- a/src/ServiceControl/CustomChecks/CustomChecksConnectionDetailsProvider.cs +++ b/src/ServiceControl/CustomChecks/CustomChecksConnectionDetailsProvider.cs @@ -1,5 +1,6 @@ namespace ServiceControl.CustomChecks { + using System.Threading; using System.Threading.Tasks; using Connection; using NServiceBus; @@ -11,7 +12,7 @@ class CustomChecksPlatformConnectionDetailsProvider : IProvidePlatformConnection public CustomChecksPlatformConnectionDetailsProvider(ReceiveAddresses receiveAddresses) => instanceMainQueue = receiveAddresses.MainReceiveAddress; - public Task ProvideConnectionDetails(PlatformConnectionDetails connection) + public Task ProvideConnectionDetails(PlatformConnectionDetails connection, CancellationToken cancellationToken = default) { connection.Add( "CustomChecks", diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs index a2442771f6..803712c068 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomCheckManager.cs @@ -63,14 +63,14 @@ async Task Run(CancellationToken cancellationToken) FailureReason = result.FailureReason }; - await checkResultProcessor.ProcessResult(detail); + await checkResultProcessor.ProcessResult(detail, cancellationToken); return check.Interval.HasValue ? TimerJobExecutionResult.ScheduleNextExecution : TimerJobExecutionResult.DoNotContinueExecuting; } - public Task Stop() => timer?.Stop(CancellationToken.None) ?? Task.CompletedTask; + public Task Stop(CancellationToken cancellationToken = default) => timer?.Stop(cancellationToken) ?? Task.CompletedTask; TimerJob timer; readonly ICustomCheck check; diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs index 8e4570999c..ca1b729ad8 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs @@ -20,7 +20,7 @@ class InternalCustomChecksHostedService( ILogger logger) : IHostedService { - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { foreach (var check in customChecks) { @@ -33,11 +33,11 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { if (managers.Any()) { - await Task.WhenAll([.. managers.Select(m => m.Stop())]); + await Task.WhenAll([.. managers.Select(m => m.Stop(cancellationToken))]); } } diff --git a/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs b/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs index f9cda8e083..e288ffa9f2 100644 --- a/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs +++ b/src/ServiceControl/CustomChecks/ReportCustomCheckResultHandler.cs @@ -42,7 +42,7 @@ public async Task Handle(ReportCustomCheckResult message, IMessageHandlerContext FailureReason = message.FailureReason }; - await checkResultProcessor.ProcessResult(checkDetails); + await checkResultProcessor.ProcessResult(checkDetails, context.CancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs index f9fb690757..ecdaca52d5 100644 --- a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs +++ b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Contracts.CustomChecks; using Infrastructure.Auth; @@ -20,7 +21,7 @@ public class CustomCheckController(ICustomChecksDataStore checksDataStore, IMess [Authorize(Policy = Permissions.ErrorCustomChecksView)] [Route("customchecks")] [HttpGet] - public async Task> CustomChecks([FromQuery] PagingInfo pagingInfo, string status = null) + public async Task> CustomChecks([FromQuery] PagingInfo pagingInfo, string status = null, CancellationToken cancellationToken = default) { var stats = await checksDataStore.GetStats(pagingInfo, status); @@ -33,9 +34,9 @@ public async Task> CustomChecks([FromQuery] PagingInfo paging [Authorize(Policy = Permissions.ErrorCustomChecksDelete)] [Route("customchecks/{id}")] [HttpDelete] - public async Task Delete(Guid id) + public async Task Delete(Guid id, CancellationToken cancellationToken = default) { - await session.SendLocal(new DeleteCustomCheck { Id = id }); + await session.SendLocal(new DeleteCustomCheck { Id = id }, cancellationToken); return Accepted(); } diff --git a/src/ServiceControl/EventLog/AuditEventLogWriter.cs b/src/ServiceControl/EventLog/AuditEventLogWriter.cs index aeb14eafc9..3134d9dab5 100644 --- a/src/ServiceControl/EventLog/AuditEventLogWriter.cs +++ b/src/ServiceControl/EventLog/AuditEventLogWriter.cs @@ -17,7 +17,7 @@ public AuditEventLogWriter(IEventLogDataStore dataStore, EventLogMappings mappin this.mappings = mappings; } - public async Task Handle(IDomainEvent message, CancellationToken cancellationToken) + public async Task Handle(IDomainEvent message, CancellationToken cancellationToken = default) { if (!mappings.HasMapping(message)) { diff --git a/src/ServiceControl/EventLog/EventLogApiController.cs b/src/ServiceControl/EventLog/EventLogApiController.cs index 4fdb061aec..87fc1a31c0 100644 --- a/src/ServiceControl/EventLog/EventLogApiController.cs +++ b/src/ServiceControl/EventLog/EventLogApiController.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using System.Net; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -17,7 +18,7 @@ public class EventLogApiController(IEventLogDataStore logDataStore) : Controller [Authorize(Policy = Permissions.ErrorEventLogView)] [Route("eventlogitems")] [HttpGet] - public async Task>> Items([FromQuery] PagingInfo pagingInfo) + public async Task>> Items([FromQuery] PagingInfo pagingInfo, CancellationToken cancellationToken = default) { // Passing knownVersion lets the persister skip work it would otherwise waste var result = await logDataStore.GetEventLogItems(pagingInfo, Request.GetKnownVersion()); diff --git a/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs b/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs index 91c9eb56f5..26b9d9c62d 100644 --- a/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs +++ b/src/ServiceControl/ExternalIntegrations/EventDispatcherHostedService.cs @@ -26,7 +26,7 @@ public EventDispatcherHostedService( this.logger = logger; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { store.Subscribe(TryDispatchEventBatch); @@ -72,7 +72,7 @@ async Task TryDispatchEventBatch(object[] allContexts) } } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken = default) { return store.StopAsync(cancellationToken); } diff --git a/src/ServiceControl/ExternalIntegrations/EventPublisher.cs b/src/ServiceControl/ExternalIntegrations/EventPublisher.cs index 61198b7ee1..bd874324f1 100644 --- a/src/ServiceControl/ExternalIntegrations/EventPublisher.cs +++ b/src/ServiceControl/ExternalIntegrations/EventPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.ExternalIntegrations { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; @@ -18,13 +19,13 @@ public object CreateDispatchContext(IDomainEvent @event) return CreateDispatchRequest((TEvent)@event); } - public Task> PublishEventsForOwnContexts(IEnumerable allContexts) + public Task> PublishEventsForOwnContexts(IEnumerable allContexts, CancellationToken cancellationToken = default) { - return PublishEvents(allContexts.OfType()); + return PublishEvents(allContexts.OfType(), cancellationToken); } protected abstract TDispatchContext CreateDispatchRequest(TEvent @event); - protected abstract Task> PublishEvents(IEnumerable contexts); + protected abstract Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs b/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs index cd189af159..13ad7c7a72 100644 --- a/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs +++ b/src/ServiceControl/ExternalIntegrations/IEventPublisher.cs @@ -1,6 +1,7 @@ namespace ServiceControl.ExternalIntegrations { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; @@ -9,6 +10,6 @@ interface IEventPublisher bool Handles(IDomainEvent @event); object CreateDispatchContext(IDomainEvent @event); - Task> PublishEventsForOwnContexts(IEnumerable allContexts); + Task> PublishEventsForOwnContexts(IEnumerable allContexts, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs b/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs index 26881dd36f..44e95ac6ea 100644 --- a/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs +++ b/src/ServiceControl/ExternalIntegrations/IntegrationEventWriter.cs @@ -20,7 +20,7 @@ public IntegrationEventWriter( this.logger = logger; } - public async Task Handle(IDomainEvent message, CancellationToken cancellationToken) + public async Task Handle(IDomainEvent message, CancellationToken cancellationToken = default) { var dispatchContexts = eventPublishers .Where(p => p.Handles(message)) diff --git a/src/ServiceControl/Hosting/Commands/AbstractCommand.cs b/src/ServiceControl/Hosting/Commands/AbstractCommand.cs index 4178465847..602ba420ab 100644 --- a/src/ServiceControl/Hosting/Commands/AbstractCommand.cs +++ b/src/ServiceControl/Hosting/Commands/AbstractCommand.cs @@ -1,11 +1,12 @@ namespace ServiceControl.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; 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/Hosting/Commands/CommandRunner.cs b/src/ServiceControl/Hosting/Commands/CommandRunner.cs index f959364f85..155475e6ae 100644 --- a/src/ServiceControl/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl/Hosting/Commands/CommandRunner.cs @@ -1,16 +1,17 @@ namespace ServiceControl.Hosting.Commands { using System; + using System.Threading; using System.Threading.Tasks; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; 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/Hosting/Commands/ImportFailedErrorsCommand.cs b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs index 0c3fb6e3c9..af8a53840a 100644 --- a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs +++ b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs @@ -17,10 +17,10 @@ class ImportFailedErrorsCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { using var app = BuildHost(settings); - await app.StartAsync(); + await app.StartAsync(cancellationToken); var importFailedErrors = app.Services.GetRequiredService(); @@ -31,7 +31,7 @@ public override async Task Execute(HostArguments args, Settings settings) { await importFailedErrors.Run(tokenSource.Token); } - catch (OperationCanceledException e) when (tokenSource.IsCancellationRequested) + catch (OperationCanceledException e) when (tokenSource.Token.IsCancellationRequested) { LoggerUtil.CreateStaticLogger().LogInformation(e, "Cancelled"); } diff --git a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs index f3ba85e132..154543c055 100644 --- a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.WindowsServices; @@ -9,7 +10,7 @@ class MaintenanceModeCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { var hostBuilder = Host.CreateApplicationBuilder(); hostBuilder.Services.AddPersistence(settings, maintenanceMode: true); @@ -21,7 +22,7 @@ public override async Task Execute(HostArguments args, Settings settings) } var host = hostBuilder.Build(); - await host.RunAsync(); + await host.RunAsync(cancellationToken); } } } diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index ac47c46850..33a6361511 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Hosting.Commands { + using System.Threading; using System.Threading.Tasks; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; @@ -14,7 +15,7 @@ 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); var assemblyScanner = endpointConfiguration.AssemblyScanner(); diff --git a/src/ServiceControl/Hosting/Commands/SetupCommand.cs b/src/ServiceControl/Hosting/Commands/SetupCommand.cs index fd3d8ebbe2..4ded1e9648 100644 --- a/src/ServiceControl/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl/Hosting/Commands/SetupCommand.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Hosting.Commands { using System.Runtime.InteropServices; + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -15,7 +16,7 @@ class SetupCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default) { var hostBuilder = Host.CreateApplicationBuilder(); hostBuilder.AddServiceControlInstallers(settings); @@ -34,7 +35,7 @@ public override async Task Execute(HostArguments args, Settings settings) EventSourceCreator.Create(); } - await host.StartAsync(); + await host.StartAsync(cancellationToken); if (args.SkipQueueCreation) { @@ -53,16 +54,16 @@ public override async Task Execute(HostArguments args, Settings settings) { if (scope.ServiceProvider.GetService() is { } databaseMigrator) { - await databaseMigrator.ApplyMigrations(); + await databaseMigrator.ApplyMigrations(cancellationToken); } if (scope.ServiceProvider.GetService() is { } bodyStorageInstaller) { - await bodyStorageInstaller.Provision(); + await bodyStorageInstaller.Provision(cancellationToken); } } - await host.StopAsync(); + await host.StopAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Api/AuditCountApi.cs b/src/ServiceControl/Infrastructure/Api/AuditCountApi.cs index 81e8311b02..8258688842 100644 --- a/src/ServiceControl/Infrastructure/Api/AuditCountApi.cs +++ b/src/ServiceControl/Infrastructure/Api/AuditCountApi.cs @@ -10,7 +10,7 @@ class AuditCountApi(GetAuditCountsForEndpointApi auditCountsForEndpointApi) : IAuditCountApi { - public async Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken) => + public async Task> GetEndpointAuditCounts(string endpoint, CancellationToken cancellationToken = default) => (await auditCountsForEndpointApi.Execute(new AuditCountsForEndpointContext(new PagingInfo(), endpoint), - $"/api/endpoints/{endpoint}/audit-count")).Results; + $"/api/endpoints/{endpoint}/audit-count", cancellationToken)).Results; } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs index a15cca6853..4d61e23ca8 100644 --- a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs +++ b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs @@ -16,7 +16,7 @@ class ConfigurationApi(ActiveLicense license, Settings settings, IHttpClientFactory httpClientFactory, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi { - public Task GetUrls(string baseUrl, CancellationToken cancellationToken) + public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) { if (!baseUrl.EndsWith('/')) { @@ -50,7 +50,7 @@ public Task GetUrls(string baseUrl, CancellationToken cancellationToke } - public Task GetConfig(CancellationToken cancellationToken) + public Task GetConfig(CancellationToken cancellationToken = default) { object content = new { @@ -117,6 +117,10 @@ public async Task GetRemoteConfigs(CancellationToken canc { status = ex.StatusCode >= System.Net.HttpStatusCode.InternalServerError ? "error" : "unavailable"; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception) { status = "error"; diff --git a/src/ServiceControl/Infrastructure/Api/EndpointsApi.cs b/src/ServiceControl/Infrastructure/Api/EndpointsApi.cs index abb1d9f27f..d0f765ac20 100644 --- a/src/ServiceControl/Infrastructure/Api/EndpointsApi.cs +++ b/src/ServiceControl/Infrastructure/Api/EndpointsApi.cs @@ -9,7 +9,7 @@ class EndpointsApi(IEndpointInstanceMonitoring monitoring) : IEndpointsApi { - public Task> GetEndpoints(CancellationToken cancellationToken) + public Task> GetEndpoints(CancellationToken cancellationToken = default) { var endpoints = new List(); foreach (var endpointsView in monitoring.GetEndpoints()) diff --git a/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs b/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs index 0e38e84755..09a3c95356 100644 --- a/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs +++ b/src/ServiceControl/Infrastructure/CriticalErrorCustomCheck.cs @@ -32,10 +32,12 @@ public override Task PerformCheck(CancellationToken cancellationTok return Task.FromResult(CheckResult.Pass); } +#pragma warning disable PS0018 // Matches the NServiceBus critical-error delegate, which takes no CancellationToken public static Task OnCriticalError(ICriticalErrorContext criticalErrorContext) { recentFailure = criticalErrorContext.Error; return Task.CompletedTask; } +#pragma warning restore PS0018 } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs b/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs index 4fe465cde9..e2294ec764 100644 --- a/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs +++ b/src/ServiceControl/Infrastructure/Metrics/MetricsReporterHostedService.cs @@ -19,7 +19,7 @@ public MetricsReporterHostedService(Metrics metrics, ILogger logger) this.logger = logger; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { reporter = new MetricsReporter(metrics, x => logger.LogInformation(x), TimeSpan.FromSeconds(5)); @@ -28,7 +28,7 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { try { diff --git a/src/ServiceControl/Infrastructure/WebApi/BodyUrlRouteFix.cs b/src/ServiceControl/Infrastructure/WebApi/BodyUrlRouteFix.cs index 78a8dba4e6..bd33b203ce 100644 --- a/src/ServiceControl/Infrastructure/WebApi/BodyUrlRouteFix.cs +++ b/src/ServiceControl/Infrastructure/WebApi/BodyUrlRouteFix.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Infrastructure.WebApi { + using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -8,6 +9,7 @@ namespace ServiceControl.Infrastructure.WebApi // This owin module rewrites incoming urls starting with // to just one /. This avoids breaking existing clients when removing NancyFX. class BodyUrlRouteFix(RequestDelegate next) { +#pragma warning disable PS0018 // ASP.NET Core middleware convention takes only the HttpContext public Task InvokeAsync(HttpContext context) { if (context.Request.Path.HasValue) @@ -21,5 +23,6 @@ public Task InvokeAsync(HttpContext context) return next(context); } +#pragma warning restore PS0018 } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/WebApi/PagingInfoModelBinder.cs b/src/ServiceControl/Infrastructure/WebApi/PagingInfoModelBinder.cs index 905574d4c7..c74ff86ec2 100644 --- a/src/ServiceControl/Infrastructure/WebApi/PagingInfoModelBinder.cs +++ b/src/ServiceControl/Infrastructure/WebApi/PagingInfoModelBinder.cs @@ -7,6 +7,7 @@ namespace ServiceControl.Infrastructure.WebApi; public class PagingInfoModelBinder : IModelBinder { +#pragma warning disable PS0018 // IModelBinder declares this without a CancellationToken public Task BindModelAsync(ModelBindingContext bindingContext) { ArgumentNullException.ThrowIfNull(bindingContext); @@ -28,4 +29,5 @@ public Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.Result = ModelBindingResult.Success(new PagingInfo(page, maxResultsPerPage)); return Task.CompletedTask; } -} \ No newline at end of file +#pragma warning restore PS0018 +} diff --git a/src/ServiceControl/Infrastructure/WebApi/RootController.cs b/src/ServiceControl/Infrastructure/WebApi/RootController.cs index 16a11f5d99..56e9f68329 100644 --- a/src/ServiceControl/Infrastructure/WebApi/RootController.cs +++ b/src/ServiceControl/Infrastructure/WebApi/RootController.cs @@ -16,16 +16,16 @@ public class RootController(IConfigurationApi configurationApi) : ControllerBase { [Route("")] [HttpGet] - public Task Urls() => configurationApi.GetUrls(Request.GetDisplayUrl(), default); + public Task Urls(CancellationToken cancellationToken = default) => configurationApi.GetUrls(Request.GetDisplayUrl(), cancellationToken); [Route("instance-info")] [Route("configuration")] [HttpGet] - public Task Config() => configurationApi.GetConfig(default); + public Task Config(CancellationToken cancellationToken = default) => configurationApi.GetConfig(cancellationToken); [Route("configuration/remotes")] [HttpGet] - public async Task RemoteConfig(CancellationToken cancellationToken) => + public async Task RemoteConfig(CancellationToken cancellationToken = default) => Ok(await configurationApi.GetRemoteConfigs(cancellationToken)); } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/WebApi/SortInfoModelBinder.cs b/src/ServiceControl/Infrastructure/WebApi/SortInfoModelBinder.cs index 0dbe13ecf3..d9ee9195e1 100644 --- a/src/ServiceControl/Infrastructure/WebApi/SortInfoModelBinder.cs +++ b/src/ServiceControl/Infrastructure/WebApi/SortInfoModelBinder.cs @@ -7,6 +7,7 @@ namespace ServiceControl.Infrastructure.WebApi; public class SortInfoModelBinder : IModelBinder { +#pragma warning disable PS0018 // IModelBinder declares this without a CancellationToken public Task BindModelAsync(ModelBindingContext bindingContext) { ArgumentNullException.ThrowIfNull(bindingContext); @@ -28,4 +29,5 @@ public Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.Result = ModelBindingResult.Success(new SortInfo(sort, direction)); return Task.CompletedTask; } -} \ No newline at end of file +#pragma warning restore PS0018 +} diff --git a/src/ServiceControl/Licensing/ActiveLicense.cs b/src/ServiceControl/Licensing/ActiveLicense.cs index 2fe69304a6..2a064fbd2a 100644 --- a/src/ServiceControl/Licensing/ActiveLicense.cs +++ b/src/ServiceControl/Licensing/ActiveLicense.cs @@ -14,7 +14,7 @@ public class ActiveLicense(ITrialLicenseDataProvider trialLicenseDataProvider, I public LicenseDetails Details { get; set; } - public async Task Refresh(CancellationToken cancellationToken) + public async Task Refresh(CancellationToken cancellationToken = default) { logger.LogDebug("Refreshing ActiveLicense"); @@ -27,7 +27,7 @@ public async Task Refresh(CancellationToken cancellationToken) IsEvaluation = detectedLicense.IsEvaluationLicense; } - internal static async Task ValidateTrialLicense(LicenseDetails licenseDetails, ITrialLicenseDataProvider trialLicenseDataProvider, CancellationToken cancellationToken) + internal static async Task ValidateTrialLicense(LicenseDetails licenseDetails, ITrialLicenseDataProvider trialLicenseDataProvider, CancellationToken cancellationToken = default) { if (licenseDetails.LicenseType.Equals("trial", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/ServiceControl/Licensing/LicenseCheckHostedService.cs b/src/ServiceControl/Licensing/LicenseCheckHostedService.cs index 52a7b41a5a..386077c7a5 100644 --- a/src/ServiceControl/Licensing/LicenseCheckHostedService.cs +++ b/src/ServiceControl/Licensing/LicenseCheckHostedService.cs @@ -9,19 +9,19 @@ 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(async _ => + timer = scheduler.Schedule(async token => { - await activeLicense.Refresh(cancellationToken); + await activeLicense.Refresh(token); return TimerJobExecutionResult.ScheduleNextExecution; }, TimeSpan.FromTicks(0), due, ex => logger.LogError(ex, "Unhandled error while refreshing the license")); return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) => timer.Stop(cancellationToken); + public Task StopAsync(CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); TimerJob timer; } diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index cadc314736..a2c734b261 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -23,7 +23,7 @@ public class LicenseController(ActiveLicense activeLicense, Settings settings, M [Authorize(Policy = Permissions.ErrorLicensingView)] [HttpGet] [Route("license")] - public async Task> License(bool refresh, string clientName, CancellationToken cancellationToken) + public async Task> License(bool refresh, string clientName, CancellationToken cancellationToken = default) { if (refresh) { @@ -54,7 +54,7 @@ public async Task> License(bool refresh, string client [Authorize(Policy = Permissions.ErrorThroughputView)] [HttpGet] [Route("license/details")] - public async Task> LicenseDetails(CancellationToken cancellationToken) + public async Task> LicenseDetails(CancellationToken cancellationToken = default) { if (activeLicense.Details.Edition != "Endpoint Size" || !activeLicense.Details.HasEndpointMetadata) { @@ -74,7 +74,7 @@ public async Task> License(bool refresh, string client [Authorize(Policy = Permissions.ErrorThroughputManage)] [HttpPost] [Route("license/detailsUpload")] - public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationToken cancellationToken) + public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationToken cancellationToken = default) { //perform date and license id checks using var brotliStream = new BrotliStream(file.OpenReadStream(), CompressionMode.Decompress); diff --git a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs index 88c91451ed..0e61eca99f 100644 --- a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs @@ -2,6 +2,7 @@ namespace ServiceControl.MessageFailures.Api { using System; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -20,7 +21,7 @@ public class ArchiveMessagesController(IMessageSession messageSession, IGroupsDa [Route("errors/archive")] [HttpPost] [HttpPatch] - public async Task ArchiveBatch(string[] messageIds) + public async Task ArchiveBatch(string[] messageIds, CancellationToken cancellationToken = default) { if (messageIds.Any(string.IsNullOrEmpty)) { @@ -31,13 +32,13 @@ public async Task ArchiveBatch(string[] messageIds) var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Archive, Permissions.ErrorMessagesArchive, MessageActionScope.Batch, - resource: null, count: messageIds.Length, operationId: operationId, async () => + resource: null, count: messageIds.Length, operationId: operationId, async ct => { foreach (var id in messageIds) { - await messageSession.Send(new ArchiveMessage { FailedMessageId = id, Scope = MessageActionScope.Batch }, AuditHeaders.LocalSendOptions(user, operationId)); + await messageSession.Send(new ArchiveMessage { FailedMessageId = id, Scope = MessageActionScope.Batch }, AuditHeaders.LocalSendOptions(user, operationId), ct); } - }); + }, cancellationToken); return Accepted(); } @@ -45,7 +46,7 @@ await auditLog.AuditedOperation(user, MessageActionKind.Archive, Permissions.Err [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/groups/{classifier?}")] [HttpGet] - public async Task GetArchiveMessageGroups(string classifier = "Exception Type and Stack Trace") + public async Task GetArchiveMessageGroups(string classifier = "Exception Type and Stack Trace", CancellationToken cancellationToken = default) { var results = await dataStore.GetArchivedGroupsByClassifier(classifier); @@ -58,13 +59,13 @@ public async Task GetArchiveMessageGroups(string classifier = "Ex [Route("errors/{messageId:required:minlength(1)}/archive")] [HttpPost] [HttpPatch] - public async Task Archive(string messageId) + public async Task Archive(string messageId, CancellationToken cancellationToken = default) { var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Archive, Permissions.ErrorMessagesArchive, MessageActionScope.Single, resource: messageId, count: 1, operationId: operationId, - () => messageSession.Send(new ArchiveMessage { FailedMessageId = messageId, Scope = MessageActionScope.Single }, AuditHeaders.LocalSendOptions(user, operationId))); + ct => messageSession.Send(new ArchiveMessage { FailedMessageId = messageId, Scope = MessageActionScope.Single }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -72,7 +73,7 @@ await auditLog.AuditedOperation(user, MessageActionKind.Archive, Permissions.Err [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("archive/groups/id/{groupId:required:minlength(1)}")] [HttpGet] - public async Task> GetGroup(string groupId, string status = default, string modified = default) + public async Task> GetGroup(string groupId, string status = default, string modified = default, CancellationToken cancellationToken = default) { var result = await dataStore.GetArchivedGroup(groupId, status, modified); diff --git a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs index f6906abad9..3ef7cd96b5 100644 --- a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -35,7 +36,7 @@ public class EditFailedMessagesController( [Authorize(Policy = Permissions.ErrorMessagesEdit)] [Route("edit/{failedMessageId:required:minlength(1)}")] [HttpPost] - public async Task> Edit(string failedMessageId, [FromBody] EditMessageModel edit) + public async Task> Edit(string failedMessageId, [FromBody] EditMessageModel edit, CancellationToken cancellationToken = default) { if (!settings.AllowMessageEditing) { @@ -91,12 +92,12 @@ public async Task> Edit(string failedMessageId, await auditLog.AuditedOperation(user, MessageActionKind.Edit, Permissions.ErrorMessagesEdit, MessageActionScope.Single, resource: failedMessageId, count: 1, operationId: operationId, - () => session.Send(new EditAndSend + ct => session.Send(new EditAndSend { FailedMessageId = failedMessageId, NewBody = base64String, NewHeaders = edit.MessageHeaders - }, AuditHeaders.LocalSendOptions(user, operationId))); + }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(new EditRetryResponse { EditIgnored = false }); } diff --git a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs index 15d6c81102..507ee6c63a 100644 --- a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.MessageFailures.Api { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -16,7 +17,7 @@ public class GetAllErrorsController(IFailedMessageQueryDataStore store) : Contro [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors")] [HttpGet] - public async Task> ErrorsGet([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string queueAddress) + public async Task> ErrorsGet([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string queueAddress, CancellationToken cancellationToken = default) { var results = await store.GetFailedMessages( status: status, @@ -34,7 +35,7 @@ public async Task> ErrorsGet([FromQuery] PagingInfo pag [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors")] [HttpHead] - public async Task ErrorsHead(string status, string modified, string queueAddress) + public async Task ErrorsHead(string status, string modified, string queueAddress, CancellationToken cancellationToken = default) { var queryResult = await store.GetFailedMessagesStats( status: status, @@ -48,7 +49,7 @@ public async Task ErrorsHead(string status, string modified, string queueAddress [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpointname}/errors")] [HttpGet] - public async Task> ErrorsByEndpointName([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string endpointName) + public async Task> ErrorsByEndpointName([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string endpointName, CancellationToken cancellationToken = default) { var results = await store.GetFailedMessagesByEndpoint( status: status, @@ -66,6 +67,6 @@ public async Task> ErrorsByEndpointName([FromQuery] Pag [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/summary")] [HttpGet] - public async Task> ErrorsSummary() => await store.GetFailedMessagesSummary(); + public async Task> ErrorsSummary(CancellationToken cancellationToken = default) => await store.GetFailedMessagesSummary(); } } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs index 271420f2a5..65eb2ba47e 100644 --- a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs @@ -1,5 +1,6 @@ namespace ServiceControl.MessageFailures.Api { + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Microsoft.AspNetCore.Authorization; @@ -13,7 +14,7 @@ public class GetErrorByIdController(IFailedMessageQueryDataStore store) : Contro [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/{failedMessageId:required:minlength(1)}")] [HttpGet] - public async Task> ErrorBy(string failedMessageId) + public async Task> ErrorBy(string failedMessageId, CancellationToken cancellationToken = default) { var result = await store.GetFailedMessage(failedMessageId); @@ -23,7 +24,7 @@ public async Task> ErrorBy(string failedMessageId) [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/last/{failedMessageId:required:minlength(1)}")] [HttpGet] - public async Task> ErrorLastBy(string failedMessageId) + public async Task> ErrorLastBy(string failedMessageId, CancellationToken cancellationToken = default) { var result = await store.GetLatestFailedMessageView(failedMessageId); diff --git a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs index e0b62e3576..b3952d01c7 100644 --- a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Serialization; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -19,7 +20,7 @@ public class PendingRetryMessagesController(IMessageSession session, ICurrentUse [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/retry")] [HttpPost] - public async Task RetryBy(string[] ids) + public async Task RetryBy(string[] ids, CancellationToken cancellationToken = default) { if (ids.Any(string.IsNullOrEmpty)) { @@ -31,7 +32,7 @@ public async Task RetryBy(string[] ids) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Batch, resource: null, count: ids.Length, operationId: operationId, - () => session.Send(m => m.MessageUniqueIds = ids, AuditHeaders.LocalSendOptions(user, operationId))); + ct => session.Send(m => m.MessageUniqueIds = ids, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -39,18 +40,18 @@ await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.Error [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/queues/retry")] [HttpPost] - public async Task RetryBy(PendingRetryRequest request) + public async Task RetryBy(PendingRetryRequest request, CancellationToken cancellationToken = default) { var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Queue, resource: request.QueueAddress, count: null, operationId: operationId, - () => session.Send(m => + ct => session.Send(m => { m.QueueAddress = request.QueueAddress; m.PeriodFrom = request.From; m.PeriodTo = request.To; - }, AuditHeaders.LocalSendOptions(user, operationId))); + }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } diff --git a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs index dc82dfe18a..3aeb27e031 100644 --- a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs +++ b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.MessageFailures.Api; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -16,7 +17,7 @@ public class QueueAddressController(IQueueAddressStore store) : ControllerBase [Authorize(Policy = Permissions.ErrorQueuesView)] [Route("errors/queues/addresses")] [HttpGet] - public async Task> GetAddresses([FromQuery] PagingInfo pagingInfo) + public async Task> GetAddresses([FromQuery] PagingInfo pagingInfo, CancellationToken cancellationToken = default) { var result = await store.GetAddresses(pagingInfo); diff --git a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs index 21c05d2cc1..0b3a896035 100644 --- a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs @@ -7,6 +7,7 @@ namespace ServiceControl.MessageFailures.Api using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Serialization; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using InternalMessages; @@ -22,7 +23,7 @@ public class ResolveMessagesController(IMessageSession session) : ControllerBase [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/resolve")] [HttpPatch] - public async Task ResolveBy(UniqueMessageIdsModel request) + public async Task ResolveBy(UniqueMessageIdsModel request, CancellationToken cancellationToken = default) { if (request.UniqueMessageIds != null) { @@ -34,7 +35,7 @@ public async Task ResolveBy(UniqueMessageIdsModel request) foreach (var id in request.UniqueMessageIds) { - await session.SendLocal(new MarkPendingRetryAsResolved { FailedMessageId = id }); + await session.SendLocal(new MarkPendingRetryAsResolved { FailedMessageId = id }, cancellationToken); } return Accepted(); @@ -59,7 +60,7 @@ await session.SendLocal(m => { m.PeriodFrom = request.From!.Value; m.PeriodTo = request.To!.Value; - }); + }, cancellationToken); return Accepted(); } @@ -67,14 +68,14 @@ await session.SendLocal(m => [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/queues/resolve")] [HttpPatch] - public async Task ResolveBy(QueueModel queueModel) + public async Task ResolveBy(QueueModel queueModel, CancellationToken cancellationToken = default) { await session.SendLocal(m => { m.QueueAddress = queueModel.QueueAddress; m.PeriodFrom = queueModel.From; m.PeriodTo = queueModel.To; - }); + }, cancellationToken); return Accepted(); } diff --git a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs index f09c72e280..ff723a7fd5 100644 --- a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -32,7 +33,7 @@ public class RetryMessagesController( [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/{failedMessageId:required:minlength(1)}/retry")] [HttpPost] - public async Task RetryMessageBy([FromQuery(Name = "instance_id")] string instanceId, string failedMessageId) + public async Task RetryMessageBy([FromQuery(Name = "instance_id")] string instanceId, string failedMessageId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.InstanceId) { @@ -40,7 +41,7 @@ public async Task RetryMessageBy([FromQuery(Name = "instance_id") var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Single, resource: failedMessageId, count: 1, operationId: operationId, - () => messageSession.Send(m => m.FailedMessageId = failedMessageId, AuditHeaders.LocalSendOptions(user, operationId))); + ct => messageSession.Send(m => m.FailedMessageId = failedMessageId, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -63,7 +64,7 @@ await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.Error [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/retry")] [HttpPost] - public async Task RetryAllBy(List messageIds) + public async Task RetryAllBy(List messageIds, CancellationToken cancellationToken = default) { if (messageIds.Any(string.IsNullOrEmpty)) { @@ -74,7 +75,7 @@ public async Task RetryAllBy(List messageIds) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Batch, resource: null, count: messageIds.Count, operationId: operationId, - () => messageSession.Send(m => m.MessageUniqueIds = messageIds.ToArray(), AuditHeaders.LocalSendOptions(user, operationId))); + ct => messageSession.Send(m => m.MessageUniqueIds = messageIds.ToArray(), AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -82,17 +83,17 @@ await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.Error [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/queues/{queueAddress:required:minlength(1)}/retry")] [HttpPost] - public async Task RetryAllBy(string queueAddress) + public async Task RetryAllBy(string queueAddress, CancellationToken cancellationToken = default) { var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Queue, resource: queueAddress, count: null, operationId: operationId, - () => messageSession.Send(m => + ct => messageSession.Send(m => { m.QueueAddress = queueAddress; m.Status = FailedMessageStatus.Unresolved; - }, AuditHeaders.LocalSendOptions(user, operationId))); + }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -100,13 +101,13 @@ await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.Error [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/retry/all")] [HttpPost] - public async Task RetryAll() + public async Task RetryAll(CancellationToken cancellationToken = default) { var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.All, resource: null, count: null, operationId: operationId, - () => messageSession.Send(new RequestRetryAll(), AuditHeaders.LocalSendOptions(user, operationId))); + ct => messageSession.Send(new RequestRetryAll(), AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -114,13 +115,13 @@ await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.Error [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/{endpointName:required:minlength(1)}/retry/all")] [HttpPost] - public async Task RetryAllByEndpoint(string endpointName) + public async Task RetryAllByEndpoint(string endpointName, CancellationToken cancellationToken = default) { var user = userAccessor.Resolve(User); var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorMessagesRetry, MessageActionScope.Endpoint, resource: endpointName, count: null, operationId: operationId, - () => messageSession.Send(new RequestRetryAll { Endpoint = endpointName }, AuditHeaders.LocalSendOptions(user, operationId))); + ct => messageSession.Send(new RequestRetryAll { Endpoint = endpointName }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } diff --git a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs index bcd37d2040..92fe85835f 100644 --- a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -18,7 +19,7 @@ public class UnArchiveMessagesController(IMessageSession session, ICurrentUserAc [Authorize(Policy = Permissions.ErrorMessagesUnarchive)] [Route("errors/unarchive")] [HttpPatch] - public async Task Unarchive(string[] ids) + public async Task Unarchive(string[] ids, CancellationToken cancellationToken = default) { if (ids.Any(string.IsNullOrEmpty)) { @@ -29,7 +30,7 @@ public async Task Unarchive(string[] ids) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Unarchive, Permissions.ErrorMessagesUnarchive, MessageActionScope.Batch, resource: null, count: ids.Length, operationId: operationId, - () => session.Send(new UnArchiveMessages { FailedMessageIds = ids }, AuditHeaders.LocalSendOptions(user, operationId))); + ct => session.Send(new UnArchiveMessages { FailedMessageIds = ids }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } @@ -37,7 +38,7 @@ await auditLog.AuditedOperation(user, MessageActionKind.Unarchive, Permissions.E [Authorize(Policy = Permissions.ErrorMessagesUnarchive)] [Route("errors/{from}...{to}/unarchive")] [HttpPatch] - public async Task Unarchive(string from, string to) + public async Task Unarchive(string from, string to, CancellationToken cancellationToken = default) { DateTime fromDateTime, toDateTime; @@ -55,7 +56,7 @@ public async Task Unarchive(string from, string to) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Unarchive, Permissions.ErrorMessagesUnarchive, MessageActionScope.Range, resource: $"{from}...{to}", count: null, operationId: operationId, - () => session.Send(new UnArchiveMessagesByRange { From = fromDateTime, To = toDateTime }, AuditHeaders.LocalSendOptions(user, operationId))); + ct => session.Send(new UnArchiveMessagesByRange { From = fromDateTime, To = toDateTime }, AuditHeaders.LocalSendOptions(user, operationId), ct), cancellationToken); return Accepted(); } diff --git a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs index 83599d162c..c036e764a9 100644 --- a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs +++ b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Text.Json.Serialization; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageRedirects; using Infrastructure.Auth; @@ -30,7 +31,7 @@ public class MessageRedirectsController( [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects")] [HttpPost] - public async Task NewRedirects(MessageRedirectRequest request) + public async Task NewRedirects(MessageRedirectRequest request, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(request.FromPhysicalAddress) || string.IsNullOrWhiteSpace(request.ToPhysicalAddress)) { @@ -78,7 +79,7 @@ await events.Raise(new MessageRedirectCreated MessageRedirectId = messageRedirect.MessageRedirectId, FromPhysicalAddress = messageRedirect.FromPhysicalAddress, ToPhysicalAddress = messageRedirect.ToPhysicalAddress - }); + }, cancellationToken); if (request.RetryExisting) { @@ -87,7 +88,7 @@ await session.SendLocal(new RetryPendingMessages QueueAddress = messageRedirect.FromPhysicalAddress, PeriodFrom = DateTime.MinValue, PeriodTo = DateTime.UtcNow - }); + }, cancellationToken); } // not using Created here because that would be turned by the HttpNoContentOutputFormatter into a 204 @@ -97,7 +98,7 @@ await session.SendLocal(new RetryPendingMessages [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects/{messageRedirectId:guid}")] [HttpPut] - public async Task UpdateRedirect(Guid messageRedirectId, MessageRedirectRequest request) + public async Task UpdateRedirect(Guid messageRedirectId, MessageRedirectRequest request, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(request.ToPhysicalAddress)) { @@ -132,7 +133,7 @@ public async Task UpdateRedirect(Guid messageRedirectId, MessageR await store.UpdateRedirect(messageRedirect); - await events.Raise(messageRedirectChanged); + await events.Raise(messageRedirectChanged, cancellationToken); return NoContent(); } @@ -140,7 +141,7 @@ public async Task UpdateRedirect(Guid messageRedirectId, MessageR [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects/{messageRedirectId:guid}")] [HttpDelete] - public async Task DeleteRedirect(Guid messageRedirectId) + public async Task DeleteRedirect(Guid messageRedirectId, CancellationToken cancellationToken = default) { var redirects = await store.GetRedirects(); @@ -158,7 +159,7 @@ await events.Raise(new MessageRedirectRemoved MessageRedirectId = messageRedirectId, FromPhysicalAddress = messageRedirect.FromPhysicalAddress, ToPhysicalAddress = messageRedirect.ToPhysicalAddress - }); + }, cancellationToken); return NoContent(); } @@ -166,7 +167,7 @@ await events.Raise(new MessageRedirectRemoved [Authorize(Policy = Permissions.ErrorRedirectsView)] [Route("redirect")] [HttpHead] - public async Task CountRedirects() + public async Task CountRedirects(CancellationToken cancellationToken = default) { var redirects = await store.GetRedirects(); @@ -177,7 +178,7 @@ public async Task CountRedirects() [Authorize(Policy = Permissions.ErrorRedirectsView)] [Route("redirects")] [HttpGet] - public async Task> Redirects(string sort, string direction, [FromQuery] PagingInfo pagingInfo) + public async Task> Redirects(string sort, string direction, [FromQuery] PagingInfo pagingInfo, CancellationToken cancellationToken = default) { var redirects = await store.GetRedirects(); diff --git a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs index 5ec475da02..5298b6a672 100644 --- a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs +++ b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Monitoring { using System; + using System.Threading; using System.Threading.Tasks; using Contracts.HeartbeatMonitoring; using EndpointControl.Contracts; @@ -23,23 +24,23 @@ public EndpointInstanceMonitor(EndpointInstanceId endpointInstanceId, bool monit public bool Monitored { get; private set; } - public async Task EnableMonitoring() + public async Task EnableMonitoring(CancellationToken cancellationToken = default) { - await domainEvents.Raise(new MonitoringEnabledForEndpoint { Endpoint = Convert(Id) }); + await domainEvents.Raise(new MonitoringEnabledForEndpoint { Endpoint = Convert(Id) }, cancellationToken); Monitored = true; } - public async Task DisableMonitoring() + public async Task DisableMonitoring(CancellationToken cancellationToken = default) { - await domainEvents.Raise(new MonitoringDisabledForEndpoint { Endpoint = Convert(Id) }); + await domainEvents.Raise(new MonitoringDisabledForEndpoint { Endpoint = Convert(Id) }, cancellationToken); Monitored = false; } - public async Task UpdateStatus(HeartbeatStatus newStatus, DateTime? latestTimestamp) + public async Task UpdateStatus(HeartbeatStatus newStatus, DateTime? latestTimestamp, CancellationToken cancellationToken = default) { if (newStatus != status) { - await RaiseStateChangeEvents(newStatus, latestTimestamp); + await RaiseStateChangeEvents(newStatus, latestTimestamp, cancellationToken); logger.LogDebug("Endpoint {LogicalEndpointName} status updated from {OldHeartbeatStatus} to {NewHeartbeatStatus}", Id.LogicalName, status, newStatus); } @@ -47,7 +48,7 @@ public async Task UpdateStatus(HeartbeatStatus newStatus, DateTime? latestTimest status = newStatus; } - async Task RaiseStateChangeEvents(HeartbeatStatus newStatus, DateTime? latestTimestamp) + async Task RaiseStateChangeEvents(HeartbeatStatus newStatus, DateTime? latestTimestamp, CancellationToken cancellationToken) { if (newStatus == HeartbeatStatus.Alive) { @@ -60,7 +61,7 @@ await domainEvents.Raise(new HeartbeatingEndpointDetected { Endpoint = Convert(Id), DetectedAt = latestTimestamp ?? DateTime.UtcNow - }); + }, cancellationToken); } else if (status == HeartbeatStatus.Dead && Monitored) { @@ -68,7 +69,7 @@ await domainEvents.Raise(new EndpointHeartbeatRestored { Endpoint = Convert(Id), RestoredAt = latestTimestamp ?? DateTime.UtcNow - }); + }, cancellationToken); } } else if (newStatus == HeartbeatStatus.Dead && Monitored) @@ -78,7 +79,7 @@ await domainEvents.Raise(new EndpointFailedToHeartbeat Endpoint = Convert(Id), DetectedAt = DateTime.UtcNow, LastReceivedAt = latestTimestamp ?? DateTime.MinValue - }); + }, cancellationToken); } } diff --git a/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs b/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs index 39319fc6d3..d53cb08be1 100644 --- a/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs +++ b/src/ServiceControl/Monitoring/EndpointInstanceMonitoring.cs @@ -4,6 +4,7 @@ namespace ServiceControl.Monitoring using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.EndpointControl; using Contracts.HeartbeatMonitoring; @@ -28,7 +29,7 @@ public EndpointInstanceMonitoring(IDomainEvents domainEvents, ILogger heartbeats.GetOrAdd(endpointInstanceId, id => new HeartbeatMonitor()).MarkAlive(timestamp); - public async Task CheckEndpoints(DateTime threshold) + public async Task CheckEndpoints(DateTime threshold, CancellationToken cancellationToken = default) { foreach (var entry in heartbeats) { @@ -36,12 +37,12 @@ public async Task CheckEndpoints(DateTime threshold) var endpointInstanceId = entry.Key; var monitor = endpoints.GetOrAdd(endpointInstanceId.UniqueId, id => new EndpointInstanceMonitor(endpointInstanceId, true, domainEvents, monitorLogger)); - await monitor.UpdateStatus(recordedHeartbeat.Status, recordedHeartbeat.Timestamp); + await monitor.UpdateStatus(recordedHeartbeat.Status, recordedHeartbeat.Timestamp, cancellationToken); } var stats = GetStats(); - await Update(stats); + await Update(stats, cancellationToken); } public bool IsNewInstance(EndpointDetails newEndpointDetails) @@ -51,7 +52,7 @@ public bool IsNewInstance(EndpointDetails newEndpointDetails) return endpoints.TryAdd(endpointInstanceId.UniqueId, new EndpointInstanceMonitor(endpointInstanceId, false, domainEvents, monitorLogger)); } - public async Task EndpointDetected(EndpointDetails newEndpointDetails) + public async Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default) { var endpointInstanceId = newEndpointDetails.ToInstanceId(); if (endpoints.TryAdd(endpointInstanceId.UniqueId, new EndpointInstanceMonitor(endpointInstanceId, false, domainEvents, monitorLogger))) @@ -60,11 +61,11 @@ await domainEvents.Raise(new EndpointDetected { DetectedAt = DateTime.UtcNow, Endpoint = newEndpointDetails - }); + }, cancellationToken); } } - public async Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt) + public async Task DetectEndpointFromHeartbeatStartup(EndpointDetails newEndpointDetails, DateTime startedAt, CancellationToken cancellationToken = default) { var endpointInstanceId = newEndpointDetails.ToInstanceId(); endpoints.GetOrAdd(endpointInstanceId.UniqueId, id => new EndpointInstanceMonitor(endpointInstanceId, true, domainEvents, monitorLogger)); @@ -73,7 +74,7 @@ await domainEvents.Raise(new EndpointStarted { EndpointDetails = newEndpointDetails, StartedAt = startedAt - }); + }, cancellationToken); } public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, bool monitored) @@ -82,7 +83,7 @@ public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, b endpoints.GetOrAdd(endpointInstanceId.UniqueId, id => new EndpointInstanceMonitor(endpointInstanceId, monitored, domainEvents, monitorLogger)); } - async Task Update(EndpointMonitoringStats stats) + async Task Update(EndpointMonitoringStats stats, CancellationToken cancellationToken) { var previousActive = previousStats?.Active ?? 0; var previousDead = previousStats?.Failing ?? 0; @@ -93,7 +94,7 @@ await domainEvents.Raise(new HeartbeatsUpdated Active = stats.Active, Failing = stats.Failing, RaisedAt = DateTime.UtcNow - }); + }, cancellationToken); previousStats = stats; } } @@ -109,8 +110,8 @@ public EndpointMonitoringStats GetStats() return stats; } - public Task EnableMonitoring(Guid id) => endpoints[id]?.EnableMonitoring(); - public Task DisableMonitoring(Guid id) => endpoints[id]?.DisableMonitoring(); + public Task EnableMonitoring(Guid id, CancellationToken cancellationToken = default) => endpoints[id]?.EnableMonitoring(cancellationToken); + public Task DisableMonitoring(Guid id, CancellationToken cancellationToken = default) => endpoints[id]?.DisableMonitoring(cancellationToken); public bool IsMonitored(Guid id) => endpoints.ContainsKey(id) && endpoints[id].Monitored; public EndpointsView[] GetEndpoints() diff --git a/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs b/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs index 2856c00c94..d866c78b91 100644 --- a/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs +++ b/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs @@ -21,7 +21,7 @@ public class HeartbeatEndpointSettingsSyncHostedService( { public TimeSpan DelayStart { get; set; } = TimeSpan.FromSeconds(20); - protected override async Task ExecuteAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { logger.LogInformation("Starting {ServiceName}", nameof(HeartbeatEndpointSettingsSyncHostedService)); @@ -38,7 +38,11 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) logger.LogInformation("Performing sync for {ServiceName}", nameof(HeartbeatEndpointSettingsSyncHostedService)); await PerformSync(cancellationToken); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) { logger.LogError(ex, $"Failed to perform sync between data in {nameof(IEndpointInstanceMonitoring)} and {nameof(IEndpointSettingsStore)}"); diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoring/RegisterEndpointStartupHandler.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoring/RegisterEndpointStartupHandler.cs index 036f02d724..1d02d86dd7 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoring/RegisterEndpointStartupHandler.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoring/RegisterEndpointStartupHandler.cs @@ -17,7 +17,7 @@ public Task Handle(RegisterEndpointStartup message, IMessageHandlerContext conte HostId = message.HostId, Name = message.Endpoint }; - return monitoring.DetectEndpointFromHeartbeatStartup(endpointDetails, message.StartedAt); + return monitoring.DetectEndpointFromHeartbeatStartup(endpointDetails, message.StartedAt, context.CancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs index 93f6818d74..2a93f67c96 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs @@ -19,21 +19,21 @@ public HeartbeatMonitoringHostedService(IEndpointInstanceMonitoring monitor, IMo this.logger = logger; gracePeriod = settings.HeartbeatGracePeriod; } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken = default) { await persistence.WarmupMonitoringFromPersistence(monitor); - timer = scheduler.Schedule(_ => CheckEndpoints(), TimeSpan.Zero, TimeSpan.FromSeconds(5), e => logger.LogError(e, "Exception occurred when monitoring endpoint instances")); + timer = scheduler.Schedule(CheckEndpoints, TimeSpan.Zero, TimeSpan.FromSeconds(5), e => logger.LogError(e, "Exception occurred when monitoring endpoint instances")); } - public Task StopAsync(CancellationToken cancellationToken) => timer.Stop(cancellationToken); + public Task StopAsync(CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); - async Task CheckEndpoints() + async Task CheckEndpoints(CancellationToken cancellationToken) { var inactivityThreshold = DateTime.UtcNow - gracePeriod; logger.LogDebug("Monitoring Endpoint Instances. Inactivity Threshold = {InactivityThreshold}", inactivityThreshold); - await monitor.CheckEndpoints(inactivityThreshold); + await monitor.CheckEndpoints(inactivityThreshold, cancellationToken); return TimerJobExecutionResult.ScheduleNextExecution; } diff --git a/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs b/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs index 7dfae2050e..1a63770e8b 100644 --- a/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs +++ b/src/ServiceControl/Monitoring/HeartbeatRestoredPublisher.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Monitoring using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts; using Contracts.HeartbeatMonitoring; @@ -21,7 +22,7 @@ protected override DispatchContext CreateDispatchRequest(EndpointHeartbeatRestor }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new HeartbeatRestored { diff --git a/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs b/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs index b3cc02365b..2e93e61b68 100644 --- a/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs +++ b/src/ServiceControl/Monitoring/HeartbeatStoppedPublisher.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Monitoring using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts; using Contracts.HeartbeatMonitoring; @@ -22,7 +23,7 @@ protected override DispatchContext CreateDispatchRequest(EndpointFailedToHeartbe }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new HeartbeatStopped { diff --git a/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs b/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs index 57603cdce4..bedf4e7a5c 100644 --- a/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Monitoring { using System; + using System.Threading; using System.Threading.Tasks; using Connection; using NServiceBus; @@ -17,7 +18,7 @@ public HeartbeatsPlatformConnectionDetailsProvider(Settings settings, ReceiveAdd instanceMainQueue = receiveAddresses.MainReceiveAddress; } - public Task ProvideConnectionDetails(PlatformConnectionDetails connection) + public Task ProvideConnectionDetails(PlatformConnectionDetails connection, CancellationToken cancellationToken = default) { // NOTE: The default grace period is 40s and the default frequency is 10s. // In a low-latency environment, an endpoint would need to miss more than 4 heartbeats to be considered down diff --git a/src/ServiceControl/Monitoring/MonitoringDataPersister.cs b/src/ServiceControl/Monitoring/MonitoringDataPersister.cs index 3707e314a4..46248bb43e 100644 --- a/src/ServiceControl/Monitoring/MonitoringDataPersister.cs +++ b/src/ServiceControl/Monitoring/MonitoringDataPersister.cs @@ -20,22 +20,22 @@ public MonitoringDataPersister(IMonitoringDataStore monitoringDataStore, IEndpoi _endpointInstanceMonitoring = endpointInstanceMonitoring; } - public Task Handle(EndpointDetected domainEvent, CancellationToken cancellationToken) + public Task Handle(EndpointDetected domainEvent, CancellationToken cancellationToken = default) { return _monitoringDataStore.CreateIfNotExists(domainEvent.Endpoint); } - public Task Handle(HeartbeatingEndpointDetected domainEvent, CancellationToken cancellationToken) + public Task Handle(HeartbeatingEndpointDetected domainEvent, CancellationToken cancellationToken = default) { return _monitoringDataStore.CreateOrUpdate(domainEvent.Endpoint, _endpointInstanceMonitoring); } - public Task Handle(MonitoringEnabledForEndpoint domainEvent, CancellationToken cancellationToken) + public Task Handle(MonitoringEnabledForEndpoint domainEvent, CancellationToken cancellationToken = default) { return _monitoringDataStore.UpdateEndpointMonitoring(domainEvent.Endpoint, true); } - public Task Handle(MonitoringDisabledForEndpoint domainEvent, CancellationToken cancellationToken) + public Task Handle(MonitoringDisabledForEndpoint domainEvent, CancellationToken cancellationToken = default) { return _monitoringDataStore.UpdateEndpointMonitoring(domainEvent.Endpoint, false); } diff --git a/src/ServiceControl/Monitoring/RegisterNewEndpointHandler.cs b/src/ServiceControl/Monitoring/RegisterNewEndpointHandler.cs index 087379f5f5..853c824d78 100644 --- a/src/ServiceControl/Monitoring/RegisterNewEndpointHandler.cs +++ b/src/ServiceControl/Monitoring/RegisterNewEndpointHandler.cs @@ -11,8 +11,8 @@ class RegisterNewEndpointHandler(IEndpointInstanceMonitoring endpointInstanceMon IHandleMessages { // for backward compatibility reasons - public Task Handle(NewEndpointDetected message, IMessageHandlerContext context) => endpointInstanceMonitoring.EndpointDetected(message.Endpoint); + public Task Handle(NewEndpointDetected message, IMessageHandlerContext context) => endpointInstanceMonitoring.EndpointDetected(message.Endpoint, context.CancellationToken); - public Task Handle(RegisterNewEndpoint message, IMessageHandlerContext context) => endpointInstanceMonitoring.EndpointDetected(message.Endpoint); + public Task Handle(RegisterNewEndpoint message, IMessageHandlerContext context) => endpointInstanceMonitoring.EndpointDetected(message.Endpoint, context.CancellationToken); } } \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs index 8adf7d13c7..7a6cf742a9 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -47,7 +48,7 @@ public void GetSupportedOperations() [Authorize(Policy = Permissions.ErrorEndpointsDelete)] [Route("endpoints/{endpointId}")] [HttpDelete] - public async Task DeleteEndpoint(Guid endpointId) + public async Task DeleteEndpoint(Guid endpointId, CancellationToken cancellationToken = default) { if (!monitoring.HasEndpoint(endpointId)) { @@ -74,7 +75,7 @@ public IList KnownEndpoints([FromQuery] PagingInfo pagingInf [Authorize(Policy = Permissions.ErrorEndpointsManage)] [Route("endpoints/{endpointId}")] [HttpPatch] - public async Task Monitoring(Guid endpointId, [FromBody] EndpointUpdateModel data) + public async Task Monitoring(Guid endpointId, [FromBody] EndpointUpdateModel data, CancellationToken cancellationToken = default) { if (!monitoring.HasEndpoint(endpointId)) { @@ -83,11 +84,11 @@ public async Task Monitoring(Guid endpointId, [FromBody] Endpoint if (data.MonitorHeartbeat) { - await monitoring.EnableMonitoring(endpointId); + await monitoring.EnableMonitoring(endpointId, cancellationToken); } else { - await monitoring.DisableMonitoring(endpointId); + await monitoring.DisableMonitoring(endpointId, cancellationToken); } return Accepted(); diff --git a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs index 44bdd6f05e..ebcbac29f4 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs @@ -30,10 +30,10 @@ public class EndpointsSettingsController( [Authorize(Policy = Permissions.ErrorEndpointsView)] [Route("endpointssettings")] [HttpGet] - public async IAsyncEnumerable Endpoints([EnumeratorCancellation] CancellationToken token) + public async IAsyncEnumerable Endpoints([EnumeratorCancellation] CancellationToken cancellationToken = default) { await using IAsyncEnumerator enumerator = - dataStore.GetAllEndpointSettings(token).GetAsyncEnumerator(token); + dataStore.GetAllEndpointSettings(cancellationToken).GetAsyncEnumerator(cancellationToken); bool noResults = true; while (await enumerator.MoveNextAsync()) { @@ -56,14 +56,14 @@ public async IAsyncEnumerable Endpoints([EnumeratorCancellation] C [Route("endpointssettings/{endpointName?}")] [HttpPatch] public async Task - UpdateTrackingSetting(string endpointName, [FromBody] EndpointSettingsUpdateModel data, CancellationToken token) + UpdateTrackingSetting(string endpointName, [FromBody] EndpointSettingsUpdateModel data, CancellationToken cancellationToken = default) { await dataStore.UpdateEndpointSettings(new EndpointSettings { #pragma warning disable IDE0055 Name = endpointName ?? string.Empty, TrackInstances = data.TrackInstances #pragma warning restore IDE0055 - }, token); + }, cancellationToken); return Accepted(); } } \ No newline at end of file diff --git a/src/ServiceControl/Notifications/Api/NotificationsController.cs b/src/ServiceControl/Notifications/Api/NotificationsController.cs index c12ae41447..2c441a260d 100644 --- a/src/ServiceControl/Notifications/Api/NotificationsController.cs +++ b/src/ServiceControl/Notifications/Api/NotificationsController.cs @@ -2,6 +2,7 @@ { using System; using System.Net; + using System.Threading; using System.Threading.Tasks; using Email; using Infrastructure.Auth; @@ -17,7 +18,7 @@ public class NotificationsController(INotificationsDataStore store, Settings set [Authorize(Policy = Permissions.ErrorNotificationsView)] [Route("notifications/email")] [HttpGet] - public async Task GetEmailNotificationsSettings() + public async Task GetEmailNotificationsSettings(CancellationToken cancellationToken = default) { await using var manager = await store.CreateNotificationsManager(); var notificationsSettings = await manager.LoadSettings(); @@ -28,7 +29,7 @@ public async Task GetEmailNotificationsSettings() [Authorize(Policy = Permissions.ErrorNotificationsManage)] [Route("notifications/email/toggle")] [HttpPost] - public async Task ToggleEmailNotifications(ToggleEmailNotifications request) + public async Task ToggleEmailNotifications(ToggleEmailNotifications request, CancellationToken cancellationToken = default) { await using var manager = await store.CreateNotificationsManager(); var notificationsSettings = await manager.LoadSettings(); @@ -43,7 +44,7 @@ public async Task ToggleEmailNotifications(ToggleEmailNotificatio [Authorize(Policy = Permissions.ErrorNotificationsManage)] [Route("notifications/email")] [HttpPost] - public async Task UpdateSettings(UpdateEmailNotificationsSettingsRequest request) + public async Task UpdateSettings(UpdateEmailNotificationsSettingsRequest request, CancellationToken cancellationToken = default) { await using var manager = await store.CreateNotificationsManager(); var notificationsSettings = await manager.LoadSettings(); @@ -68,7 +69,7 @@ public async Task UpdateSettings(UpdateEmailNotificationsSettings [Authorize(Policy = Permissions.ErrorNotificationsTest)] [Route("notifications/email/test")] [HttpPost] - public async Task SendTestEmail() + public async Task SendTestEmail(CancellationToken cancellationToken = default) { await using var manager = await store.CreateNotificationsManager(); var notificationsSettings = await manager.LoadSettings(); @@ -78,7 +79,12 @@ public async Task SendTestEmail() await emailSender.Send( notificationsSettings.Email, $"[{settings.InstanceName}] health check notification check successful", - $"[{settings.InstanceName}] health check notification check successful."); + $"[{settings.InstanceName}] health check notification check successful.", + cancellationToken: cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception e) { diff --git a/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs b/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs index cb4d5351ed..ef697065c2 100644 --- a/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs +++ b/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs @@ -42,7 +42,7 @@ public CustomChecksMailNotification(IMessageSession messageSession, Settings set } } - public Task Handle(CustomCheckFailed domainEvent, CancellationToken cancellationToken) + public Task Handle(CustomCheckFailed domainEvent, CancellationToken cancellationToken = default) { if (IsHealthCheck(domainEvent.CustomCheckId)) { @@ -67,7 +67,7 @@ public Task Handle(CustomCheckFailed domainEvent, CancellationToken cancellation return Task.CompletedTask; } - public Task Handle(CustomCheckSucceeded domainEvent, CancellationToken cancellationToken) + public Task Handle(CustomCheckSucceeded domainEvent, CancellationToken cancellationToken = default) { if (IsHealthCheck(domainEvent.CustomCheckId)) { diff --git a/src/ServiceControl/Notifications/Email/EmailNotificationHostedService.cs b/src/ServiceControl/Notifications/Email/EmailNotificationHostedService.cs index 5e85f3b991..fbc2fea34a 100644 --- a/src/ServiceControl/Notifications/Email/EmailNotificationHostedService.cs +++ b/src/ServiceControl/Notifications/Email/EmailNotificationHostedService.cs @@ -9,9 +9,9 @@ class EmailNotificationHostedService : IHostedService EmailThrottlingState throttlingState; public EmailNotificationHostedService(EmailThrottlingState throttlingState) => this.throttlingState = throttlingState; - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken = default) { throttlingState.CancellationTokenSource.Cancel(); diff --git a/src/ServiceControl/Notifications/Email/EmailSender.cs b/src/ServiceControl/Notifications/Email/EmailSender.cs index 57e07375c5..5fe9091f65 100644 --- a/src/ServiceControl/Notifications/Email/EmailSender.cs +++ b/src/ServiceControl/Notifications/Email/EmailSender.cs @@ -3,21 +3,26 @@ using System; using System.Net; using System.Net.Mail; + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; public class EmailSender(ILogger logger) { - public async Task Send(EmailNotifications settings, string subject, string body, string emailDropFolder = null) + public async Task Send(EmailNotifications settings, string subject, string body, string emailDropFolder = null, CancellationToken cancellationToken = default) { try { using (var client = CreateSmtpClient(settings, emailDropFolder)) using (var mailMessage = new MailMessage(settings.From, settings.To, subject, body)) { - await client.SendMailAsync(mailMessage); + await client.SendMailAsync(mailMessage, cancellationToken); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogWarning(e, "Failure sending email"); diff --git a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs index 671491ed07..1ff11a1b15 100644 --- a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs +++ b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Notifications.Email { using System; + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NServiceBus; @@ -51,9 +52,13 @@ public async Task Handle(SendEmailNotification message, IMessageHandlerContext c message.Body += "\n\nWARNING: Your SMTP server was temporarily unavailable. Make sure to check ServicePulse for a full list of health check notifications."; } - await emailSender.Send(notifications.Email, message.Subject, message.Body, emailDropFolder); + await emailSender.Send(notifications.Email, message.Subject, message.Body, emailDropFolder, cancellationToken); } - catch (Exception e) when (e is not OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception e) { if (message.FailureNotification) { diff --git a/src/ServiceControl/Operations/ErrorIngestion.cs b/src/ServiceControl/Operations/ErrorIngestion.cs index 52ef5491f9..834b16c517 100644 --- a/src/ServiceControl/Operations/ErrorIngestion.cs +++ b/src/ServiceControl/Operations/ErrorIngestion.cs @@ -71,19 +71,19 @@ public ErrorIngestion( ); } - public override async Task StartAsync(CancellationToken cancellationToken) + public override async Task StartAsync(CancellationToken cancellationToken = default) { await watchdog.Start(() => applicationLifetime.StopApplication(), cancellationToken); await base.StartAsync(cancellationToken); } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken = default) { try { var contexts = new List(transportSettings.MaxConcurrency.Value); - while (await channel.Reader.WaitToReadAsync(stoppingToken)) + while (await channel.Reader.WaitToReadAsync(cancellationToken)) { // will only enter here if there is something to read. try @@ -97,10 +97,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) batchSizeMeter.Mark(contexts.Count); using (batchDurationMeter.Measure()) { - await ingestor.Ingest(contexts, stoppingToken); + await ingestor.Ingest(contexts, cancellationToken); } } - catch (Exception e) + catch (OperationCanceledException e) when (cancellationToken.IsCancellationRequested) { // signal all message handling tasks to terminate foreach (var context in contexts) @@ -108,10 +108,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _ = context.GetTaskCompletionSource().TrySetException(e); } - if (e is OperationCanceledException && stoppingToken.IsCancellationRequested) + logger.LogInformation(e, "Batch cancelled"); + break; + } + catch (Exception e) + { + // signal all message handling tasks to terminate + foreach (var context in contexts) { - logger.LogInformation(e, "Batch cancelled"); - break; + _ = context.GetTaskCompletionSource().TrySetException(e); } logger.LogInformation(e, "Ingesting messages failed"); @@ -123,13 +128,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } // will fall out here when writer is completed } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // ExecuteAsync cancelled } } - public override async Task StopAsync(CancellationToken cancellationToken) + public override async Task StopAsync(CancellationToken cancellationToken = default) { try { @@ -153,7 +158,7 @@ public override async Task StopAsync(CancellationToken cancellationToken) } } - async Task EnsureStarted(CancellationToken cancellationToken = default) + async Task EnsureStarted(CancellationToken cancellationToken) { try { @@ -172,12 +177,20 @@ async Task EnsureStarted(CancellationToken cancellationToken = default) await StopAndTeardownInfrastructure(cancellationToken); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { try { await StopAndTeardownInfrastructure(cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception teardownException) { throw new AggregateException(e, teardownException); @@ -222,6 +235,10 @@ async Task SetUpAndStartInfrastructure(CancellationToken cancellationToken) logger.LogInformation(LogMessages.StartedInfrastructure); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogError(e, "Failed to start infrastructure"); @@ -255,6 +272,10 @@ async Task StopAndTeardownInfrastructure(CancellationToken cancellationToken) logger.LogInformation(LogMessages.StoppedInfrastructure); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogError(e, "Failed to stop infrastructure"); @@ -289,7 +310,7 @@ Task OnCriticalError(string failure, Exception exception) return watchdog.OnFailure(failure); } - async Task EnsureStopped(CancellationToken cancellationToken = default) + async Task EnsureStopped(CancellationToken cancellationToken) { try { diff --git a/src/ServiceControl/Operations/ErrorIngestor.cs b/src/ServiceControl/Operations/ErrorIngestor.cs index 2f90579005..9b53d7a0b1 100644 --- a/src/ServiceControl/Operations/ErrorIngestor.cs +++ b/src/ServiceControl/Operations/ErrorIngestor.cs @@ -51,7 +51,7 @@ public ErrorIngestor(Metrics metrics, logQueueAddress = new UnicastAddressTag(transportCustomization.ToTransportQualifiedQueueName(this.settings.ErrorLogQueue)); } - public async Task Ingest(List contexts, CancellationToken cancellationToken) + public async Task Ingest(List contexts, CancellationToken cancellationToken = default) { var failedMessages = new List(contexts.Count); var retriedMessages = new List(contexts.Count); @@ -76,11 +76,11 @@ public async Task Ingest(List contexts, CancellationToken cancel var announcerTasks = new List(contexts.Count); foreach (var context in storedFailed) { - announcerTasks.Add(errorProcessor.Announce(context)); + announcerTasks.Add(errorProcessor.Announce(context, cancellationToken)); } foreach (var context in retriedMessages) { - announcerTasks.Add(retryConfirmationProcessor.Announce(context)); + announcerTasks.Add(retryConfirmationProcessor.Announce(context, cancellationToken)); } await Task.WhenAll(announcerTasks); @@ -99,6 +99,10 @@ public async Task Ingest(List contexts, CancellationToken cancel context.GetTaskCompletionSource().TrySetResult(true); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { logger.LogWarning(e, "Forwarding messages failed"); @@ -117,8 +121,8 @@ async Task> PersistFailedMessages(List> PersistFailedMessages(List messageContexts, CancellationTo : Task.CompletedTask; } - public async Task VerifyCanReachForwardingAddress(CancellationToken cancellationToken) + public async Task VerifyCanReachForwardingAddress(CancellationToken cancellationToken = default) { try { @@ -182,6 +190,10 @@ public async Task VerifyCanReachForwardingAddress(CancellationToken cancellation await messageDispatcher.Value.Dispatch(transportOperations, new TransportTransaction(), cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception e) { throw new Exception($"Unable to write to forwarding queue {settings.ErrorLogQueue}", e); diff --git a/src/ServiceControl/Operations/ErrorProcessor.cs b/src/ServiceControl/Operations/ErrorProcessor.cs index a539042eba..93550d10a2 100644 --- a/src/ServiceControl/Operations/ErrorProcessor.cs +++ b/src/ServiceControl/Operations/ErrorProcessor.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using Contracts.Operations; @@ -31,13 +32,13 @@ public ErrorProcessor( failedMessageFactory = new FailedMessageFactory(failedMessageEnrichers); } - public async Task> Process(IReadOnlyList contexts, IIngestionUnitOfWork unitOfWork) + public async Task> Process(IReadOnlyList contexts, IIngestionUnitOfWork unitOfWork, CancellationToken cancellationToken = default) { var storedContexts = new List(contexts.Count); var tasks = new List(contexts.Count); foreach (var context in contexts) { - tasks.Add(ProcessMessage(context, unitOfWork)); + tasks.Add(ProcessMessage(context, unitOfWork, cancellationToken)); } await Task.WhenAll(tasks); @@ -71,7 +72,7 @@ public async Task> Process(IReadOnlyList(); var headers = messageContext.Headers; @@ -86,7 +87,7 @@ public Task Announce(MessageContext messageContext) EndpointId = failingEndpointId, FailedMessageId = failedMessageId, RepeatedFailure = true - }); + }, cancellationToken); } return domainEvents.Raise(new MessageFailed @@ -94,10 +95,10 @@ public Task Announce(MessageContext messageContext) FailureDetails = failureDetails, EndpointId = failingEndpointId, FailedMessageId = headers.UniqueId() - }); + }, cancellationToken); } - async Task ProcessMessage(MessageContext context, IIngestionUnitOfWork unitOfWork) + async Task ProcessMessage(MessageContext context, IIngestionUnitOfWork unitOfWork, CancellationToken cancellationToken) { bool isOriginalMessageId = true; if (!context.Headers.TryGetValue(Headers.MessageId, out var messageId)) diff --git a/src/ServiceControl/Operations/RetryConfirmationProcessor.cs b/src/ServiceControl/Operations/RetryConfirmationProcessor.cs index 43243faff8..33a43180c1 100644 --- a/src/ServiceControl/Operations/RetryConfirmationProcessor.cs +++ b/src/ServiceControl/Operations/RetryConfirmationProcessor.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Operations { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using Infrastructure.DomainEvents; @@ -17,7 +18,7 @@ public RetryConfirmationProcessor(IDomainEvents domainEvents) this.domainEvents = domainEvents; } - public async Task Process(List contexts, IIngestionUnitOfWork unitOfWork) + public async Task Process(List contexts, IIngestionUnitOfWork unitOfWork, CancellationToken cancellationToken = default) { foreach (var context in contexts) { @@ -26,12 +27,12 @@ public async Task Process(List contexts, IIngestionUnitOfWork un } } - public Task Announce(MessageContext messageContext) + public Task Announce(MessageContext messageContext, CancellationToken cancellationToken = default) { return domainEvents.Raise(new MessageFailureResolvedByRetry { FailedMessageId = messageContext.Headers[RetryUniqueMessageIdHeader], - }); + }, cancellationToken); } readonly IDomainEvents domainEvents; diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs index ec152c57bf..f0b5b957c2 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Recoverability.API { using System; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -20,7 +21,7 @@ public class FailureGroupsArchiveController( [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsArchive)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/archive")] [HttpPost] - public async Task ArchiveGroupErrors(string groupId) + public async Task ArchiveGroupErrors(string groupId, CancellationToken cancellationToken = default) { if (!archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) { @@ -28,11 +29,11 @@ public async Task ArchiveGroupErrors(string groupId) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Archive, Permissions.ErrorRecoverabilityGroupsArchive, MessageActionScope.Group, - resource: groupId, count: null, operationId: operationId, async () => + resource: groupId, count: null, operationId: operationId, async ct => { await archiver.StartArchiving(groupId, ArchiveType.FailureGroup); - await bus.Send(m => { m.GroupId = groupId; }, AuditHeaders.LocalSendOptions(user, operationId)); - }); + await bus.Send(m => { m.GroupId = groupId; }, AuditHeaders.LocalSendOptions(user, operationId), ct); + }, cancellationToken); } return Accepted(); diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs index 6da6addce4..2a33edc330 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -38,7 +39,7 @@ public string[] GetSupportedClassifiers() [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/comment")] [HttpPost] - public async Task EditComment(string groupId, string comment) + public async Task EditComment(string groupId, string comment, CancellationToken cancellationToken = default) { await store.EditComment(groupId, comment); @@ -48,7 +49,7 @@ public async Task EditComment(string groupId, string comment) [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/comment")] [HttpDelete] - public async Task DeleteComment(string groupId) + public async Task DeleteComment(string groupId, CancellationToken cancellationToken = default) { await store.DeleteComment(groupId); @@ -58,14 +59,14 @@ public async Task DeleteComment(string groupId) [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{classifier?}")] [HttpGet] - public async Task GetAllGroups(string classifier = "Exception Type and Stack Trace", string classifierFilter = default) + public async Task GetAllGroups(string classifier = "Exception Type and Stack Trace", string classifierFilter = default, CancellationToken cancellationToken = default) { if (classifierFilter == "undefined") { classifierFilter = null; } - var results = await fetcher.GetGroups(classifier, classifierFilter); + var results = await fetcher.GetGroups(classifier, classifierFilter, cancellationToken); Response.WithDeterministicEtag(EtagHelper.CalculateEtag(results)); return results; } @@ -73,7 +74,7 @@ public async Task GetAllGroups(string classifier = "Exception [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors")] [HttpGet] - public async Task> GetGroupErrors(string groupId, [FromQuery] SortInfo sortInfo, [FromQuery] PagingInfo pagingInfo, string status = default, string modified = default) + public async Task> GetGroupErrors(string groupId, [FromQuery] SortInfo sortInfo, [FromQuery] PagingInfo pagingInfo, string status = default, string modified = default, CancellationToken cancellationToken = default) { var results = await store.GetGroupErrors(groupId, status, modified, sortInfo, pagingInfo); @@ -85,7 +86,7 @@ public async Task> GetGroupErrors(string groupId, [From [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors")] [HttpHead] - public async Task GetGroupErrorsCount(string groupId, string status = default, string modified = default) + public async Task GetGroupErrorsCount(string groupId, string status = default, string modified = default, CancellationToken cancellationToken = default) { var results = await store.GetGroupErrorsCount(groupId, status, modified); @@ -95,7 +96,7 @@ public async Task GetGroupErrorsCount(string groupId, string status = default, s [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/history")] [HttpGet] - public async Task GetRetryHistory() + public async Task GetRetryHistory(CancellationToken cancellationToken = default) { var retryHistory = await retryStore.GetRetryHistory(); @@ -107,7 +108,7 @@ public async Task GetRetryHistory() [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/id/{groupId:required:minlength(1)}")] [HttpGet] - public async Task> GetGroup(string groupId, string status = default, string modified = default) + public async Task> GetGroup(string groupId, string status = default, string modified = default, CancellationToken cancellationToken = default) { var result = await store.GetUnresolvedGroup(groupId, status, modified); diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs index 423237a31e..c2efea71bb 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Recoverability.API { using System; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -20,7 +21,7 @@ public class FailureGroupsRetryController( [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsRetry)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/retry")] [HttpPost] - public async Task ArchiveGroupErrors(string groupId) + public async Task ArchiveGroupErrors(string groupId, CancellationToken cancellationToken = default) { var started = DateTime.UtcNow; @@ -30,15 +31,15 @@ public async Task ArchiveGroupErrors(string groupId) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Retry, Permissions.ErrorRecoverabilityGroupsRetry, MessageActionScope.Group, - resource: groupId, count: null, operationId: operationId, async () => + resource: groupId, count: null, operationId: operationId, async ct => { - await retryingManager.Wait(groupId, RetryType.FailureGroup, started); + await retryingManager.Wait(groupId, RetryType.FailureGroup, started, cancellationToken: ct); await bus.Send(new RetryAllInGroup { GroupId = groupId, Started = started - }, AuditHeaders.LocalSendOptions(user, operationId)); - }); + }, AuditHeaders.LocalSendOptions(user, operationId), ct); + }, cancellationToken); } return Accepted(); diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs index 3671a35bc2..abf059fc6c 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Recoverability.API { using System; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -20,7 +21,7 @@ public class FailureGroupsUnarchiveController( [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsUnarchive)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/unarchive")] [HttpPost] - public async Task UnarchiveGroupErrors(string groupId) + public async Task UnarchiveGroupErrors(string groupId, CancellationToken cancellationToken = default) { if (!archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) { @@ -28,11 +29,11 @@ public async Task UnarchiveGroupErrors(string groupId) var operationId = this.AuditOperationId(); await auditLog.AuditedOperation(user, MessageActionKind.Unarchive, Permissions.ErrorRecoverabilityGroupsUnarchive, MessageActionScope.Group, - resource: groupId, count: null, operationId: operationId, async () => + resource: groupId, count: null, operationId: operationId, async ct => { await archiver.StartUnarchiving(groupId, ArchiveType.FailureGroup); - await bus.Send(m => { m.GroupId = groupId; }, AuditHeaders.LocalSendOptions(user, operationId)); - }); + await bus.Send(m => { m.GroupId = groupId; }, AuditHeaders.LocalSendOptions(user, operationId), ct); + }, cancellationToken); } return Accepted(); diff --git a/src/ServiceControl/Recoverability/API/GroupFetcher.cs b/src/ServiceControl/Recoverability/API/GroupFetcher.cs index 2261181ea4..d97b930fe7 100644 --- a/src/ServiceControl/Recoverability/API/GroupFetcher.cs +++ b/src/ServiceControl/Recoverability/API/GroupFetcher.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using ServiceControl.Persistence; using ServiceControl.Persistence.Recoverability; @@ -17,7 +18,7 @@ public GroupFetcher(IGroupsDataStore store, IRetryHistoryDataStore retryStore, I this.archiver = archiver; } - public async Task GetGroups(string classifier, string classifierFilter) + public async Task GetGroups(string classifier, string classifierFilter, CancellationToken cancellationToken = default) { var dbGroups = await store.GetUnresolvedGroupsByClassifier(classifier, classifierFilter); var retryHistory = await retryStore.GetRetryHistory(); diff --git a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs index 9be6bec2b2..7d60ef4254 100644 --- a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Recoverability.API { + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Microsoft.AspNetCore.Authorization; @@ -14,7 +15,7 @@ public class UnacknowledgedGroupsController(IRetryHistoryDataStore retryStore, I [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/unacknowledgedgroups/{groupId:required:minlength(1)}")] [HttpDelete] - public async Task AcknowledgeOperation(string groupId) + public async Task AcknowledgeOperation(string groupId, CancellationToken cancellationToken = default) { if (archiver.IsArchiveInProgressFor(groupId)) { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs index de0bc0eb8c..51726625f3 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageArchivedPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -16,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageArchived @ }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs index 51d3eb10ac..0afd8d73b2 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupArchivedPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using ExternalIntegrations; using Recoverability; @@ -16,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageGroupBatch }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs index 1a94a8ef53..5c434e2c1b 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessageGroupBatchUnarchivedPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using ExternalIntegrations; using Recoverability; @@ -16,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessageGroupBatch }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesUnArchived() { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs index e2520d64b2..381ab4e1df 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/FailedMessagesUnarchivedPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -17,7 +18,7 @@ protected override DispatchContext CreateDispatchRequest(FailedMessagesUnArchive }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.FailedMessagesUnArchived { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageEditedAndRetriedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageEditedAndRetriedPublisher.cs index a737606e10..54edc54261 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageEditedAndRetriedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageEditedAndRetriedPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -15,7 +16,7 @@ protected override DispatchContext CreateDispatchRequest(MessageEditedAndRetried FailedMessageId = @event.FailedMessageId, }; - protected override Task> PublishEvents(IEnumerable contexts) => + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) => Task.FromResult(contexts.Select(x => (object)new Contracts.MessageEditedAndRetried { FailedMessageId = x.FailedMessageId, diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs index 81d265bafe..9076b8e668 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailedPublisher.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -25,7 +26,7 @@ protected override DispatchContext CreateDispatchRequest(MessageFailed @event) }; } - protected override async Task> PublishEvents(IEnumerable contexts) + protected override async Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { var ids = contexts.Select(x => x.FailedMessageId).ToArray(); var results = await dataStore.GetFailedMessagesByIds(ids); diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs index 85dd3b26a2..2337cfdffe 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedByRetryPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -16,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(MessageFailureResolvedB }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.MessageFailureResolvedByRetry { diff --git a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs index 3f014c37c6..0cd9d710ff 100644 --- a/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs +++ b/src/ServiceControl/Recoverability/ExternalIntegration/MessageFailureResolvedManuallyPublisher.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability.ExternalIntegration { using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts.MessageFailures; using ExternalIntegrations; @@ -16,7 +17,7 @@ protected override DispatchContext CreateDispatchRequest(MessageFailureResolvedM }; } - protected override Task> PublishEvents(IEnumerable contexts) + protected override Task> PublishEvents(IEnumerable contexts, CancellationToken cancellationToken = default) { return Task.FromResult(contexts.Select(r => (object)new Contracts.MessageFailureResolvedManually { diff --git a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs index b4154836c1..3e59f938ec 100644 --- a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs +++ b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs @@ -142,21 +142,21 @@ public BulkRetryBatchCreationHostedService(RetriesGateway retries, IAsyncTimer s this.logger = logger; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { if (retries != null) { - timer = scheduler.Schedule(_ => ProcessRequestedBulkRetryOperations(), interval, interval, e => logger.LogError(e, "Unhandled exception while processing bulk retry operations")); + timer = scheduler.Schedule(ProcessRequestedBulkRetryOperations, interval, interval, e => logger.LogError(e, "Unhandled exception while processing bulk retry operations")); } return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) => timer?.Stop(cancellationToken) ?? Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken = default) => timer?.Stop(cancellationToken) ?? Task.CompletedTask; - async Task ProcessRequestedBulkRetryOperations() + async Task ProcessRequestedBulkRetryOperations(CancellationToken cancellationToken) { - var processedRequests = await retries.ProcessNextBulkRetry(); + var processedRequests = await retries.ProcessNextBulkRetry(cancellationToken); return processedRequests ? TimerJobExecutionResult.ExecuteImmediately : TimerJobExecutionResult.ScheduleNextExecution; } @@ -174,12 +174,12 @@ public RebuildRetryGroupStatusesHostedService(RetryDocumentManager retryDocument this.retryDocumentManager = retryDocumentManager; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { - return retryDocumentManager.RebuildRetryOperationState(); + return retryDocumentManager.RebuildRetryOperationState(cancellationToken); } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken = default) { return Task.CompletedTask; } @@ -196,24 +196,24 @@ public AdoptOrphanBatchesFromPreviousSessionHostedService(RetryDocumentManager r this.logger = logger; } - internal async Task AdoptOrphanedBatchesAsync() + internal async Task AdoptOrphanedBatchesAsync(CancellationToken cancellationToken = default) { - var moreWorkRemaining = await retryDocumentManager.AdoptOrphanedBatches(); + var moreWorkRemaining = await retryDocumentManager.AdoptOrphanedBatches(cancellationToken); return moreWorkRemaining; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { - timer = scheduler.Schedule(async _ => + timer = scheduler.Schedule(async token => { - var hasMoreWork = await AdoptOrphanedBatchesAsync(); + var hasMoreWork = await AdoptOrphanedBatchesAsync(token); return hasMoreWork ? TimerJobExecutionResult.ScheduleNextExecution : TimerJobExecutionResult.DoNotContinueExecuting; }, TimeSpan.Zero, TimeSpan.FromMinutes(2), e => logger.LogError(e, "Unhandled exception while trying to adopt orphaned batches")); return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) => timer.Stop(cancellationToken); + public Task StopAsync(CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); TimerJob timer; readonly IAsyncTimer scheduler; @@ -235,13 +235,13 @@ public ProcessRetryBatchesHostedService( this.logger = logger; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken = default) { timer = scheduler.Schedule(Process, TimeSpan.Zero, settings.ProcessRetryBatchesFrequency, e => logger.LogError(e, "Unhandled exception while processing retry batches")); return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) => timer.Stop(cancellationToken); + public Task StopAsync(CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); async Task Process(CancellationToken cancellationToken) { diff --git a/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs b/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs index 386396da15..c9887cf8c3 100644 --- a/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Recoverability { + using System.Threading; using System.Threading.Tasks; using Connection; using ServiceBus.Management.Infrastructure.Settings; @@ -10,7 +11,7 @@ class RecoverabilityPlatformConnectionDetailsProvider : IProvidePlatformConnecti public RecoverabilityPlatformConnectionDetailsProvider(Settings settings) => this.settings = settings; - public Task ProvideConnectionDetails(PlatformConnectionDetails connection) + public Task ProvideConnectionDetails(PlatformConnectionDetails connection, CancellationToken cancellationToken = default) { connection.Add("ErrorQueue", settings.ErrorQueue); return Task.CompletedTask; diff --git a/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs b/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs index 46abb2ec80..4945aedeaa 100644 --- a/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs +++ b/src/ServiceControl/Recoverability/Retrying/FailedMessageRetryCleaner.cs @@ -15,7 +15,7 @@ public FailedMessageRetryCleaner(IFailedMessageRetryDataStore dataStore) this.dataStore = dataStore; } - public Task Handle(MessageFailed message, CancellationToken cancellationToken) + public Task Handle(MessageFailed message, CancellationToken cancellationToken = default) { if (message.RepeatedFailure) { diff --git a/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs b/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs index d46f577c99..5b644b5e6d 100644 --- a/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/Handlers/RetriesHandler.cs @@ -47,13 +47,13 @@ public Task Handle(RequestRetryAll message, IMessageHandlerContext context) public Task Handle(RetryMessage message, IMessageHandlerContext context) { var (user, operationId) = AuditHeaders.Read(context.MessageHeaders); - return retries.StartRetryForSingleMessage(message.FailedMessageId, user, operationId); + return retries.StartRetryForSingleMessage(message.FailedMessageId, user, operationId, context.CancellationToken); } public Task Handle(RetryMessagesById message, IMessageHandlerContext context) { var (user, operationId) = AuditHeaders.Read(context.MessageHeaders); - return retries.StartRetryForMessageSelection(message.MessageUniqueIds, user, operationId); + return retries.StartRetryForMessageSelection(message.MessageUniqueIds, user, operationId, context.CancellationToken); } public Task Handle(RetryMessagesByQueueAddress message, IMessageHandlerContext context) diff --git a/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs b/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs index 9a4e8e7761..ed7b3291b3 100644 --- a/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/Handlers/RetryAllInGroupHandler.cs @@ -36,7 +36,7 @@ public async Task Handle(RetryAllInGroup message, IMessageHandlerContext context } var started = message.Started ?? DateTime.UtcNow; - await retryingManager.Wait(message.GroupId, RetryType.FailureGroup, started, originator, group?.Type, group?.Last); + await retryingManager.Wait(message.GroupId, RetryType.FailureGroup, started, originator, group?.Type, group?.Last, context.CancellationToken); var (user, operationId) = AuditHeaders.Read(context.MessageHeaders); diff --git a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs index 707fc1d340..2a7db48314 100644 --- a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs @@ -14,7 +14,7 @@ public StoreHistoryHandler(IRetryHistoryDataStore store, Settings settings) this.settings = settings; } - public Task Handle(RetryOperationCompleted message, CancellationToken cancellationToken) + public Task Handle(RetryOperationCompleted message, CancellationToken cancellationToken = default) { return store.RecordRetryOperationCompleted( message.RequestId, diff --git a/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs b/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs index 81364cd320..9e3160c9e8 100644 --- a/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs +++ b/src/ServiceControl/Recoverability/Retrying/InMemoryRetry.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Recoverability { using System; + using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; using Microsoft.Extensions.Logging; @@ -35,7 +36,7 @@ public static string MakeOperationId(string requestId, RetryType retryType) return $"{retryType}/{requestId}"; } - public Task Wait(DateTime started, string originator = null, string classifier = null, DateTime? last = null) + public Task Wait(DateTime started, string originator = null, string classifier = null, DateTime? last = null, CancellationToken cancellationToken = default) { RetryState = RetryState.Waiting; NumberOfMessagesPrepared = 0; @@ -55,7 +56,7 @@ public Task Wait(DateTime started, string originator = null, string classifier = RetryType = retryType, Progress = GetProgress(), StartTime = Started - }); + }, cancellationToken); } public void Fail() @@ -63,7 +64,7 @@ public void Fail() Failed = true; } - public Task Prepare(int totalNumberOfMessages) + public Task Prepare(int totalNumberOfMessages, CancellationToken cancellationToken = default) { RetryState = RetryState.Preparing; TotalNumberOfMessages = totalNumberOfMessages; @@ -78,10 +79,10 @@ public Task Prepare(int totalNumberOfMessages) Progress = GetProgress(), IsFailed = Failed, StartTime = Started - }); + }, cancellationToken); } - public Task PrepareBatch(int numberOfMessagesPrepared) + public Task PrepareBatch(int numberOfMessagesPrepared, CancellationToken cancellationToken = default) { NumberOfMessagesPrepared = numberOfMessagesPrepared; @@ -93,20 +94,20 @@ public Task PrepareBatch(int numberOfMessagesPrepared) Progress = GetProgress(), IsFailed = Failed, StartTime = Started - }); + }, cancellationToken); } - public Task PrepareAdoptedBatch(int numberOfMessagesPrepared, string originator, string classifier, DateTime startTime, DateTime last) + public Task PrepareAdoptedBatch(int numberOfMessagesPrepared, string originator, string classifier, DateTime startTime, DateTime last, CancellationToken cancellationToken = default) { Originator = originator; Started = startTime; Last = last; Classifier = classifier; - return PrepareBatch(numberOfMessagesPrepared); + return PrepareBatch(numberOfMessagesPrepared, cancellationToken); } - public Task Forwarding() + public Task Forwarding(CancellationToken cancellationToken = default) { RetryState = RetryState.Forwarding; @@ -118,10 +119,10 @@ public Task Forwarding() Progress = GetProgress(), IsFailed = Failed, StartTime = Started - }); + }, cancellationToken); } - public async Task BatchForwarded(int numberOfMessagesForwarded) + public async Task BatchForwarded(int numberOfMessagesForwarded, CancellationToken cancellationToken = default) { NumberOfMessagesForwarded += numberOfMessagesForwarded; @@ -133,18 +134,18 @@ await domainEvents.Raise(new RetryMessagesForwarded Progress = GetProgress(), IsFailed = Failed, StartTime = Started - }); + }, cancellationToken); - await CheckForCompletion(); + await CheckForCompletion(cancellationToken); } - public Task Skip(int numberOfMessagesSkipped) + public Task Skip(int numberOfMessagesSkipped, CancellationToken cancellationToken = default) { NumberOfMessagesSkipped += numberOfMessagesSkipped; - return CheckForCompletion(); + return CheckForCompletion(cancellationToken); } - async Task CheckForCompletion() + async Task CheckForCompletion(CancellationToken cancellationToken) { if (NumberOfMessagesForwarded + NumberOfMessagesSkipped != TotalNumberOfMessages) { @@ -166,7 +167,7 @@ await domainEvents.Raise(new RetryOperationCompleted NumberOfMessagesProcessed = NumberOfMessagesForwarded, Last = Last ?? DateTime.MaxValue, Classifier = Classifier - }); + }, cancellationToken); if (retryType == RetryType.FailureGroup) { @@ -175,7 +176,7 @@ await domainEvents.Raise(new MessagesSubmittedForRetry FailedMessageIds = new string[0], NumberOfFailedMessages = NumberOfMessagesForwarded, Context = Originator - }); + }, cancellationToken); } logger.LogInformation("Retry operation {RequestId} completed. {NumberOfMessagesSkipped} messages skipped, {NumberOfMessagesForwarded} forwarded. Total {TotalNumberOfMessages}", diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs index 1c87eed688..50c4c40d0f 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSender.cs @@ -25,7 +25,7 @@ public virtual async Task HandleMessage(MessageContext message, IMessageDispatch if (outgoingHeaders.TryGetValue("ServiceControl.Retry.Attempt.MessageId", out var attemptMessageId)) { - body = await FetchFromFailedMessage(outgoingHeaders, messageId, attemptMessageId); + body = await FetchFromFailedMessage(outgoingHeaders, messageId, attemptMessageId, cancellationToken); outgoingHeaders.Remove("ServiceControl.Retry.Attempt.MessageId"); } else @@ -56,7 +56,7 @@ 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) + async Task FetchFromFailedMessage(Dictionary outgoingHeaders, string messageId, string attemptMessageId, CancellationToken cancellationToken) { var uniqueMessageId = outgoingHeaders["ServiceControl.Retry.UniqueMessageId"]; byte[] body = await errorMessageStore.GetFailedMessageBody(uniqueMessageId); diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs index 82b59e0b36..1ad988bb9b 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs @@ -33,12 +33,12 @@ ILogger logger this.errorQueueNameCache = errorQueueNameCache; this.logger = logger; faultManager = new CaptureIfMessageSendingFails(dataStore, domainEvents, IncrementCounterOrProlongTimer, logger); - timer = new Timer(state => StopInternal().GetAwaiter().GetResult()); + timer = new Timer(state => StopInternal(CancellationToken.None).GetAwaiter().GetResult()); } public string InputAddress { get; } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken = default) { transportInfrastructure = await transportCustomization.CreateTransportInfrastructure(InputAddress, transportSettings, Handle, faultManager.OnError, (_, __) => Task.CompletedTask, TransportTransactionMode.SendsAtomicWithReceive); messageReceiver = transportInfrastructure.Receivers[InputAddress]; @@ -48,11 +48,11 @@ public async Task StartAsync(CancellationToken cancellationToken) errorQueueNameCache.ResolvedErrorAddress = errorQueueTransportAddress; } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { timer.Dispose(); endedPrematurely = true; - await StopInternal(); + await StopInternal(cancellationToken); await transportInfrastructure.Shutdown(cancellationToken); } @@ -99,7 +99,7 @@ void CountMessageAndStopIfReachedTarget() logger.LogDebug("Target count reached. Shutting down forwarder"); // NOTE: This needs to run on a different thread or a deadlock will happen trying to shut down the receiver - _ = Task.Run(StopInternal); + _ = Task.Run(() => StopInternal(CancellationToken.None), CancellationToken.None); } } @@ -148,7 +148,7 @@ public virtual async Task Run(string forwardingBatchId, Predicate store.MoveBatchToStaging(batchId); + protected virtual Task MoveBatchToStaging(string batchId, CancellationToken cancellationToken = default) => store.MoveBatchToStaging(batchId); - public async Task ProcessNextBulkRetry() // Invoked from BulkRetryBatchCreationHostedService in schedule + public async Task ProcessNextBulkRetry(CancellationToken cancellationToken = default) // Invoked from BulkRetryBatchCreationHostedService in schedule { if (!bulkRequests.TryDequeue(out var request)) { return false; } - await ProcessRequest(request); + await ProcessRequest(request, cancellationToken); return true; } - async Task ProcessRequest(BulkRetryRequest request) + async Task ProcessRequest(BulkRetryRequest request, CancellationToken cancellationToken) { - var (batches, latestAttempt) = await request.GetRequestedBatches(store); + var (batches, latestAttempt) = await request.GetRequestedBatches(store, cancellationToken); var totalMessages = batches.Sum(b => b.Length); if (!operationManager.IsOperationInProgressFor(request.RequestId, request.RetryType) && totalMessages > 0) { var numberOfMessagesAdded = 0; - await operationManager.Preparing(request.RequestId, request.RetryType, totalMessages); + await operationManager.Preparing(request.RequestId, request.RetryType, totalMessages, cancellationToken); for (var i = 0; i < batches.Count; i++) { - await AssignMessagesToBatch(request.RequestId, request.RetryType, batches[i], request.StartTime, latestAttempt, request.Originator, GetBatchName(i + 1, batches.Count, request.Originator), request.Classifier, request.InitiatedBy, request.OperationId); + await AssignMessagesToBatch(request.RequestId, request.RetryType, batches[i], request.StartTime, latestAttempt, request.Originator, GetBatchName(i + 1, batches.Count, request.Originator), request.Classifier, request.InitiatedBy, request.OperationId, cancellationToken); numberOfMessagesAdded += batches[i].Length; - await operationManager.PreparedBatch(request.RequestId, request.RetryType, numberOfMessagesAdded); + await operationManager.PreparedBatch(request.RequestId, request.RetryType, numberOfMessagesAdded, cancellationToken); } } } @@ -176,7 +177,7 @@ public BulkRetryRequest( protected abstract Task Invoke(IRetryBatchStore store, Func callback); - public async Task, DateTime>> GetRequestedBatches(IRetryBatchStore store) + public async Task, DateTime>> GetRequestedBatches(IRetryBatchStore store, CancellationToken cancellationToken = default) { var response = new List(); var currentBatch = new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs b/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs index 31e602ae96..0315e65297 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryDocumentManager.cs @@ -2,6 +2,7 @@ namespace ServiceControl.Recoverability { using System; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -17,7 +18,7 @@ public RetryDocumentManager(IHostApplicationLifetime applicationLifetime, IRetry this.logger = logger; } - public async Task AdoptOrphanedBatches() + public async Task AdoptOrphanedBatches(CancellationToken cancellationToken = default) { var orphanedBatches = await store.GetOrphanedBatches(RetrySessionId); @@ -27,7 +28,7 @@ public async Task AdoptOrphanedBatches() await Task.WhenAll(orphanedBatches.Results.Select(b => Task.Run(async () => { logger.LogInformation("Adopting retry batch {BatchId} with {BatchMessageCount} messages", b.Id, b.MessageCount); - await MoveBatchToStaging(b.Id); + await MoveBatchToStaging(b.Id, cancellationToken); }))); foreach (var batch in orphanedBatches.Results) @@ -46,9 +47,9 @@ await Task.WhenAll(orphanedBatches.Results.Select(b => Task.Run(async () => return orphanedBatches.QueryStats.IsStale || orphanedBatches.Results.Any(); } - public virtual Task MoveBatchToStaging(string batchId) => store.MoveBatchToStaging(batchId); + public virtual Task MoveBatchToStaging(string batchId, CancellationToken cancellationToken = default) => store.MoveBatchToStaging(batchId); - public async Task RebuildRetryOperationState() + public async Task RebuildRetryOperationState(CancellationToken cancellationToken = default) { var stagingBatchGroups = await store.GetAvailableBatchGroups(); @@ -56,7 +57,7 @@ public async Task RebuildRetryOperationState() { logger.LogDebug("Rebuilt retry operation status for {RetryType}/{RetryRequestId}. Aggregated batchsize: {RetryBatchSize}", group.RetryType, group.RequestId, group.InitialBatchSize); - await operationManager.PreparedAdoptedBatch(group.RequestId, group.RetryType, group.InitialBatchSize, group.InitialBatchSize, group.Originator, group.Classifier, group.StartTime, group.Last); + await operationManager.PreparedAdoptedBatch(group.RequestId, group.RetryType, group.InitialBatchSize, group.InitialBatchSize, group.Originator, group.Classifier, group.StartTime, group.Last, cancellationToken); } } diff --git a/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs b/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs index 8f244f539a..c8d231b6be 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryProcessor.cs @@ -38,15 +38,15 @@ public RetryProcessor( corruptedReplyToHeaderStrategy = new CorruptedReplyToHeaderStrategy(RuntimeEnvironment.MachineName, logger); } - Task Enqueue(TransportOperations outgoingMessages) + Task Enqueue(TransportOperations outgoingMessages, CancellationToken cancellationToken) { - return messageDispatcher.Value.Dispatch(outgoingMessages, new TransportTransaction()); + return messageDispatcher.Value.Dispatch(outgoingMessages, new TransportTransaction(), cancellationToken); } public async Task ProcessBatches(CancellationToken cancellationToken = default) => - await ForwardCurrentBatch(cancellationToken) || await MoveStagedBatchesToForwardingBatch(); + await ForwardCurrentBatch(cancellationToken) || await MoveStagedBatchesToForwardingBatch(cancellationToken); - async Task MoveStagedBatchesToForwardingBatch() + async Task MoveStagedBatchesToForwardingBatch(CancellationToken cancellationToken) { try { @@ -60,9 +60,9 @@ async Task MoveStagedBatchesToForwardingBatch() { logger.LogInformation("Staging batch {StagingBatchId}", stagingBatch.Id); redirects = await redirectsStore.GetRedirects(); - var stagedMessages = await Stage(stagingBatch); + var stagedMessages = await Stage(stagingBatch, cancellationToken); var skippedMessages = stagingBatch.InitialBatchSize - stagedMessages; - await retryingManager.Skip(stagingBatch.RequestId, stagingBatch.RetryType, skippedMessages); + await retryingManager.Skip(stagingBatch.RequestId, stagingBatch.RetryType, skippedMessages, cancellationToken); if (stagedMessages > 0) { @@ -120,13 +120,13 @@ async Task Forward(RetryBatch forwardingBatch, CancellationToken cancellationTok { var messageCount = forwardingBatch.MessageCount; - await retryingManager.Forwarding(forwardingBatch.RequestId, forwardingBatch.RetryType); + await retryingManager.Forwarding(forwardingBatch.RequestId, forwardingBatch.RetryType, cancellationToken); if (isRecoveringFromPrematureShutdown) { logger.LogWarning("Recovering from premature shutdown. Starting forwarder for batch {ForwardingBatchId} in timeout mode", forwardingBatch.Id); await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), null, cancellationToken); - await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, forwardingBatch.InitialBatchSize); + await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, forwardingBatch.InitialBatchSize, cancellationToken); } else { @@ -140,7 +140,7 @@ async Task Forward(RetryBatch forwardingBatch, CancellationToken cancellationTok await returnToSender.Run(forwardingBatch.Id, IsPartOfStagedBatch(forwardingBatch.StagingId), messageCount, cancellationToken); } - await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, messageCount); + await retryingManager.ForwardedBatch(forwardingBatch.RequestId, forwardingBatch.RetryType, messageCount, cancellationToken); } logger.LogInformation("Done forwarding batch {ForwardingBatchId}", forwardingBatch.Id); @@ -155,7 +155,7 @@ static Predicate IsPartOfStagedBatch(string stagingId) }; } - async Task Stage(RetryBatch stagingBatch) + async Task Stage(RetryBatch stagingBatch, CancellationToken cancellationToken) { var stagingId = Guid.NewGuid().ToString(); @@ -180,7 +180,7 @@ async Task Stage(RetryBatch stagingBatch) transportOperations[current++] = ToTransportOperation(messageToStage, stagingId); } - await TryDispatch(stagingBatch.Id, transportOperations, messagesToStage, stageAttemptsById, previousAttemptFailed); + await TryDispatch(stagingBatch.Id, transportOperations, messagesToStage, stageAttemptsById, previousAttemptFailed, cancellationToken); AuditStagedMessages(stagingBatch, messagesToStage); @@ -192,7 +192,7 @@ await domainEvents.Raise(new MessagesSubmittedForRetry FailedMessageIds = failedIds, NumberOfFailedMessages = failedIds.Length, Context = stagingBatch.Context - }); + }, cancellationToken); } await store.MarkBatchAsForwarding(stagingBatch.Id, stagingId, [.. stageAttemptsById.Keys]); @@ -235,27 +235,31 @@ void AuditStagedMessages(RetryBatch stagingBatch, IReadOnlyCollection messages, - IReadOnlyDictionary stageAttemptsById, bool previousAttemptFailed) + IReadOnlyDictionary stageAttemptsById, bool previousAttemptFailed, CancellationToken cancellationToken) { - return previousAttemptFailed ? ConcurrentDispatchToTransport(transportOperations, stageAttemptsById) : - BatchDispatchToTransport(batchId, transportOperations, messages); + return previousAttemptFailed ? ConcurrentDispatchToTransport(transportOperations, stageAttemptsById, cancellationToken) : + BatchDispatchToTransport(batchId, transportOperations, messages, cancellationToken); } - Task ConcurrentDispatchToTransport(IReadOnlyCollection transportOperations, IReadOnlyDictionary stageAttemptsById) + Task ConcurrentDispatchToTransport(IReadOnlyCollection transportOperations, IReadOnlyDictionary stageAttemptsById, CancellationToken cancellationToken) { var tasks = new List(transportOperations.Count); foreach (var transportOperation in transportOperations) { - tasks.Add(TryStageMessage(transportOperation, stageAttemptsById)); + tasks.Add(TryStageMessage(transportOperation, stageAttemptsById, cancellationToken)); } return Task.WhenAll(tasks); } - async Task BatchDispatchToTransport(string batchId, TransportOperation[] transportOperations, IReadOnlyCollection messages) + async Task BatchDispatchToTransport(string batchId, TransportOperation[] transportOperations, IReadOnlyCollection messages, CancellationToken cancellationToken) { try { - await Enqueue(new TransportOperations(transportOperations)); + await Enqueue(new TransportOperations(transportOperations), cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception e) { @@ -267,13 +271,17 @@ async Task BatchDispatchToTransport(string batchId, TransportOperation[] transpo } } - async Task TryStageMessage(TransportOperation transportOperation, IReadOnlyDictionary stageAttemptsById) + async Task TryStageMessage(TransportOperation transportOperation, IReadOnlyDictionary stageAttemptsById, CancellationToken cancellationToken) { var uniqueMessageId = transportOperation.Message.Headers["ServiceControl.Retry.UniqueMessageId"]; try { - await Enqueue(new TransportOperations(transportOperation)); + await Enqueue(new TransportOperations(transportOperation), cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception e) { @@ -294,7 +302,7 @@ async Task TryStageMessage(TransportOperation transportOperation, IReadOnlyDicti await domainEvents.Raise(new MessageFailedInStaging { UniqueMessageId = uniqueMessageId - }); + }, cancellationToken); } throw new RetryStagingException(e); diff --git a/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs b/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs index 83917ff965..3b9c91ac18 100644 --- a/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs +++ b/src/ServiceControl/Recoverability/Retrying/RetryingManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; using Microsoft.Extensions.Logging; @@ -16,11 +17,11 @@ public RetryingManager(IDomainEvents domainEvents, ILogger logg this.logger = logger; } - public Task Wait(string requestId, RetryType retryType, DateTime started, string originator = null, string classifier = null, DateTime? last = null) + public Task Wait(string requestId, RetryType retryType, DateTime started, string originator = null, string classifier = null, DateTime? last = null, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - return summary.Wait(started, originator, classifier, last); + return summary.Wait(started, originator, classifier, last, cancellationToken); } public bool IsOperationInProgressFor(string requestId, RetryType retryType) @@ -38,40 +39,40 @@ public bool IsRetryInProgressFor(string requestId) return retryOperations.Values.Any(o => o.RequestId == requestId && o.IsInProgress()); } - public async Task Preparing(string requestId, RetryType retryType, int totalNumberOfMessages) + public async Task Preparing(string requestId, RetryType retryType, int totalNumberOfMessages, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.Prepare(totalNumberOfMessages); + await summary.Prepare(totalNumberOfMessages, cancellationToken); } - public async Task PreparedAdoptedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared, int totalNumberOfMessages, string originator, string classifier, DateTime startTime, DateTime last) + public async Task PreparedAdoptedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared, int totalNumberOfMessages, string originator, string classifier, DateTime startTime, DateTime last, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.Prepare(totalNumberOfMessages); - await summary.PrepareAdoptedBatch(numberOfMessagesPrepared, originator, classifier, startTime, last); + await summary.Prepare(totalNumberOfMessages, cancellationToken); + await summary.PrepareAdoptedBatch(numberOfMessagesPrepared, originator, classifier, startTime, last, cancellationToken); } - public async Task PreparedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared) + public async Task PreparedBatch(string requestId, RetryType retryType, int numberOfMessagesPrepared, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.PrepareBatch(numberOfMessagesPrepared); + await summary.PrepareBatch(numberOfMessagesPrepared, cancellationToken); } - public async Task Forwarding(string requestId, RetryType retryType) + public async Task Forwarding(string requestId, RetryType retryType, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.Forwarding(); + await summary.Forwarding(cancellationToken); } - public async Task ForwardedBatch(string requestId, RetryType retryType, int numberOfMessagesForwarded) + public async Task ForwardedBatch(string requestId, RetryType retryType, int numberOfMessagesForwarded, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.BatchForwarded(numberOfMessagesForwarded); + await summary.BatchForwarded(numberOfMessagesForwarded, cancellationToken); } public void Fail(RetryType retryType, string requestId) @@ -81,10 +82,10 @@ public void Fail(RetryType retryType, string requestId) summary.Fail(); } - public async Task Skip(string requestId, RetryType retryType, int numberOfMessagesSkipped) + public async Task Skip(string requestId, RetryType retryType, int numberOfMessagesSkipped, CancellationToken cancellationToken = default) { var summary = GetOrCreate(retryType, requestId); - await summary.Skip(numberOfMessagesSkipped); + await summary.Skip(numberOfMessagesSkipped, cancellationToken); } InMemoryRetry GetOrCreate(RetryType retryType, string requestId) diff --git a/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs b/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs index 6ddbafa10f..25f1f73096 100644 --- a/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs +++ b/src/ServiceControl/SagaAudit/SagaUpdatedHandler.cs @@ -20,7 +20,7 @@ public async Task Handle(SagaUpdatedMessage message, IMessageHandlerContext cont if (auditQueueName is null || nextAuditQueueNameRefresh < DateTime.UtcNow) { - await RefreshAuditQueue(); + await RefreshAuditQueue(context.CancellationToken); } if (auditQueueName is null) @@ -35,14 +35,14 @@ public async Task Handle(SagaUpdatedMessage message, IMessageHandlerContext cont await context.ForwardCurrentMessageTo(auditQueueName); } - async Task RefreshAuditQueue() + async Task RefreshAuditQueue(CancellationToken cancellationToken) { if (nextAuditQueueNameRefresh > DateTime.UtcNow) { return; } - await semaphore.WaitAsync(); + await semaphore.WaitAsync(cancellationToken); try { if (nextAuditQueueNameRefresh > DateTime.UtcNow) @@ -50,7 +50,7 @@ async Task RefreshAuditQueue() return; } - var connectionDetails = await connectionBuilder.BuildPlatformConnection(); + var connectionDetails = await connectionBuilder.BuildPlatformConnection(cancellationToken); // First instance is named `SagaAudit`, following instance `SagaAudit1`..`SagaAuditN` if (connectionDetails.ToDictionary().TryGetValue("SagaAudit", out var sagaAuditObj) && sagaAuditObj is JsonElement sagaAudit) @@ -61,6 +61,10 @@ async Task RefreshAuditQueue() logger.LogInformation("Refreshed audit queue name '{AuditQueueName}' from ServiceControl Audit instance. Will continue to use this value for forwarding saga update messages for the next 5 minutes", auditQueueName); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception x) { logger.LogWarning("Unable to refresh audit queue name from ServiceControl Audit instance. Will continue to check at most every 15 seconds. Exception message: {ExceptionMessage}", x.Message); diff --git a/src/ServiceControl/SagaAudit/SagasController.cs b/src/ServiceControl/SagaAudit/SagasController.cs index d3c082de8f..f0ff9f28b6 100644 --- a/src/ServiceControl/SagaAudit/SagasController.cs +++ b/src/ServiceControl/SagaAudit/SagasController.cs @@ -1,6 +1,7 @@ namespace ServiceControl.SagaAudit { using System; + using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Infrastructure.WebApi; @@ -16,10 +17,10 @@ public class SagasController(GetSagaByIdApi getSagaByIdApi) : ControllerBase [Authorize(Policy = Permissions.ErrorSagasView)] [Route("sagas/{id}")] [HttpGet] - public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id) + public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken = default) { QueryResult result = - await getSagaByIdApi.Execute(new SagaByIdContext(pagingInfo, id), Request.GetEncodedPathAndQuery()); + await getSagaByIdApi.Execute(new SagaByIdContext(pagingInfo, id), Request.GetEncodedPathAndQuery(), cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results;