Skip to content

feat(net)!: rolling-window (error-rate) circuit breaker (ADR-0031 Am#3)#119

Merged
NotAProfDev merged 8 commits into
mainfrom
feat/rolling-window-breaker
Jul 9, 2026
Merged

feat(net)!: rolling-window (error-rate) circuit breaker (ADR-0031 Am#3)#119
NotAProfDev merged 8 commits into
mainfrom
feat/rolling-window-breaker

Conversation

@NotAProfDev

@NotAProfDev NotAProfDev commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes #118. Second Tier-2 hardening item split from #102 (after Retry-After honoring, #117).

What

Replaces the CircuitBreaker's consecutive-count trip trigger with a count-based rolling error-rate window (ADR-0031 Amendment #3). Today a single Success resets the failure streak, so a venue failing ~50 % of interleaved traffic (F S F S…) never trips — the top resilience-detection hole from the deep-review §2B. ADR-0031 §5 always planned this ("consecutive-count for v1; rolling-window later").

How

  • New RateWindow unit (rate_window.rs) — a fixed-capacity VecDeque ring of the last-N Failure/Success outcomes with a running failure count; zero per-request allocation, clock-free, integer trip math (u64-widened to avoid overflow).
  • Trip rule (Closed, on a Failure): len ≥ minimum_calls && failures*100 ≥ failure_rate_threshold*len ( trips). Success now dilutes the rate instead of resetting; Ignored (4xx/Auth) is never a sample; a venue 429 (TripNow) still trips immediately on retry_after_fallback/honored value (Amendment chore: Fix cargo-deny workspace wildcards #2, unchanged). Open/HalfOpen/probe-guard/Retry-After are unchanged; recovery builds a fresh window on HalfOpen → Closed.
  • Telemetry: the http_circuit_breaker_transitions_total{to="open"} counter gains a bounded reason label (rate/throttle/probe_failed/abandoned) so a rate-degradation trip is distinguishable from a 429 penalty-box trip.
  • Prior art: tower-resilience-circuitbreaker (cited in the ADR; not adopted — OATH keeps its RPITIT &self/no-dyn Service).

Breaking (pre-release)

CircuitBreakerConfig drops failure_threshold and gains failure_rate_threshold: u8 (1..=100), window_size: NonZeroU32, and minimum_calls: NonZeroU32, validated at boot in stack::validate_config (BuildError::RateThresholdRange / MinCallsExceedWindow). Recommended v1 profile: 50 % / N=50 / min_calls=10 (tunable, not hardcoded).

Tests

TDD throughout: the RateWindow unit, the pure Breaker (incl. the motivating interleaved-50 % case, the min-calls floor, ignored-not-sampled, a discriminating reset-on-close), the boot-validation rejections, and the reason telemetry. just ci + just msrv green.

Reviews

Four per-task reviews (spec + quality) + a final whole-branch review (opus): Ready to merge, 0 Critical / 0 Important. Two non-blocking nice-to-haves deferred: a doc note that should_trip assumes min_calls ≥ 1 (moot — NonZeroU32, validated ≤ window_size), and a sentence on the service_tests::cfg 100/N/N no-interleave assumption.

Spec: docs/superpowers/specs/2026-07-09-net-http-rolling-window-breaker-design.md · Plan: docs/superpowers/plans/2026-07-09-net-http-rolling-window-breaker.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Breaking Changes

    • Circuit breaker trips now use a rolling failure-rate window instead of consecutive failures.
    • Configuration now uses rate/window settings and adds startup validation for invalid values.
  • New Features

    • Open-state telemetry now includes a reason label for clearer breaker transitions.
  • Bug Fixes

    • Local throttling decisions no longer count toward breaker trips.
    • Retry/probe handling now reports more precise open reasons.

NotAProfDev and others added 6 commits July 9, 2026 15:29
Design for replacing the CircuitBreaker's consecutive-count trip trigger
with a count-based rolling error-rate window (ADR-0031 Amendment #3).
Tier-2 hardening item split from #102.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the CircuitBreaker's Closed-state trip trigger from a consecutive
transport-failure counter to the count-based error-rate rolling window
built in the prior commit (ADR-0031 Amendment #3): a sustained failure
rate over a window now trips the breaker, not a run of back-to-back
failures a lone interleaved success used to reset.

Breaking: CircuitBreakerConfig drops `failure_threshold` and gains
`failure_rate_threshold: u8`, `window_size: NonZeroU32`, and
`minimum_calls: NonZeroU32`; boot validation (`stack::validate_config`)
rejects an out-of-range threshold and a `minimum_calls > window_size`
config via two new BuildError variants. Open/Half-Open/probe-guard/
Retry-After behavior is unchanged. Every construction site (doctests,
test helpers, the hyper example) is updated to the new fields.
Records ADR-0031 Amendment #3 (consecutive-count trip -> rolling error-rate window) and the corresponding CHANGELOG entry under [Unreleased] -> Changed.
@NotAProfDev NotAProfDev added the enhancement New feature or request label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@NotAProfDev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bba06d7b-6051-495b-b144-6657ab79c0c7

📥 Commits

Reviewing files that changed from the base of the PR and between d6a6a45 and 5127e44.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • crates/adapter/net/http/api/src/circuit_breaker.rs
  • crates/adapter/net/http/api/src/rate.rs
  • crates/adapter/net/http/api/src/stack.rs
  • docs/adr/0031-http-resilience-venue-pacing.md
📝 Walkthrough

Walkthrough

This PR replaces the net-http CircuitBreaker's consecutive-failure trip policy with a rolling error-rate window. It adds a RateWindow module, swaps CircuitBreakerConfig fields (failure_thresholdfailure_rate_threshold, window_size, minimum_calls), adds boot-time validation, threads an optional TripReason through breaker APIs, extends the to="open" transition metric with a reason label, updates call sites/tests, and documents the change in ADR-0031, a design spec, an implementation plan, and the changelog.

Changes

Rolling error-rate breaker

Layer / File(s) Summary
RateWindow module
crates/adapter/net/http/api/src/rate_window.rs, crates/adapter/net/http/api/src/lib.rs
New fixed-capacity RateWindow/Outcome types with O(1) push/eviction, failure-count tracking, and should_trip predicate; module wired into the crate.
Config and validation
crates/adapter/net/http/api/src/circuit_breaker.rs, crates/adapter/net/http/api/src/rate.rs, crates/adapter/net/http/api/src/stack.rs
CircuitBreakerConfig fields swapped to failure_rate_threshold/window_size/minimum_calls; new BuildError variants and validate_config checks with tests.
Breaker state machine and TripReason
crates/adapter/net/http/api/src/circuit_breaker.rs
Closed state now holds a RateWindow; record/on_abandoned_probe return Option<TripReason>; module docs and constructor updated.
Transition telemetry
crates/adapter/net/http/api/src/meter.rs, crates/adapter/net/http/api/src/circuit_breaker.rs
breaker_transition accepts an optional reason label; probe/record call sites emit reason on open transitions.
Breaker/service test updates
crates/adapter/net/http/api/src/circuit_breaker.rs
Unit and service tests rewritten with new rate-based helpers, asserting threshold, min-calls, ignored-outcome, half-open, and reason=rate behavior.
Downstream config call sites
crates/adapter/net/http/api/src/stack.rs, crates/adapter/net/http/hyper/src/build.rs, crates/adapter/net/http/hyper/examples/client_with_directives.rs
Updated doc examples, test helpers, and the example app to the new config field names.
Documentation
docs/adr/0031-http-resilience-venue-pacing.md, docs/superpowers/plans/..., docs/superpowers/specs/..., CHANGELOG.md
ADR amendment, implementation plan, design spec, and changelog entry describing the new policy, config, and telemetry label.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • NotAProfDev/oath#85: Modifies the same circuit-breaker config/state machine and trip behavior in circuit_breaker.rs.
  • NotAProfDev/oath#104: Touches the same Throttled-as-Ignored classification and Half-Open probe handling in circuit_breaker.rs.
  • NotAProfDev/oath#117: Modifies the same breaker record/429 open-trip behavior and metrics plumbing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and clearly describes the rolling-window circuit breaker change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rolling-window-breaker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/adapter/net/http/api/src/stack.rs (1)

211-225: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding window_size at boot to avoid a large eager allocation.

validate_config checks minimum_calls <= window_size and the threshold range, but places no ceiling on window_size itself. RateWindow::new (rate_window.rs) does VecDeque::with_capacity(window_size.get() as usize) — an eager allocation that scales directly with a misconfigured value (up to ~4 billion Outcome slots for NonZeroU32::MAX), and this reallocates on every Half-Open→Closed recovery, not just at boot.

Since this is boot-time operator config rather than attacker-controlled input, this is a defensive suggestion rather than a security bug — a sane upper bound (e.g. a few thousand) would catch a units/typo mistake before it causes a large or failing allocation.

🛡️ Example additional guard
     if cfg.circuit_breaker.minimum_calls.get() > cfg.circuit_breaker.window_size.get() {
         return Err(BuildError::MinCallsExceedWindow(
             cfg.circuit_breaker.minimum_calls.get(),
             cfg.circuit_breaker.window_size.get(),
         ));
     }
+    // Sanity ceiling: guards against a units/typo mistake causing a very large
+    // eager `VecDeque` allocation in `RateWindow::new`.
+    const MAX_WINDOW_SIZE: u32 = 10_000;
+    if cfg.circuit_breaker.window_size.get() > MAX_WINDOW_SIZE {
+        return Err(BuildError::WindowSizeTooLarge(
+            cfg.circuit_breaker.window_size.get(),
+            MAX_WINDOW_SIZE,
+        ));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/adapter/net/http/api/src/stack.rs` around lines 211 - 225, Add a sane
upper bound check for circuit breaker window size in validate_config alongside
the existing threshold and minimum_calls validation, so oversized values are
rejected before RateWindow::new allocates a large VecDeque. Use the existing
config fields in stack.rs and the RateWindow::new constructor in rate_window.rs
as the key symbols to update, and return a BuildError for values above the
chosen boot-time limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/adapter/net/http/api/src/stack.rs`:
- Around line 211-225: Add a sane upper bound check for circuit breaker window
size in validate_config alongside the existing threshold and minimum_calls
validation, so oversized values are rejected before RateWindow::new allocates a
large VecDeque. Use the existing config fields in stack.rs and the
RateWindow::new constructor in rate_window.rs as the key symbols to update, and
return a BuildError for values above the chosen boot-time limit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f205b71-832a-4bda-a444-7e6400f6895b

📥 Commits

Reviewing files that changed from the base of the PR and between 344d77a and d6a6a45.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • crates/adapter/net/http/api/src/circuit_breaker.rs
  • crates/adapter/net/http/api/src/lib.rs
  • crates/adapter/net/http/api/src/meter.rs
  • crates/adapter/net/http/api/src/rate.rs
  • crates/adapter/net/http/api/src/rate_window.rs
  • crates/adapter/net/http/api/src/stack.rs
  • crates/adapter/net/http/hyper/examples/client_with_directives.rs
  • crates/adapter/net/http/hyper/src/build.rs
  • docs/adr/0031-http-resilience-venue-pacing.md
  • docs/superpowers/plans/2026-07-09-net-http-rolling-window-breaker.md
  • docs/superpowers/specs/2026-07-09-net-http-rolling-window-breaker-design.md

NotAProfDev and others added 2 commits July 9, 2026 18:02
Reject a window_size above a 10k sanity ceiling in validate_config
(new BuildError::WindowSizeTooLarge) so a units/typo mistake can't force
a huge eager RateWindow VecDeque alloc — re-allocated on every recovery.
Consistent with the existing fail-closed boot validation. Addresses the
CodeRabbit review on PR #119.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NotAProfDev NotAProfDev merged commit 10e30ca into main Jul 9, 2026
5 checks passed
@NotAProfDev NotAProfDev deleted the feat/rolling-window-breaker branch July 9, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(net): rolling-window (error-rate) circuit breaker — replace consecutive-count (ADR-0031 Amendment #3)

1 participant