Skip to content

fix: let the limiter count only calls the circuit lets through - #38

Merged
costela merged 1 commit into
mainfrom
fix/limiter-counts-admitted-calls
Oct 9, 2026
Merged

costela merged 1 commit into
mainfrom
fix/limiter-counts-admitted-calls

Conversation

@costela

@costela costela commented Oct 9, 2026

Copy link
Copy Markdown
Member

Why

Wrap claimed the circuit's state for a call, including the half-open call, before any middleware ran, and the concurrency limiter took a slot before the circuit checked the call. As a result:

  • calls into an open circuit waited for or took a slot instead of failing fast;
  • a call the limiter rejected used up the half-open call, keeping the circuit open for another full delay;
  • a call that waited for a slot entered the circuit with the state it had seen before waiting, even if the circuit had opened meanwhile.

What

  • Wrap only reads the state for the middleware. The circuit decides on its current state, and claims the half-open call, once the call reaches it.
  • The limiter lets calls into an open circuit through without a slot, so the circuit rejects them right away.
  • The closed case of Circuit.State is split off to be inlinable, which removes the ~1.5ns the extra state read cost per call. Remaining difference vs main: within ±1.2% (benchstat, n=10, interleaved runs; command and table in the commit message).
  • Tests for all three symptoms (each fails on main). The permit-leak test now uses a call that loses the half-open race, since calls into an open circuit no longer take a slot.

Behavior change for custom middleware: a half-open state passed to ObserverForCall no longer means the call gets the half-open call.

Independent of #37; the tests pass with and without it.

🤖 Generated with Claude Code

Wrap claimed the circuit's state for a call, including the half-open
call, before any middleware ran, and the limiter took a slot before the
circuit checked the call. As a result:
- calls into an open circuit waited for or took a slot instead of
  failing fast;
- a call the limiter rejected used up the half-open call, keeping the
  circuit open for another full delay;
- a call that waited for a slot entered the circuit with the state it
  had seen before waiting, even if the circuit had opened meanwhile.

Wrap now only reads the state for the middleware. The circuit decides
on its current state, and claims the half-open call, once the call
reaches it, and the limiter lets calls into an open circuit through
without a slot. For custom middleware, a half-open state no longer means
the call gets the half-open call.

The extra state read cost ~1.5ns per call until the closed case of
Circuit.State was made inlinable. benchstat, n=10, main and this branch
run interleaved, 1s each:
  ./main.test|./pr.test -test.run '^$' -test.bench BenchmarkHoglet_Do_ \
    -test.benchmem -test.benchtime 1s -test.cpu 1,8,24 -test.count 1
  Hoglet_Do_EWMA                       27.0n   27.3n       ~ (p=0.325)
  Hoglet_Do_EWMA-8                     3.41n   3.41n       ~ (p=0.493)
  Hoglet_Do_EWMA-24                    1.97n   1.99n  +1.07% (p=0.033)
  Hoglet_Do_SlidingWindow             103.4n  104.3n  +0.92% (p=0.010)
  Hoglet_Do_SlidingWindow-8            53.0n   52.0n  -1.88% (p=0.011)
  Hoglet_Do_SlidingWindow_rotating    103.5n  104.8n  +1.16% (p=0.002)
  Hoglet_Do_SlidingWindow_rotating-8   54.9n   54.8n       ~ (p=0.796)
  Hoglet_Do_SlidingWindow_rotating-24  43.8n   43.8n       ~ (p=0.424)
Hoglet_Do_SlidingWindow-24 is bimodal on main (±53%) and left out.
allocs/op stay 0. An earlier control with identical code in both
binaries shifted by up to 1.89%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

🟢 Approval recommended

The implementation matches the stated behavior and includes focused regression coverage for all identified failure modes.

0 open findings

What changed in this PR

Moves circuit admission checks behind middleware delays while ensuring open-circuit calls bypass concurrency-limit slots.

Changes:

  • Defers current-state and half-open admission decisions to the circuit.
  • Makes concurrency limiters skip open-circuit calls.
  • Adds regression coverage for slot usage and state changes while waiting.
File Description
hoglet.go Updates circuit state handling and middleware contract.
hoglet_test.go Tests half-open rejection and delayed-call behavior.
limiter.go Bypasses permits for open-circuit calls.
limiter_test.go Tests permit release and open-circuit bypass.

🧠 Review effort: Balanced


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

@costela
costela marked this pull request as ready for review October 9, 2026 10:48
@costela
costela merged commit d40e8d2 into main Oct 9, 2026
3 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.

3 participants