Skip to content

Deeper cancellation: flow the pipe token into each stage's job - #51

Merged
timonkrebs merged 4 commits into
mainfrom
claude/quirky-pascal-zw17zz
Jul 2, 2026
Merged

Deeper cancellation: flow the pipe token into each stage's job#51
timonkrebs merged 4 commits into
mainfrom
claude/quirky-pascal-zw17zz

Conversation

@timonkrebs

Copy link
Copy Markdown
Owner

Summary

Structured-concurrency cancellation was only observed between stages — a stage already running was awaited to completion before the next checkpoint could throw. This implements the planned Deeper cancellation feature: cancellation-aware I and Let overloads that take a Func<…, CancellationToken, Task<…>> and flow the pipe's carried token into the running job, so work already in flight can be cancelled, not just observed between stages.

StructuredTask<int> task = x.I((v, ct) => FetchAsync(v, ct))     // ct is the pipe's token
                            .I((v, ct) => ProcessAsync(v, ct));
task.CancellationTokenSource.Cancel();   // interrupts the running stage, not just between stages

Changes

  • StructuredConcurrency.cs — cancellation-aware I overloads (value / Task / StructuredTask sources) and source-arg Let overloads (value / StructuredTask / deferred sources). The StructuredTask-source paths share the carried token via a new token-aware CheckedChain; the value/Task entry points own a fresh CancellationTokenSource. A third token-aware Let stays a loud compile error (mirrors the existing token-free guard) so it can't silently drop a deferred.
  • TupleDestructuringGenerator.cs — the Roslyn generator emits the same (…, CancellationToken) shape for every tuple arity (value / Task / StructuredTask tuple sources), so (a, b, ct) => … works too.
  • StructuredConcurrencyCancellationTests.cs (new) — covers success, in-flight interruption, token sharing across stages, the up-front cancelled-source check, owned-CTS disposal, token/token-free composition, and tuple sources.
  • README.md — documents the new overloads; "Deeper cancellation" leaves Planned Features.

Design notes

  • The carried token still gates each await (via CheckedAwait), so a job that ignores the token it was handed is nonetheless abandoned at its next await once cancellation is requested.
  • The cancellation trigger is the chain's CancellationTokenSource (external .Cancel() or a token propagated from an upstream source). Sibling-fault-triggered cancellation is intentionally not wired here — Await still observes every deferred so all exceptions surface.
  • Ownership/disposal semantics are unchanged: StructuredTask-source chains share one CTS (ownership transfer); value/Task entry points own a fresh CTS disposed with the resulting StructuredTask.

Testing

All 196 tests pass in Debug and Release, zero warnings. The interruption tests block the job on Task.Delay(Timeout.Infinite, ct) — an infinite delay can only complete via the flowed-in token, so a missing token would hang to the test's fail-fast guard rather than pass. This was validated with a mutation check (handing the job a dead token), which made the interruption test fail with its guard message.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VsqPxHuJp8KckiU4FE5hHk


Generated by Claude Code

claude added 2 commits June 20, 2026 06:08
The structured-concurrency between-stage cancellation is now hardened and
covered by tests (PR #50), so reword the 'Deeper cancellation' planned item to
show that progress while keeping the genuinely-remaining work explicit:
interrupting in-flight work by flowing the token into each stage's job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsqPxHuJp8KckiU4FE5hHk
Structured-concurrency cancellation was only observed between stages — a stage
already running was awaited to completion before the next checkpoint could
throw. This adds cancellation-aware I and Let overloads that take a
Func<..., CancellationToken, Task<...>> and flow the pipe's carried token into
the running job, so work already in flight can be cancelled, not just observed
between stages.

- I overloads for value, Task and StructuredTask sources, plus the source-arg
  Let overloads (value / StructuredTask / deferred sources), take a trailing
  CancellationToken and pass the chain's token in. The StructuredTask-source
  paths share the carried token via a token-aware CheckedChain; the value/Task
  entry points own a fresh CancellationTokenSource.
- A third token-aware Let stays a loud compile error (mirrors the token-free
  guard) so it cannot silently drop a deferred.
- The Roslyn generator emits the same (..., CancellationToken) shape for every
  tuple arity (value / Task / StructuredTask tuple sources).
- Tests cover success, in-flight interruption (jobs block on
  Task.Delay(Timeout.Infinite, ct) so they can only complete via the flowed-in
  token), token sharing across stages, the up-front cancelled-source check,
  owned-CTS disposal, token/token-free composition, and tuple sources.
- README documents the new overloads; "Deeper cancellation" leaves Planned
  Features.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsqPxHuJp8KckiU4FE5hHk
Copilot AI review requested due to automatic review settings June 20, 2026 06:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9e59a7660

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread PipeEx.SourceGenerators/TupleDestructuringGenerator.cs Outdated
…dex review)

The generated cancellation-aware tuple I overloads used plain awaits, so a tuple
job that ignored the token it was handed was not abandoned at its awaits: if the
chain was cancelled before the source completed (or while the job ran), the job
still ran and the chain could complete successfully despite Cancel(). The scalar
token overloads gate every await with CheckedAwait; the tuple overloads now do the
same — and the StructuredTask-tuple overload adds the up-front synchronous check —
so cancellation is honoured between the source and the job, and after the job,
even when the job ignores the token.

- Make CheckedAwait internal so the generated TupleDestructuring class can reuse it.
- Generator: gate each await in the three tuple token overloads with CheckedAwait;
  StructuredTask source also throws up-front on an already-cancelled source.
- Tests: a token-ignoring tuple job still cancels (check after the source, before
  the job; trailing check after the job) and an already-cancelled StructuredTask
  tuple source throws at the call site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsqPxHuJp8KckiU4FE5hHk
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@timonkrebs

Copy link
Copy Markdown
Owner Author

Check this!

@timonkrebs timonkrebs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #51: Deeper cancellation

Overview

This PR correctly delivers the "deeper cancellation" feature: new I and Let overloads that take a Func<…, CancellationToken, Task<…>> and flow the pipe's own token into each running job. The design is sound, CheckedAwait routing is consistent for the new paths, and the test file is unusually thorough (mutation-verified interruption tests, disposal, token-sharing, token-free composition, and tuple sources).

CI is green on all four platforms. The core behaviour is correct. I found one pre-existing bug surfaced by this PR, one maintainability hazard introduced by this PR, and two missing tests. Details in the inline comments below.


Generated by Claude Code

@timonkrebs timonkrebs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #51: Deeper cancellation

Overview

This PR correctly delivers the "deeper cancellation" feature: new I and Let overloads that accept Func<…, CancellationToken, Task<…>> and flow the pipe's own token into each running job. The design is sound — ownership transfer via the source argument, CheckedAwait gating on every await, up-front synchronous cancellation checks, and correct CancellationTokenSource disposal semantics are all handled correctly. The test file is unusually thorough: mutation-verified interruption tests, disposal, token-sharing, token-free composition, and tuple sources.

CI is green on all four platforms.

Issues found

  1. Pre-existing bug (FILE comment on TupleDestructuringGenerator.cs) — The token-free StructuredTask<({ty})> source tuple overloads skip cancellation entirely (plain await s.ConfigureAwait(false)), while their scalar counterparts route through CheckedChain/CheckedAwait. This PR's new cancellation-aware tuple overload correctly uses CheckedAwait, making the inconsistency visible. The token-free generated overloads should be fixed in the same way.

  2. Maintainability risk (inline on generator line 172) — The StructuredTask<({ty})> cancellation-aware generated overload inlines the CheckedChain pattern instead of calling it. If CheckedChain changes, the generator must be updated in sync.

  3. Missing test (inline on StructuredConcurrency.cs line 70) — The Task<TSource> source I overload creates and owns a fresh CancellationTokenSource, but there is no disposal test matching I_ValueSource_TokenJob_OwnsAndDisposesCtsAfterAwait.

  4. Minor (inline on test file line 321)AwaitCanceledPromptly / Cancel are typed to StructuredTask<int> and should be made generic for future flexibility.

  5. Positive note (inline on StructuredConcurrency.cs line 75) — The ownership-transfer pattern (new StructuredTask<TResult>(task, source)) is applied correctly for the StructuredTask-source overload. ✅

Items 3 and 4 are low-risk; item 1 is the most important and pre-dates this PR but should be tracked.


Generated by Claude Code

Comment thread PipeEx.SourceGenerators/TupleDestructuringGenerator.cs
Comment thread PipeEx.SourceGenerators/TupleDestructuringGenerator.cs
Comment thread PipeEx.StructuredConcurrency/StructuredConcurrency.cs
Comment thread PipeEx.Tests/StructuredConcurrencyCancellationTests.cs
Comment thread PipeEx.StructuredConcurrency/StructuredConcurrency.cs
Review item 1 (pre-existing bug): the token-free StructuredTask-tuple I
overloads bypassed cancellation with plain awaits, so cancellation requested
between the source completing and the projection running was silently ignored
— unlike the scalar path, which routes through CheckedChain. The generated
sync- and async-func StructuredTask-tuple overloads now mirror the scalar
CheckedChain contract exactly: up-front synchronous check (already-cancelled
source throws at the call site), source await gated by CheckedAwait, and a
trailing check after the projection. New tests pin success, call-site throw,
and cancellation-during-source skipping the projection for both overloads
(mutation-verified against the plain-await regression).

Review item 2 (maintainability): the generated StructuredTask-tuple overloads
inline the CheckedChain pattern because the tuple func signature cannot be fed
to CheckedChain without an adapter; emitted comments now say so and point at
CheckedChain, and both CheckedChain doc comments point back at the generator so
the two stay in sync.

Review item 3: added I_TaskSource_TokenJob_OwnsAndDisposesCtsAfterAwait,
mirroring the value-source disposal test for the Task-source token overload.

Review item 4: the Cancel / AwaitCanceledPromptly test helpers are now generic
over the chain's result type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsqPxHuJp8KckiU4FE5hHk
@timonkrebs
timonkrebs merged commit 804ae69 into main Jul 2, 2026
4 checks passed
@timonkrebs
timonkrebs deleted the claude/quirky-pascal-zw17zz branch July 2, 2026 10:39
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.

3 participants