Skip to content

Give a held session's later probes their own throttle slot - #20

Merged
gvonnessi merged 1 commit into
mainfrom
fix/hold-probe-throttle-starvation
Aug 10, 2026
Merged

gvonnessi merged 1 commit into
mainfrom
fix/hold-probe-throttle-starvation

Conversation

@gvonnessi

Copy link
Copy Markdown
Contributor

Summary

A session that switched billing mid-life could stay held forever rather than for SUB_HOLD_SECS.

The hold that stops a resumed subscription session's shadow counter being presented as spend lifts only once that session's own cost is observed moving with no windows — at least two observations, 45 minutes apart. Only the first was exempt from the throttle, via changes_shape; every later probe competed for a slot that is global across sessions.

So the failure needs no unusual input, only a second busy session. A peer rendering on the same timer takes the slot, the switched session is never observed moving, and the proof the hold waits for is never assembled. The status line then shows limits n/a for the life of the session and real API-key spend never reaches the day total.

Found in the wild on a machine running four concurrent sessions: the switched session had logged its windowed anchor and exactly one probe, then nothing for over an hour.

  • Later probes from a held session now take a per-session slot too, bounded exactly as changes_shape is — one extra write per minute per session — and gated on the counter having actually moved.
  • The hold verdict is hoisted above the throttle, since it now governs both whether a sample may take a slot and whether it may count as spend. Behaviour-preserving for every other path.

Test plan

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo build, cargo test --all (85 passed)
  • New a_held_sessions_later_probes_are_never_lost_to_the_shared_throttle — a peer contends for the slot every minute across the whole hold window; asserts the probes keep landing, that spend is counted again once movement is proven, and that the windowed anchor survived (so the release came from proof, not from retention evicting the anchor)
  • Mutation-verified: reverting just the !hold_probe term fails the new test with got 2 — the anchor plus one probe, exactly the field signature
  • New a_frozen_hold_probe_still_waits_its_turn — a frozen counter still writes nothing, so the exemption cannot become a back door. Pins the outcome rather than the mechanism, and says so: two guards produce it, and removing either alone leaves it green
  • Sample volume checked against MAX_ENTRIES so pruning cannot make the new test pass vacuously

The hold that stops a resumed subscription session's shadow counter being
presented as spend lifts only once that session's own cost is observed moving
with no windows — at least two observations, SUB_HOLD_SECS apart. Only the
first of those was exempt from the shared throttle, via changes_shape; every
later probe competed for a slot that is global across sessions.

So the failure needed no unusual input, only a second busy session. A peer
rendering on the same timer takes the slot every minute, the switched session
is never observed moving, and the proof the hold waits for can never be
assembled. The wait stops being a 45-minute one and becomes permanent: the
status line shows `limits n/a` for the life of the session and real API-key
spend never reaches the day total.

Later probes from a held session now take a per-session slot too, bounded
exactly as changes_shape is — one extra write per minute per session — and
gated on the counter having actually moved.
@gvonnessi
gvonnessi merged commit 1584f36 into main Aug 10, 2026
3 checks passed
@gvonnessi
gvonnessi deleted the fix/hold-probe-throttle-starvation branch August 10, 2026 14:50
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