fix: honor ctx cancellation in terminal operations - #18
Merged
Merged
Conversation
tr1v3r
force-pushed
the
fix/terminal-ctx-cancel
branch
from
September 14, 2026 02:31
56e2f9f to
6ce4d34
Compare
The WithContext contract (export.go: "a cancelled context makes intermediate operations stop pulling and terminals return promptly"; doc.go and README promise WithContext as the remedy for infinite sources) silently failed on bare pipelines: ToSlice/Collect/Count(-1 path)/Last/First/Seq and the materialize-based stages (Sort/ReverseSort/Reverse/Pick end<0/Execute) never consulted ctx, so Repeat(1).WithContext(cancelled).ToSlice() hung forever. Root cause: only ForEach/AllMatch/AnyMatch/NonMatch/Reduce*/Take checked s.cancelled(); materialize() and the remaining terminals pulled upstream unconditionally. Fix: materialize(ctx, seq) consults ctx at every element boundary — empty when cancelled up front, partial results when cancelled mid-stream (same semantics as the Reduce family) — and all six call sites pass the streamer's ctx. First/Last/Count(iteration path)/Seq gain per-element checks; Seq returns a wrapped sequence so native range loops over bare pipelines honor WithContext; Pick's counting loop gains the check that every other intermediate op already had. Docs updated to state the cooperative, element-boundary cancellation semantics precisely. Tests: cancel_bare_test.go — 12 terminals on a pre-cancelled bare pipeline return empty/zero; an infinite source unblocks every terminal after mid-run cancellation (5s watchdogs); the README repro Repeat(1).WithContext(ctx).ToSlice() returns after cancel; uncancelled control results unchanged. go test ./... -race -count=1 green; vet clean; golangci-lint 0 issues.
tr1v3r
force-pushed
the
fix/terminal-ctx-cancel
branch
from
September 14, 2026 02:35
6ce4d34 to
caff3b9
Compare
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.
Problem
The published cancellation contract was silently broken on bare pipelines (no intermediate ops between
WithContextand the terminal):WithContext): "a cancelled context makes intermediate operations stop pulling and terminals return promptly."LimitorWithContext" → `stream.Repeat(1).WithContext(ctx).Take() // cancellable: ok*Hang repro (pre-fix):
Affected terminals/paths:
ToSlice,Collect,Count(-1 path),Last,First,Seq, and every materialize-based stage —Sort,ReverseSort,Reverse,Pick(end<0),Execute.Root cause
Only
ForEach/AllMatch/AnyMatch/NonMatch/Reduce*/Takecheckeds.cancelled().materialize()and the remaining terminals pulled upstream unconditionally, so a cancelled context never stopped a bare pipeline (andRepeat+terminal hung forever).Fix
materialize(ctx, seq)now consults ctx at every element boundary: empty when cancelled up front, the elements pulled so far when cancelled mid-stream — the same partial-result semantics the Reduce family already had. All six call sites pass the streamer's ctx (Sort / ReverseSort / Reverse / Pick materialize path / Execute / ToSlice).First(cancelled → zero value),Last(cancelled → partial),Countiteration path (cancelled → count so far; the O(1) sizeHint fast path consumes nothing and deliberately does not consult ctx — documented) gain per-element checks.Seq()returns a wrapped sequence checkingcancelled()per element, so nativefor rangeloops over bare pipelines honorWithContext.Pick's counting loop gains the check every other intermediate op already had (aligns withSkip/Limit).export.goWithContext/ToSlice/Last/Count/Seq entries,doc.go,README.md) now state the cooperative element-boundary cancellation semantics precisely (incl. the "later operations only" boundary).Test evidence
New
cancel_bare_test.go:ToSlice/Collect/Last/Count/First/Sort/ReverseSort/Reverse/Pickboth paths/Execute/Seqrange)Repeat(1).WithContext(ctx).ToSlice()returns after cancel