Skip to content

fix(source/kafka): honor drain contract after Close; make Nak redeliver in-session - #199

Merged
joshua-temple merged 2 commits into
mainfrom
feat/source-v1-p0-contract
Aug 23, 2026
Merged

joshua-temple merged 2 commits into
mainfrom
feat/source-v1-p0-contract

Conversation

@joshua-temple

Copy link
Copy Markdown
Collaborator

Checkpoint 1/5 — P0-1 (drain) + P0-2 (Nak contract) on the road to source/v1.0.0

Implements the two blocking P0s from the v1-readiness review (crucible-source-v1-review.md).

P0-1 — Kafka Next/NextBatch violated the drain contract after Close

The pre-poll check closed && inFlight == 0 meant a closed subscription with in-flight records kept calling PollRecords and handed out new records — a direct violation of the source.Subscription drain contract for any standalone driver. Now: after Close, never poll; yield only already-buffered records; when the buffer is empty, block until the last in-flight settle lands (settle-wakeup channel, ctx-honoring), then return ErrDrained.

P0-2 — core at-least-once claim vs Kafka plain-Nak reality (review option (a))

Plain Nak previously only declined to mark → skipped for the live group until restart/rebalance while source/doc.go claimed universal at-least-once. Plain Nak now goes through the same machinery as NakAfter(d) with d=0: pause partition → re-seek to the record's offset → resume. Redelivery is deterministic within a live subscription; cross-restart redelivery rides committed offsets and stays documented best-effort (the one divergence).

Contract text now in one voice across: source/doc.go (# Contract), source/inlet.go (Next), source/handler.go (ActionNak), source/kafka/kafka.go (ack model), source/kafka/README.md (table + divergence section). Also documents requeueWithDelay cost model + poll-error record discard.

Evidence (all run locally on this branch)

Check Command Result
vet+build core go vet ./... && go build ./... (source/) ok
unit+-race core go test -race -count=1 ./... ok
fuzz smoke go test -fuzz=FuzzOrderedSettle -fuzztime=8s + batch variant ok
integration go test -tags integration -count=1 ./... (RedPanda/Docker 28.4.0) ok 15.7s
kafka vet/race GOWORK=off go vet && go test -race -count=1 ./... (source/kafka) ok

New tests (all pass under -race -count=1): TestNextAfterCloseYieldsOnlyBufferedThenDrained, TestNextBatchAfterCloseYieldsOnlyBufferedThenDrained, TestNextAfterCloseDoesNotPollWithEmptyBufferAndNoInFlight, TestPlainNakReseeksPartitionImmediately, TestNakAfterDelaysThenReseeks, TestDuplicateSettleIsSafe, TestHopper_RedeliveryStormBoundedResources.

Regressions vs baseline: none (full matrix green before and after).

API diff vs baseline: none (behavior/docs/tests only).

…er in-session

P0-1: Next/NextBatch re-checked closed&&inFlight==0 before each poll, so a
closed subscription with records still in flight kept polling and yielded NEW
records — violating the source.Subscription drain contract when driven outside
the Hopper. Once closed they now never poll: already-buffered records are
yielded, then the call blocks on a settle-wakeup channel until the last
in-flight record settles, then returns ErrDrained (ctx cancellation honored).

P0-2 (review option a): plain Nak previously only declined to mark, so the
record was skipped for the live group until restart/rebalance while the core
contract claimed universal at-least-once. Plain Nak now re-seeks through the
same machinery as NakAfter with delay zero (pause partition, seek to the
record's offset, resume), making redelivery deterministic within a live
subscription. Cross-restart redelivery rides committed offsets and stays
documented best-effort.

Contract text aligned in one voice across source/doc.go, inlet.go,
handler.go ActionNak, kafka package doc, and kafka README divergence table;
requeueWithDelay cost model documented (head-of-line pause, buffered-ahead
records, concurrent-mark override); poll-error record discard documented.

Tests: close-then-Next/NextBatch regressions against the real subscription
(fake poller), plain-Nak immediate re-seek, NakAfter delay, duplicate-settle
safety, Hopper redelivery storm (bounded goroutines, >=501 deliveries).
@joshua-temple
joshua-temple force-pushed the feat/source-v1-p0-contract branch from 9517260 to ce69966 Compare August 23, 2026 02:50
@joshua-temple
joshua-temple merged commit 974da59 into main Aug 23, 2026
121 checks passed
@joshua-temple
joshua-temple deleted the feat/source-v1-p0-contract branch August 23, 2026 03:06
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.

1 participant