Skip to content

Continue consistent application of optional cancellation tokens - #5729

Merged
johnsimons merged 1 commit into
john/cancel_part1from
john/cancel_part2
Aug 11, 2026
Merged

johnsimons merged 1 commit into
john/cancel_part1from
john/cancel_part2

Conversation

@johnsimons

Copy link
Copy Markdown
Member

Why

Third step in threading CancellationToken through the platform. The two host projects,
ServiceControl and ServiceControl.Audit, carried 260 of the tracked cancellation diagnostics
between them. This PR retires 253 of them: every controller action, hosted service, message handler
and mid-layer method now takes and forwards a token down to the persistence boundary.

Persistence interfaces themselves are unchanged and come next; tokens added here mostly terminate at
the data store call, which is the expected shape at this stage.

What this does

Applies the same convention as the previous PRs: non-private members take
CancellationToken cancellationToken = default last, private helpers take it required, named
cancellationToken throughout.

  • Controllers (62 actions) get a token bound from HttpContext.RequestAborted. IMessageSession
    sends, IDomainEvents.Raise and the scatter-gather fan-out all receive it.
  • Hosted services thread stoppingToken / the startup token. Several IAsyncTimer callbacks were
    discarding the token the timer already handed them (_ =>); they now use it, which is what Phase 1's
    AsyncTimer change was for.
  • NServiceBus handlers forward context.CancellationToken. NSB0002 enforces this automatically
    once the callee accepts one.
  • Mid-layer: the retry pipeline (RetriesGateway, RetryProcessor, RetryingManager,
    InMemoryRetry, RetryDocumentManager), the error and audit ingestion pipelines, endpoint
    monitoring, custom checks, external integrations, notifications and the platform-connection providers.

IEndpointInstanceMonitoring was pulled forward from the persistence phase: it lives in
ServiceControl.Persistence but is implemented in ServiceControl, so it had to move with its
implementation. That retires 5 of that project's diagnostics early.

Debt blocks narrowed, not deleted

Both hosts keep an .editorconfig block, but scoped to named files rather than the whole project:
ServiceControl goes from 8 rules project-wide to 3 rules on 5 files, ServiceControl.Audit from 7 to
2 rules on 4 files. Each block records what it is waiting on:

Blocked on Files
TransportCustomization.CreateTransportInfrastructure taking Func<string, Exception, Task> for onCriticalError ErrorIngestion, ErrorIngestionFaultPolicy, ImportFailureCircuitBreaker (both hosts)
IRetryBatchStore callbacks typed Func<string, DateTime, Task> RetriesGateway
IExternalIntegrationRequestsDataStore.Subscribe typed Func<object[], Task> EventDispatcherHostedService
IFailedAuditStorage.ProcessFailedMessages handing its callback a separate token that PS0020 cannot see ImportFailedAudits

One is not an interface problem: RetriesGateway.AssignMessagesToBatch is private and should take a
required token, but its trailing optional parameters make that illegal. It needs a parameter reorder,
which didn't belong in this diff.

Bug fixed along the way

ScatterGatherApi.FetchAndParse caught OperationCanceledException with the comment "Intentional,
used to gracefully handle timeout"
and returned an empty result for that remote. Once a token flows
in, a client disconnect and an HttpClient timeout arrive as the same exception type, so an abandoned
request would have been reported as "that remote timed out" and quietly excluded from the aggregate.
It now rethrows when the caller's token is cancelled and only absorbs genuine per-remote timeouts.

Notes for reviewers

  • Both ingestion loops used the older inline shape, catch (Exception e) followed by
    if (e is OperationCanceledException && token.IsCancellationRequested). These are now a filtered
    catch (OperationCanceledException) ahead of the general handler. Behaviour is preserved exactly,
    including signalling every in-flight message context before breaking out of the batch loop.
  • Six methods implement framework interfaces that declare no token (IModelBinder.BindModelAsync,
    ASP.NET middleware InvokeAsync, the NServiceBus critical-error delegate). PS0018 is unsatisfiable
    there, so they carry site-level pragmas with justifications rather than fixes, same as the ASP.NET
    auth interfaces in the previous PR.
  • The HttpApiRoutes approval test changed and this is expected. It snapshots the route table
    including parameter lists, so every action picked up , CancellationToken cancellationToken. Before
    accepting the new snapshot I checked mechanically that stripping the token parameters from the
    received file makes it byte-identical to the approved one: zero residual differences, so no route,
    no HTTP verb and no existing parameter changed.
  • Test doubles updated to match: FaultyPublisher, CustomRetriesGateway, CustomRetryDocumentManager,
    MockEndpointInstanceMonitoring, TestApi.

@johnsimons johnsimons self-assigned this Aug 11, 2026
@johnsimons
johnsimons marked this pull request as ready for review August 11, 2026 04:07
@johnsimons
johnsimons requested a review from rbev August 11, 2026 04:08
Expands the scope of `CancellationToken` parameters across the codebase, standardizing their naming and making them optional where appropriate. This further reduces cancellation analyzer debt by removing suppressions from `.editorconfig` files and adding pragmas for interface limitations.
@johnsimons
johnsimons merged commit 9056a49 into master Aug 11, 2026
146 of 147 checks passed
@johnsimons
johnsimons deleted the john/cancel_part2 branch August 11, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants