Skip to content

test: stop the stall-guard tests blaming the guard for working - #13

Merged
packetloss404 merged 1 commit into
mainfrom
fix/stall-guard-premise
Sep 5, 2026
Merged

test: stop the stall-guard tests blaming the guard for working#13
packetloss404 merged 1 commit into
mainfrom
fix/stall-guard-premise

Conversation

@packetloss404

Copy link
Copy Markdown
Owner

Fixes the TestStallGuard_TickKeepsAlive flake found while verifying the shutdown-budget work, plus the same defect waiting in TestStallGuard_ConcurrentTicks.

What was wrong

Both tests assert a negative: that the guard does not fire while Ticks keep arriving. That premise holds only while the ticking goroutine is actually scheduled inside the guard's window — and the windows were 30 ms and 40 ms.

A loaded machine starves a ticker past 30 ms easily. The guard then fires, correctly, and the test reports it as a bug in the guard. Reproduced at 3 failures in a batch of 25 under six concurrent suite runs, on a tree with no other changes.

Two changes, and the second is the one that matters

1. The windows scale with testwait.Factor. Not testwait.Timeout — this is a real-time window the guard itself measures, and Timeout's five-second floor would stretch a millisecond-scale test into a ten-second one. What the test needs from the scale is the ratio: scheduling jitter is roughly constant in absolute terms, so a wider window makes a starved ticker proportionally rarer.

2. But rarer is not correct, so the tests now verify their own premise instead of assuming it. Each records the widest gap between its own Ticks. A cancellation is a failure only when every Tick demonstrably landed inside the window; when the ticker was itself starved past it, the test says so and skips — because firing was the right thing to do.

The test can now only fail when the guard actually misbehaves.

Verified in both directions

control result
Tick() stubbed to do nothing (guard genuinely broken) both tests fail, naming the evidence: cancelled although every Tick landed inside the 300ms window (widest gap 147ms)
PACKETCODE_TEST_TIMEOUT_SCALE=0.05 → impossible 1.5 ms window skips, not a false failure: widest gap between Ticks was 2ms, past the 1.5ms guard window, so cancelling was the correct behaviour
6 batches × 25 iterations under 6 concurrent full-suite runs 900 executions, 0 failures, 0 skips — the widened window alone sufficed, with the premise check as the net

Also -race clean, and the concurrent test still calls Tick off-lock so the race detector still sees it hit from eight goroutines at once.

Checks

  • go test ./... clean; go test -race -run TestStallGuard clean.
  • golangci-lint (repo config, truncation disabled) on an LF checkout of this commit under GOOS=linux, darwin, windows: 0 issues each. (Linting the CRLF working tree directly is meaningless here — every file reports a gofmt diff.)

🤖 Generated with Claude Code

TestStallGuard_TickKeepsAlive was flaky, and TestStallGuard_ConcurrentTicks
had the same defect waiting.

Both assert a negative: that the guard does not fire while Ticks keep
arriving. That premise holds only while the ticking goroutine is actually
scheduled inside the guard's window, and the windows were 30ms and 40ms.
A loaded machine starves a ticker past 30ms easily, at which point the
guard fires -- correctly -- and the test reports it as a bug in the guard.
Under six concurrent suite runs this produced 3 failures in a batch of 25,
on a tree with no other changes.

Two changes, and the second is the one that matters.

The windows scale with testwait.Factor. Not testwait.Timeout: this is a
real-time window the guard itself measures, and Timeout's five-second
floor would stretch a millisecond-scale test into a ten-second one. What
the test needs from the scale is the ratio, because scheduling jitter is
roughly constant in absolute terms, so a wider window makes a starved
ticker proportionally rarer.

But rarer is not correct, so the tests now verify their own premise
instead of assuming it. Each records the widest gap between its own Ticks.
A cancellation is a failure only when every Tick demonstrably landed
inside the window; when the ticker was itself starved past it, the test
says so and skips, because firing was the right thing to do. The test can
now only fail when the guard actually misbehaves.

Verified in both directions. With Tick stubbed out to do nothing, both
tests fail and name the evidence: "cancelled although every Tick landed
inside the 300ms window (widest gap 147ms)". Under a deliberately
impossible 1.5ms window they skip rather than lie. And under six
concurrent full-suite runs, 900 executions pass with no skips at all --
the widened window alone was enough, with the premise check as the net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T20:03:44.514117Z 84ef12e PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@packetloss404
packetloss404 merged commit 4ee348f into main Sep 5, 2026
16 checks passed

@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: 84ef12ec22

ℹ️ 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".

defer g.Stop()

// Tick several times at sub-timeout intervals so the guard never fires.
rec := newTickRecorder()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Start recording before arming the guard

If the test goroutine is descheduled after NewStallGuard arms its timer but before this recorder is created, the guard can correctly expire while that initial starvation interval is never recorded. On resumption, last starts at the current time and subsequent ticks can all show sub-window gaps, so explainCancellation reports a failure instead of skipping—the exact load-induced false failure this change is intended to prevent. The concurrent test has the same ordering; initialize the recorder timestamp before arming the guard (or otherwise include the arm-to-first-tick interval).

Useful? React with 👍 / 👎.

@packetloss404
packetloss404 deleted the fix/stall-guard-premise branch September 5, 2026 20:03
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