A circuit breaker for Ruby. It wraps calls to flaky dependencies and fails fast while they are down, instead of piling up timeouts.
Instead of logging every request, it keeps success and failure counters in fixed time buckets, so storage stays constant per circuit no matter the traffic. State changes go through compare and set operations, so it works across threads and processes without locks. Failures inside the breaker itself never reach your callers.
bundle add termicaRegister a circuit once, then run calls through it:
Termica.for(:payments)
Termica.run(:payments) do
http.post("/charge", body)
endrun returns the block's value. If the block raises, the error is re-raised unchanged and counted as a failure. Once the error rate crosses the threshold, the circuit opens and run raises Termica::CircuitOpenError without executing the block. After a cool off period a single probe request is let through. Enough consecutive probe successes close the circuit again.
Options are passed to Termica.for. All durations are integer seconds.
Termica.for(:payments, threshold: 0.3, cool_off_time: 120)| option | default | meaning |
|---|---|---|
window_size |
300 | how many seconds of history the error rate is calculated over |
threshold |
0.5 | error rate that opens the circuit |
minimum_requests |
10 | below this many requests in the window the circuit never opens, so a single failure can't trip an idle circuit |
cool_off_time |
60 | how long the circuit stays open before the first probe |
recovery_threshold |
5 | consecutive probe successes needed to close the circuit |
probe_timeout |
30 | a probe that hasn't resolved after this long counts as abandoned and can be re-claimed. Must be longer than the timeout of the operation itself |
skipped_errors |
[] |
error classes that are re-raised but not counted as failures, for example validation errors from the remote API |
The default backend keeps everything in memory, which is fine for a single process. To share circuit state across processes, use the Redis/Valkey backend. It takes anything that responds to with and yields a Redis client, such as a connection_pool:
pool = ConnectionPool.new(size: 5) { Redis.new(url: ENV["REDIS_URL"]) }
Termica.persistence_backend = Termica::Persistence::Valkey.new(pool)To get notified when a circuit opens, add notifiers. Each one must respond to notify(circuit, from, to, error):
Termica.notifiers = [MyLoggingNotifier.new]The breaker never lets its own failures (for example Redis being unreachable) reach your callers. It fails open, keeps running your blocks, and reports the problem through error_notifier:
Termica.error_notifier = ->(error) { Sentry.capture_exception(error) }After checking out the repo, run bin/setup to install dependencies. The tests need a local Redis and use database 15 (override with REDIS_URL). Run them with rake test.
The gem is available as open source under the terms of the MIT License.