Fix forEach(cancelPrevious:) leaving its in-flight body running after cancel - #87
Merged
Merged
Conversation
… cancel A child is spawned first and keyed into its parent's cancellation context afterwards (inheritCancellationContext), so the parent's context could be cancelled in between: its keyed cancel had already run and never reached the child. forEach(cancelPrevious:) hit this when subscription.cancel() landed right after a body started, so the body ran to completion. That's the intermittent testForEachCancelPreviousInheritsContext timeout (7 in 10 failed locally when run alone). One-shot contexts (anonymous cancellationContext, which wraps every node.task) now use a ContextToken key that stays cancelled. Closing it and snapshotting its registrations share one critical section with registration, so a racing registration either lands in the snapshot or sees the context closed. User keys stay reusable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reach-cancelprevious # Conflicts: # CHANGELOG.md
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.
Fixes the intermittent
InheritCancellationContextTests.testForEachCancelPreviousInheritsContextfailure (CI runs 36043894226 and 35728035828, and PR #86's first macOS serial run). The flake was a real cancellation bug.Bug
A child task is spawned first and keyed into its parent's cancellation context afterwards (
inheritCancellationContext()), so the parent's context can be cancelled in between. ItscancelAll(for: key)has then already run and never reaches the child, which keeps running with nothing left to cancel it.forEach(cancelPrevious: true), ifsubscription.cancel()lands right after a body starts, the outer loop is cancelled but the body is not. The loop then waits for the body, which runs to completion; in the test that's a 30 s sleep.task { … }.inheritCancellationContext()inside a task whose context was just cancelled.Evidence
main, the test fails 7 times in 10 when run alone locally. Failing runs time out at exactly thewaitUntilbudget, because cancellation never arrives.Fix
cancellationContext { }that wraps everynode.task) now use aContextTokenkey that stays cancelled once cancelled.cancelAll(for:)closes the token in the same critical section that snapshots its registrations. Keying into a closed context, or registering inside one, takes the same lock and cancels immediately. So a racing registration either lands in the snapshot or sees the context closed, never neither.cancellationContext(for:),cancel(for:),cancelInFlight) are reusable by design and unchanged.Task.isCancelledininheritCancellationContext()) left a residual window between the snapshot and the parent'sTask.cancel(): still 1 in 10 failing. That's why the check is linearized under the store lock instead.Tests
CancelledContextKeyingTests:inheritCancellationContext(), and the child is cancelled at once. Fails deterministically without the fix.cancelAll.Full suite: 3 local runs in parallel and 1 serial, all passing, with no compiler warnings. CHANGELOG entry added under
[Unreleased].🤖 Generated with Claude Code