Hi π β another find from our circuit-breaker alpha evaluation. This one's a small tweak we'd love to see while it's still alpha: it would open the utility up to use cases that are currently blocked β concurrent, in-process (threaded) fan-out to a shared circuit. Details below.
Expected Behaviour
On recovery, the breaker elects a single prober β exactly one request tests the recovering backend, which (per the docs) "stops a thundering herd of every environment hammering a recovering backend at once." And the failure counter trips at exactly failure_threshold.
Current Behaviour
Under concurrent threads sharing one circuit, neither holds. The single-prober election keys on a per-process id, so all threads in a process share the owner: after one thread wins the half-open election, its sibling threads pass the half_open_owner == <id> check too and also probe. We saw two probes for a single recovery window β the exact thundering herd the election exists to prevent. (Separately, the in-memory failure counter's increment isn't atomic, so it can overshoot failure_threshold across threads.)
We hit this evaluating the breaker around an SMTP send behind a worker-thread pool. We recognise the utility is modelled per-execution-environment (single-threaded under the Lambda model), so this may be considered by-design β but the protection silently fails under the natural threaded use, and nothing in the docs signals the assumption.
Possible Solution
Make the probe-owner id per-thread: the existing DynamoDB conditional election then elects a single thread across all threads and processes β cross-process coordination is unchanged (it's the DDB write; per-thread granularity just lets the same write disambiguate threads). Lock the in-memory counter increments, and guard the persistence's per-call state.
No boto3 change needed β the persistence already uses the low-level client, so the heaviest part of Idempotency's own thread-safety fix (#1899) is already done here. Thread safety has been added across the toolkit over time (Idempotency #1899, Logger thread-safe keys #5141, with CI for it #6889), so this keeps the breaker in step. If instead the single-threaded assumption is intended to stay, at minimum the docs should say so β a "not thread-safe within a process" note like Logger's append_keys warning.
Tests we'd add (fitting the existing thread-safe CI patterns, #6889):
- N threads sharing one circuit β exactly one probe per recovery window, regardless of thread count.
- N threads driving failures β the circuit trips at exactly
failure_threshold, with no overshoot from racing increments.
- The same guarantees hold across processes as well as threads β one election spanning both.
Happy to open a PR with the per-thread election, the locks, and these tests.
Steps to Reproduce
- Share one circuit across N worker threads in a single process.
- Make the guarded downstream fail so the circuit trips OPEN.
- Keep the threads calling; after
recovery_timeout, more than one thread runs the half-open probe concurrently β multiple HALF_OPEN β OPEN reopens for one recovery window (and the failure counter can trip a few over failure_threshold).
Environment
- Powertools for AWS Lambda (Python):
3.31.0 β utility circuit_breaker_alpha
- Runtime: Python 3.14 (not Lambda-specific β reproduces in any multi-threaded process)
- Packaging: PyPi
Hi π β another find from our circuit-breaker alpha evaluation. This one's a small tweak we'd love to see while it's still alpha: it would open the utility up to use cases that are currently blocked β concurrent, in-process (threaded) fan-out to a shared circuit. Details below.
Expected Behaviour
On recovery, the breaker elects a single prober β exactly one request tests the recovering backend, which (per the docs) "stops a thundering herd of every environment hammering a recovering backend at once." And the failure counter trips at exactly
failure_threshold.Current Behaviour
Under concurrent threads sharing one circuit, neither holds. The single-prober election keys on a per-process id, so all threads in a process share the owner: after one thread wins the half-open election, its sibling threads pass the
half_open_owner == <id>check too and also probe. We saw two probes for a single recovery window β the exact thundering herd the election exists to prevent. (Separately, the in-memory failure counter's increment isn't atomic, so it can overshootfailure_thresholdacross threads.)We hit this evaluating the breaker around an SMTP send behind a worker-thread pool. We recognise the utility is modelled per-execution-environment (single-threaded under the Lambda model), so this may be considered by-design β but the protection silently fails under the natural threaded use, and nothing in the docs signals the assumption.
Possible Solution
Make the probe-owner id per-thread: the existing DynamoDB conditional election then elects a single thread across all threads and processes β cross-process coordination is unchanged (it's the DDB write; per-thread granularity just lets the same write disambiguate threads). Lock the in-memory counter increments, and guard the persistence's per-call state.
No boto3 change needed β the persistence already uses the low-level client, so the heaviest part of Idempotency's own thread-safety fix (#1899) is already done here. Thread safety has been added across the toolkit over time (Idempotency #1899, Logger thread-safe keys #5141, with CI for it #6889), so this keeps the breaker in step. If instead the single-threaded assumption is intended to stay, at minimum the docs should say so β a "not thread-safe within a process" note like Logger's
append_keyswarning.Tests we'd add (fitting the existing thread-safe CI patterns, #6889):
failure_threshold, with no overshoot from racing increments.Happy to open a PR with the per-thread election, the locks, and these tests.
Steps to Reproduce
recovery_timeout, more than one thread runs the half-open probe concurrently β multipleHALF_OPEN β OPENreopens for one recovery window (and the failure counter can trip a few overfailure_threshold).Environment
3.31.0β utilitycircuit_breaker_alpha