JetStream Consumer AckWait MaxDeliver Export

I’ll pin PR #4’s current base/head SHAs, inspect the diff and surrounding contracts, then run focused validation. I’ll keep code findings separate from any still-pending GitHub checks.
The local checkout is clean and exactly matches PR head 0e17ee7; GitHub reports base f28a4ca and no CI checks. I’m splitting review by package while I inspect cross-package behavior, dependency posture, and run the repository validation.
The standard build, lint, and race suite are green. I’m now checking two lifecycle boundaries the happy-path tests do not exercise: concurrent/manual stop versus context cancellation, and whether long-handler heartbeats protect messages that Consume has prefetched but not yet dispatched.
The lifecycle review has surfaced a real concurrency defect in Runner.Stop, plus a shutdown error-classification mismatch. I’m reproducing the separate prefetch/heartbeat risk before deciding whether it is a finding or only an API caveat.
The focused diagnostic is hitting an environment-only bind/start issue outside the repo, so I’m not treating that unconfirmed prefetch concern as a review finding. The repository’s own uncached race suite remains green.
The PR head moved during review (from 0e17ee7 to 2d1955e), so the earlier snapshot is stale. I’m re-pinning the new diff and retesting the findings against the updated head before giving a verdict.
The updated head fixes the MaxDeliver zero-value mismatch, but the remaining lifecycle findings persist. I’m doing one final head/check/worktree verification so the review is anchored to the exact commit I report.
Verdict: request changes on head 2d1955e (base f28a4ca). Three merge-blocking lifecycle issues remain.
Findings
-
P1 — Prefetch defeats
KeepInProgress.jsconsumer.go:157callsConsumewith its 500-message default buffer, while callbacks run serially. Only the active message gets heartbeats atjsconsumer.go:212; messages waiting behind a long handler can exhaust AckWait and redeliver concurrently. UsePullMaxMessages(1)for heartbeat-enabled consumers or expose a bounded prefetch. -
P1 —
Runner.Stopis neither concurrency-safe nor a completion boundary.jsconsumer.go:123mutates the interface fieldr.ccwithout synchronization, while context cancellation independently calls it atjsconsumer.go:167. Explicit stop plus cancellation causes a data race. It also returns beforecc.Closed(), allowing active handlers to race connection/database teardown. -
P1 — A transient heartbeat failure permanently disables heartbeats.
keepinprogress.go:52exits on everyInProgresserror. Errors such asnats.ErrReconnectBufExceededcan recover after reconnection, but the helper never retries or reports the failure, allowing unexpected redelivery during a still-running handler. -
P2 — Clean shutdowns can still log warnings.
jsconsumer.go:158usesnuts.IsShutdownFetchErr, butConsumeErrHandleremits the distinctjetstream.ErrConnectionClosed; the classifier only recognizes corenatserrors. -
P2 — Empty
Durablesilently creates an ephemeral consumer.jsconsumer.go:146does not validate the promised durable name. NATS accepts this and generates a fresh consumer, losing stable resume/update behavior. -
P3 — Multiple W3C baggage headers are truncated.
tracecontext.go:28lacks OTel’sValuesGetter, so only the first baggage header value is extracted. -
P3 — Plain
Nak()is unobservable in the advertised test double.fakemsg.go:55records nothing, so tests cannot distinguish a NAK from no disposition.
The newly added EffectiveMaxDeliver correctly fixes the earlier zero-value mismatch.
Validation passed: uncached go test -race -count=1 ./..., lint, build, go mod tidy -diff, and git diff --check. GitHub’s only reported check, Cursor Bugbot, passes; the worktree is clean and exactly matches 2d1955e.