Class: Pgbus::Client::ConnectionHealth
- Inherits:
-
Object
- Object
- Pgbus::Client::ConnectionHealth
- Defined in:
- lib/pgbus/client/connection_health.rb
Overview
In-memory, process-local circuit breaker for database-down conditions.
Distinct from Pgbus::CircuitBreaker (which is per-queue and persists its pause state in the database — useless when the database itself is down). This latch is a single shared object owned by Pgbus::Client that trips on raw connection failures across any operation, so a fleet of workers stops hammering a dead database and flooding the error tracker for the whole outage (issue #197).
State machine:
closed -> open after OPEN_THRESHOLD consecutive ConnectionErrors
open -> half_open once the backoff window elapses (admits ONE probe)
half_open -> closed on a successful probe (resets backoff)
half_open -> open on a failed probe (backoff doubles, capped)
Thresholds are constants, not configuration, mirroring CircuitBreaker: the values rarely need tuning and exposing them never proved useful.
Thread safety: a single Mutex serializes every state transition. The guarded operation itself runs outside the lock (only the gate decision and the outcome recording are inside), so a slow probe never blocks other worker threads from failing fast.
Constant Summary collapse
- OPEN_THRESHOLD =
Consecutive ConnectionErrors that trip the breaker from closed to open.
5- BASE_BACKOFF =
Initial backoff (seconds) on the first trip. Doubles on each re-open.
1.0- MAX_BACKOFF =
Cap on the exponential backoff (seconds). After ~6 re-opens the curve plateaus here so a perpetually-down database stops probing more than once a minute.
60.0
Instance Method Summary collapse
- #closed? ⇒ Boolean
-
#initialize(clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }, on_open: nil, on_close: nil) ⇒ ConnectionHealth
constructor
A new instance of ConnectionHealth.
- #open? ⇒ Boolean
-
#run_guarded ⇒ Object
Gate an operation through the breaker.
Constructor Details
#initialize(clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }, on_open: nil, on_close: nil) ⇒ ConnectionHealth
Returns a new instance of ConnectionHealth.
39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/pgbus/client/connection_health.rb', line 39 def initialize(clock: -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }, on_open: nil, on_close: nil) @clock = clock @on_open = on_open @on_close = on_close @mutex = Mutex.new @state = :closed @failure_count = 0 @trip_count = 0 @open_until = nil end |
Instance Method Details
#closed? ⇒ Boolean
76 77 78 |
# File 'lib/pgbus/client/connection_health.rb', line 76 def closed? @mutex.synchronize { @state == :closed } end |
#open? ⇒ Boolean
80 81 82 |
# File 'lib/pgbus/client/connection_health.rb', line 80 def open? @mutex.synchronize { @state == :open } end |
#run_guarded ⇒ Object
Gate an operation through the breaker.
- closed / half-open-probe-admitted: yields, records the outcome.
- open (window not elapsed) or half-open (probe already in flight): raises Pgbus::ConnectionCircuitOpenError without yielding — no pool checkout, no ConnectionError, no ErrorReporter noise.
The breaker never swallows an error — every exception propagates. A ConnectionError in the closed state counts toward the trip threshold; a failure of the single half-open probe (on ANY error) re-opens the breaker; any other closed-state error is not counted.
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/pgbus/client/connection_health.rb', line 62 def run_guarded admitted = admit_or_raise # :run (was closed) or :probe (was open) begin result = yield rescue StandardError => e record_failure(admitted, e) raise end record_success(admitted) result end |