Skip to content

fix(api): a busy consumer is not an idle stream (#46) - #48

Merged
sre-helmcode merged 2 commits into
mainfrom
fix/flaky-oversize-follow-46
Oct 12, 2026
Merged

sre-helmcode merged 2 commits into
mainfrom
fix/flaky-oversize-follow-46

Conversation

@sre-helmcode

@sre-helmcode sre-helmcode commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #46

Root cause

A real (minor) client bug, not a racy test counter. In followOnce, the idle timer keeps running while Follow itself is busy: a slow onFrame, or the main goroutine descheduled on a loaded runner. During that time the scanner goroutine has already read the next line and is blocked handing it over on the unbuffered lines channel, so it stops reading and lastRead goes stale. When the main loop returns to select, both timer.C and lines are ready; if select picked the timer, the quiet check failed and the working connection was dropped as errIdle and reconnected.

In TestFollowRefusesOversizedLinesAndFrames (5 MiB frame, 200ms test idle limit) a stall on the ubuntu runner did exactly that before the 4 MiB frame cap was reached, so the second connection refused the frame: fatal error, but calls=2. The oversize refusal itself was always fatal and never retried; the retry came from the spurious idle reconnect before it.

Fix

  • runs.go: when the idle timer fires, a line already waiting wins (non-blocking receive), the same way a read that already ended already did. Only when nothing is waiting does the lastRead quiet check decide idleness. Genuinely idle connections are detected exactly as before.
  • New TestFollowBusyConsumerIsNotIdle: onFrame sleeps 200ms (2x the 100ms idle limit) on each of 5 events, followed by either end or an oversized frame. Deterministic repro on the old client (first draft 20/20 failures, final version 10/10), including the exact CI string frame: err=the platform sent a stream frame over 4194304 bytes calls=2.
  • TestFollowRefusesOversizedLinesAndFrames no longer runs on the 200ms helper idle limit it does not exercise (30s), so a stalled runner cannot reconnect it.
  • TestFollowOversizedLineArrivingSlowlyIsNotIdle keeps its purpose (the line takes ~660ms to arrive, longer than the 250ms idle limit) with a wide margin between each 20ms chunk gap and the limit (was 60ms vs 20ms).

Test evidence

  • Before: go test -count=20 -run TestFollowBusyConsumerIsNotIdle ./internal/api/ -> 20/20 FAIL.
  • After, under CPU pressure (2x ncpu yes hogs): go test -race -count=2000 -cpu=1,2,4 -run TestFollowRefusesOversizedLinesAndFrames ./internal/api/ -> ok (6000 runs).
  • After: GOMAXPROCS=1 go test -race -count=60 -cpu=1,2,4 -run 'TestFollowBusyConsumerIsNotIdle|TestFollowOversizedLineArrivingSlowlyIsNotIdle' -> ok.
  • After: GOMAXPROCS=1 go test -race -count=30 -cpu=1,2 -run 'TestFollowBusyConsumerIsNotIdle|TestFollowRefusesOversized' -> ok.
  • go test -race ./..., go vet ./..., gofmt -l clean.

Review

Blocking QA review (Claude Code, Opus): APPROVED, no blockers. Nits applied: wider margins in the new test, comment on where the frame cap trips. Known residual window (not fixed, by design): if the whole process freezes for longer than the idle limit right after the reader hands over a line and before it reads the next one (already in the scanner buffer), that is still read as idle. Unrealistic with the production 45s limit, and the oversize test now runs with 30s.

Deploy notes

Test-and-client-internal change; no VERSION bump on purpose (merging a bump auto-releases). It ships with the next release.

🤖 Generated with Claude Code

barckcode and others added 2 commits October 12, 2026 20:50
Follow's idle timer also expires while Follow itself is busy (a slow
onFrame, or the goroutine descheduled on a loaded runner). The reader is
then blocked handing over a line it already read, so lastRead goes stale
and select could pick the expired timer over the waiting line: a working
stream was reconnected as idle. Before an oversized frame, that turned
the fatal refusal into a second connection, which is the calls=2 flake
in TestFollowRefusesOversizedLinesAndFrames.

When the timer fires, a line already waiting now wins, like a read that
already ended did. TestFollowBusyConsumerIsNotIdle reproduces the race
deterministically (20/20 failures before, including the exact
"frame over 4194304 bytes calls=2"). The oversize tests no longer run
on the 200ms test idle limit they do not exercise, and the slowly
arriving line test keeps a wide margin between chunk gaps and the limit.

Fixes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: a 100ms idle limit with a 200ms busy consumer keeps the
race reproducible (10/10 failures on the old client) without the limit
itself being tight enough to reconnect on a loaded runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sre-helmcode
sre-helmcode merged commit b6cdc00 into main Oct 12, 2026
5 checks passed
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.

Flaky: TestFollowRefusesOversizedLinesAndFrames (calls=2 on ubuntu)

2 participants