Repository navigation
NCBC-4336: Complete WhenAnySuccessful when the caller's token cancels - #241
Open
davidkelly wants to merge 2 commits into
Open
davidkelly wants to merge 2 commits into
davidkelly wants to merge 2 commits into
Conversation
Motivation ========== WhenAnySuccessful, behind GetAnyReplicaAsync and LookupInAnyReplicaAsync, completed its result only from continuations registered with the caller's token. A token cancelled before every replica finished cancelled those continuations without running them, so the call never returned. Two tests that should have caught it discarded their assertions. The CancellationTokenSource the method created was never disposed (NCBC-4249), and every replica fault was reported as an unobserved task exception. Modification ============ Register the continuations with CancellationToken.None and let the token cancel the result through a registration, disposed once the result completes. Run the continuations inline and the caller's continuation asynchronously. Remove the unused CancellationTokenSource. Make the fail-safe observe the WhenAll fault and stop once the result is complete in any state. Rewrite the two masked tests and add cancellation tests. They drive the replicas with TaskCompletionSource and assert synchronously, with no delays or timeouts. Results ======= Cancelling the token completes the call as cancelled at once. Five of the new tests fail on master and pass with the fix; the unit suite passes on net8.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FIT performer imagePublished: ghcr.io/couchbase/dotnet-fit-performer:NCBC-4336Run FIT locally against this PR: fit run preset <preset-name> --performer dotnet-fit-performer:NCBC-4336Or run it from the workflow here. Note Each push to this PR replaces the image and edits this comment. The image is deleted 7 days after the last push. |
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the changes addressing cancellation hangs in WhenAnySuccessful.
No open issues 🎉
What changed in this PR
This PR fixes cancellation hangs in WhenAnySuccessful, which supports GetAnyReplicaAsync and LookupInAnyReplicaAsync.
Changes:
- Cancel the result directly through the caller’s token while allowing replica continuations to run independently.
- Dispose the cancellation registration, observe aggregate faults, and safely handle already-completed results.
- Replace ineffective exception assertions and add deterministic tests for cancellation and replica completion ordering.
| File | Description |
|---|---|
tests/Couchbase.UnitTests/Utils/TaskHelpersTests.cs |
Repairs exception assertions and covers cancellation before and after replica completion. |
src/Couchbase/Utils/TaskHelpers.cs |
Fixes cancellation handling, continuation scheduling, and fail-safe completion. |
Review effort: Balanced
davidkelly
marked this pull request as ready for review
October 30, 2026 16:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GetAnyReplicaAsyncandLookupInAnyReplicaAsyncnever returned if the caller'sCancellationTokenwas cancelled before every replica read finished.TaskHelpers.WhenAnySuccessfulcompleted its result only from continuations registered with that token, and a cancelled continuation never runs.LazyCancellationwould not fix this: it only delays the cancellation, and the continuation still never runs.Changes in
WhenAnySuccessfulCancellationToken.None. The caller's token now cancels the result through aRegistercallback, which is disposed once the result completes.ExecuteSynchronously), and create the result withRunContinuationsAsynchronouslyso the caller's own code never runs insidects.Cancel()or a replica's completion.CancellationTokenSource, which was never disposed and whose token nothing read (NCBC-4249).WhenAllfault, and return once the result is complete in any state. Before, every replica fault was reported as an unobserved task exception, and the fail-safe could throw from a discarded continuation.Tests
AllCanceledandAllFailed. They discarded their assertions under#pragma warning disable xUnit2021, so they could never fail.All of these use
TaskCompletionSourceand an explicitCancel(), and check the state immediately, with no delays or timeouts.Verification
TaskHelpersTestspassed in 25 of 25 runs on net8.0.Behaviour change: callers now always resume on the thread pool after awaiting
WhenAnySuccessful.🤖 Generated with Claude Code