Skip to content

A test asserted five of fifteen waited, and CI got one - #776

Merged
jorgemanrubia merged 2 commits into
mainfrom
overflow-waits-provably
Sep 19, 2026
Merged

jorgemanrubia merged 2 commits into
mainfrom
overflow-waits-provably

Conversation

@jorgemanrubia

Copy link
Copy Markdown
Member

TestGateQueuesOversubscribedInvocationsWithinTheSlotLimit went red on a pull request that had not touched internal/resilience: it expects five of fifteen callers to find every slot taken, and got one. The assertion is mine, from Two gate tests were failing on the clock, not on the gate, and it is wrong in exactly the way that card was about — it described a race and called it a fact.

Follows up A gate test spends its 10-second budget on the machine, not on the gate.

Why a barrier was not enough

#774 replaced a wall-clock bound with a barrier so the children arrive together instead of as fast as the box can fork. That fixed the start. It does not keep them together afterwards: a child released and then descheduled arrives after an earlier holder has finished, takes the slot it freed, and never waits. On a two-core runner with fifteen processes that gap is wide, and the count drops.

The witness was doing its job, incidentally — it refused to say five when five had not queued. What was wrong was the expectation, not the instrument.

Holding the table full

The parent now keeps the slot table saturated until the contention has actually happened. Each child announces the first time the bulkhead turns it away, and announces when it holds a slot and then waits before releasing it. The parent blocks until ten are held and five have been refused, and only then lets the holders go.

That makes the oversubscription something the test waited for rather than something it hoped for, so five is exact rather than likely. It waits rather than polls on purpose: a bulkhead that stopped excluding anyone cannot make this test pass with a smaller number — it hangs and fails on the test's own deadline, loudly, instead of reporting one and looking like a flake.

Evidence

Spreading the arrivals 50ms apart, which is what a loaded runner does to fifteen forks, reproduces the failure deliberately:

  • old version, arrivals spread: red, peak 4 against 10 and slotWaits 0 against 5 — the CI symptom and worse
  • new version, same spread: green, four runs out of four
  • concurrency limit removed entirely: the new test hangs and fails on its deadline rather than passing
  • five runs on a saturated core under race instrumentation: green

Main

Main has had one Test run since #774 merged and it was green, so this is a latent flake rather than a broken main. It is on its own branch rather than inside Nothing started the connector again after a crash, and no skill could start it at all, whose Tests check it is currently blocking.

TestGateQueuesOversubscribedInvocationsWithinTheSlotLimit asserted that
five of fifteen callers found every slot taken. CI got one. The assertion
was mine, from #774, and it was wrong in the way that card was about: it
described a race and called it a fact.

A barrier at the start lines up the arrivals, which is what the last
change fixed. It does not keep them together afterwards. A child released
and then descheduled arrives after an earlier holder has already finished
and takes the slot it freed, so it never waits — and on a two-core runner
under fifteen processes that gap is wide.

So the parent now holds the slot table full. Each child announces the
first time the bulkhead turns it away, and announces holding a slot and
then waits before releasing it; the parent blocks until ten are held and
five have been refused, and only then lets the holders go. The
oversubscription is something the test waited for rather than something
it hoped for, and five is exact.

It waits rather than polls on purpose. A bulkhead that stopped excluding
anyone cannot make this test pass with a smaller number — it hangs and
fails on the test's own deadline, loudly, instead of reporting one and
looking like a flake.

Evidence. Spreading the arrivals 50ms apart, which is what a loaded
runner does to fifteen forks, fails the old version with peak 4 against
10 and slotWaits 0 against 5 — the CI symptom, reproduced deliberately —
and leaves the new one passing, four times out of four. Removing the
concurrency limit entirely makes the new test hang and fail on its
deadline rather than pass. Five runs on a saturated core under race
instrumentation pass.

Main has had one run since #774 merged and it was green, so this was a
latent flake rather than a broken main; it surfaced on a pull request
that had not touched this package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 19, 2026 16:36
@github-actions github-actions Bot added the tests Tests (unit and e2e) label Sep 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The new helper’s slots argument does not configure child bulkheads, causing non-default usages to hang.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Stabilizes the oversubscription gate test by holding acquired slots until contention is observed.

Changes:

  • Adds explicit WAITING and HELD synchronization.
  • Replaces timing-dependent contention with deterministic coordination.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
internal/​resilience/​gate_test.go Adds deterministic bulkhead oversubscription coordination.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/resilience/gate_test.go
runSlotOversubscription took a slots argument and did not use it for
anything but its own arithmetic: the children went on running under
DefaultConfig's ten. Any other value left the parent waiting for a split
that cannot happen.

That matters more here than an ignored parameter usually would. This
helper waits for the oversubscription rather than sampling it, which is
the point of the change — but it means a mismatch no longer shows up as a
wrong number. It shows up as the test deadline, with a message about a
timeout and nothing about the cause. Confirmed before fixing: eight
callers against five slots hung for the full forty-five seconds.

The count now goes to the children through BH_MAX_CONCURRENT, beside
BH_MAX_TOKENS, and the helper refuses a caller environment that would
override it.

Its own test, because the failure mode is a deadline rather than an
assertion: five slots for eight callers, five hold, three are refused.
Removing the wiring hangs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 19, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approved

The synchronization correctly guarantees measurable oversubscription and addresses the previous feedback.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@jorgemanrubia
jorgemanrubia merged commit 7799304 into main Sep 19, 2026
27 checks passed
@jorgemanrubia
jorgemanrubia deleted the overflow-waits-provably branch September 19, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants