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
fix(circuit_breaker): make probe election per-thread and synchronize local counters
The half-open probe owner id was minted once per process, so every thread
in a process passed the owner check once any sibling won the election and
all of them probed the recovering backend. The id is now a per-thread
uuid (pid-aware for forked workers); the existing DynamoDB conditional
write then elects one prober across threads and processes with no
protocol change.
The in-memory failure/success counters and observed-state map are now
guarded by a lock, with threshold crossings detected atomically so a
trip is persisted exactly once. Persistence settings are keyed per
circuit name instead of living as shared instance attributes, and the
local cache no longer raises into the protected call on concurrent
expiry.
Copy file name to clipboardExpand all lines: docs/utilities/circuit_breaker.md
+11Lines changed: 11 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,6 +20,7 @@ The circuit breaker utility stops sending traffic to an unhealthy downstream dep
20
20
* Hands rejected requests to an `on_circuit_open` callback so you decide what happens next (buffer, drop, return a cached value)
21
21
* Tests recovery with an explicit half-open probe rather than blindly retrying everything at once
22
22
* Shares circuit state across execution environments via Amazon DynamoDB
23
+
* Safe under concurrency: one half-open probe across all threads and execution environments, and synchronized failure counting
23
24
* Keeps the healthy path write-free: failures are counted in memory and only persisted on a state transition
24
25
25
26
## Terminology
@@ -182,6 +183,16 @@ Passing both raises `CircuitBreakerConfigError`. An exception that doesn't count
182
183
183
184
After `recovery_timeout` seconds, the circuit moves to `HALF_OPEN` and elects a **single** execution environment (via a conditional DynamoDB write) to run a probe. If `success_threshold` consecutive probes succeed, the circuit closes; a single failing probe reopens it. This stops a thundering herd of every environment hammering a recovering backend at once.
184
185
186
+
!!! note "Thread safety"
187
+
The utility is safe to share across threads: within a multi-threaded environment the probe election picks a single
188
+
thread, so the single-prober guarantee spans threads as well as environments, and the in-memory failure counter is
189
+
synchronized. Single-threaded functions (the normal Lambda model) are unaffected. `on_circuit_open` and
190
+
`on_transition` hooks may run concurrently from multiple threads.
191
+
192
+
Probe ownership belongs to the thread that won the election. If that thread never runs the circuit again (for
193
+
example, a thread-per-request worker pool), recovery waits for the probe lease to expire before another thread or
194
+
environment takes over.
195
+
185
196
### State coordination across environments
186
197
187
198
The consecutive-failure counter lives in memory per execution environment, so a healthy circuit performs **no writes**. Only when an environment reaches `failure_threshold` does it persist `OPEN`. The shared state is cached locally for `local_cache_max_age` seconds to avoid a read per invocation. A cache miss (cold start or expired entry) forces a read-through before routing.
0 commit comments