Add CircuitBreaker + AsyncTimeout (0.10.0)#51
Conversation
Spec for the two resilience primitives that complete the Polly-shape suite: classic consecutive-failure CircuitBreaker/AsyncCircuitBreaker and an async-only AsyncTimeout (overall pipeline deadline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six-task TDD plan: CircuitOpenError, AsyncTimeout, shared _CircuitBreakerState + AsyncCircuitBreaker, sync CircuitBreaker, property test, docs/release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e breaker) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ers on open Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…syncRetry The recommended chain wrongly placed AsyncBulkhead inside AsyncRetry, contradicting the tested [AsyncBulkhead, AsyncRetry] guidance (one slot per logical call). Correct order: AsyncTimeout -> AsyncCircuitBreaker -> AsyncBulkhead -> AsyncRetry -> terminal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The accumulate-advances-with-break loop only exercised the `break` branch when generated advances happened to sum past reset_timeout, which Hypothesis did not hit on every run — intermittently dropping coverage to 99.98% and failing --cov-fail-under=100. Map each clock step to a fraction strictly below reset_timeout instead; no branch, 100% every run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…codes
Requiring frozenset[int] made the natural call site
`failure_status_codes={500, 503}` (a set literal) a ty error. Accept
Collection[int] and freeze internally — membership semantics unchanged,
no friction pushed onto callers. Tests now pass a plain set and a list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed in c52d343. 1. 2. 3. Logging under the lock (sync) — leaving as-is, per your note. It matches the Re-verified: |
Summary
Completes the Polly-shape resilience suite by adding the two missing strategies, both pure-stdlib and additive (no new optional extra):
CircuitBreaker/AsyncCircuitBreaker— classic consecutive-failure circuit breaker (Polly pre-v8 default). Opens afterfailure_thresholdconsecutive counted failures, probes afterreset_timeout, closes aftersuccess_thresholdconsecutive half-open successes. A counted failure =NetworkError, httpwareTimeoutError, or aStatusErrorwhose status is infailure_status_codes(default: all 5xx). 4xx including 429 count as successes (429 = healthy-but-throttling). The transition logic lives once in a lock-free_CircuitBreakerState; the async wrapper relies on asyncio atomicity + a single-event-loop guard (carried fromAsyncBulkhead), the sync wrapper serializes with athreading.Lock.AsyncTimeout— async-only overall wall-clock deadline across the whole inner pipeline (most importantly across anAsyncRetryloop, which httpx2 can't bound). Usesasyncio.timeout(...).expired()to re-wrap only its own deadline ashttpware.TimeoutError, leaving inner timeouts untouched. No syncTimeout(sync Python can't cancel a blocking call mid-flight; httpx2 covers sync per-call timeouts).CircuitOpenError—ClientErrorsubclass withretry_after: float | None, raised when the breaker refuses a request.New observability events (stable surface):
circuit.opened,circuit.rejected,circuit.half_open,circuit.closed(loggerhttpware.circuit_breaker),timeout.exceeded(loggerhttpware.timeout).Docs (
resilience.md,errors.md, README) document the recommended (not enforced) chain order:AsyncTimeout → AsyncCircuitBreaker → AsyncBulkhead → AsyncRetry → terminal—AsyncBulkheadstays outsideAsyncRetry(one slot per logical call), consistent with the existing tested guidance.Spec:
planning/specs/2026-06-13-circuit-breaker-and-timeout-design.md· Plan:planning/plans/2026-06-13-circuit-breaker-and-timeout.md· Release notes:planning/releases/0.10.0.md.Test Plan
just lint-ciclean (ruff format/check, ty)just test— 555 passed, 100% coverage (verified stable across 10 runs; property-test coverage made deterministic)reset_timeout,nextis never forwardedCircuitOpenErrorpickle round-trip; cross-event-loop guard; 429/4xx-as-success; customfailure_status_codeshttpx2.MockTransport🤖 Generated with Claude Code