You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CircuitBreaker.force_open() / force_closed() — manual control #118
src/httpware/middleware/resilience/circuit_breaker.py has no manual override. Polly calls this ManualControl: a way for an operator to pin the breaker open (shed all load to a backend known to
be sick) or pin it closed (ignore the breaker during a controlled test).
State of the surrounding work
The rest of the control surface is shipped and this is the only piece left. 0.13.0 added the opt-in
time-based failure-rate mode; 0.14.0 added the read-only state property and the public CircuitState enum. The original decision was that the control surface would be events-only — no state, no reset(), no isolate(). Half of that has since been reversed by the read-only state property, deliberately: reading state is an observability affordance and cannot desync
anything. Writing state is the half that was not reversed.
Why it is deferred rather than declined
It is the genuinely YAGNI half for an HTTP client, where the operator who wants to stop sending
requests can simply stop sending requests — unlike a server-side breaker, the caller owns the
call site. But it is not wrong in principle, so it is demand-gated rather than rejected. If it is
ever declined outright rather than built, that belongs in docs/adr/ as a rejected alternative,
not here.
Do not regress
Whatever shape this takes, preserve httpware's HTTP-native failure classification: 429 and the rest
of 4xx count as circuit-breaker successes by default. That is docs/adr/0004-4xx-does-not-trip-the-circuit-breaker.md, and it is what puts this breaker ahead of
the generic predicate-over-"did it fail" breakers it is measured against. A manual-control API must
not become a back door that reintroduces "any failure trips it".
Revisit trigger
A concrete user report naming an operational situation that failure_status_codes, the read-only state property, and not sending the request cannot together address.
The per-instance primitive does not solve the operational problem it is named for. A service runs as several replicas, each with its own breaker state. force_open() on one replica pins one replica; an admin endpoint that calls it reaches whichever pod the load balancer picked, and a pod that starts afterwards comes up unpinned. The in-process libraries this was modelled on have the same limitation and do not solve it:
Polly's CircuitBreakerManualControl is an in-process object; the docs say nothing about replicas.
Resilience4j has FORCED_OPEN / DISABLED / METRICS_ONLY and a Spring Boot actuator write operation (/actuator/circuitbreakers/{name} with updateState), which changes one JVM.
The two approaches that do work fleet-wide are out of scope for httpware:
Config-driven pins (Hystrix circuitBreaker.forceOpen / forceClosed as dynamic Archaius properties read on every admit). The library defines a knob and a central config system delivers it. For an HTTP client this collapses into what callers can already do: read the same flag before sending and not send. A breaker method adds a reconcile loop on every replica and nothing the flag check lacks.
Shared state in an external store (pybreaker CircuitRedisStorage, stoplight lock(RED) on a Redis data store). That puts a network round trip inside every admit decision and adds a failure mode when the store is down. _CircuitBreakerState is documented as performing no I/O inside a transition, and that is the right contract for a thin wrapper.
What stays.failure_status_codes, the read-only state property, and the events remain the control surface. The HTTP-native classification in docs/adr/0003-4xx-does-not-trip-the-circuit-breaker.md is untouched.
Per this issue's own note, the decline is recorded in docs/adr/0004-circuit-breaker-scope.md (PR #128).
Activity
lesnik512 commentedon Sep 19, 2026
Closing as declined rather than deferred.
The per-instance primitive does not solve the operational problem it is named for. A service runs as several replicas, each with its own breaker state.
force_open()on one replica pins one replica; an admin endpoint that calls it reaches whichever pod the load balancer picked, and a pod that starts afterwards comes up unpinned. The in-process libraries this was modelled on have the same limitation and do not solve it:CircuitBreakerManualControlis an in-process object; the docs say nothing about replicas.FORCED_OPEN/DISABLED/METRICS_ONLYand a Spring Boot actuator write operation (/actuator/circuitbreakers/{name}withupdateState), which changes one JVM.The two approaches that do work fleet-wide are out of scope for httpware:
circuitBreaker.forceOpen/forceClosedas dynamic Archaius properties read on every admit). The library defines a knob and a central config system delivers it. For an HTTP client this collapses into what callers can already do: read the same flag before sending and not send. A breaker method adds a reconcile loop on every replica and nothing the flag check lacks.CircuitRedisStorage, stoplightlock(RED)on a Redis data store). That puts a network round trip inside every admit decision and adds a failure mode when the store is down._CircuitBreakerStateis documented as performing no I/O inside a transition, and that is the right contract for a thin wrapper.What stays.
failure_status_codes, the read-onlystateproperty, and the events remain the control surface. The HTTP-native classification indocs/adr/0003-4xx-does-not-trip-the-circuit-breaker.mdis untouched.Per this issue's own note, the decline is recorded in
docs/adr/0004-circuit-breaker-scope.md(PR #128).docs(adr): compress to 10 records in the domain-modeling format (#128)