Low commit activity in last 3 years
Circuit breaker pattern with closed, open, and half-open states, configurable failure thresholds, timeout, error class filtering, event callbacks, metrics, and exponential backoff.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-circuit

Tests Gem Version Last updated

Minimal circuit breaker with configurable thresholds, error filtering, and exponential backoff

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-circuit"

Or install directly:

gem install philiprehberger-circuit

Usage

require "philiprehberger/circuit"

breaker = Philiprehberger::Circuit::Breaker.new(:payment_api, threshold: 5, timeout: 30)

breaker.call do
  PaymentGateway.charge(amount)
end
# After 5 failures -> circuit opens -> raises OpenError for 30s
# After 30s -> half-open -> allows one probe request

Fallback

Return a fallback value when the circuit is open instead of raising:

breaker.call(fallback: -> { :queued }) do
  PaymentGateway.charge(amount)
end

Error Filtering

breaker = Philiprehberger::Circuit::Breaker.new(:api, error_classes: [Net::TimeoutError, Net::OpenTimeout])

Half-Open Control

Limit the number of probe requests allowed in half-open state:

breaker = Philiprehberger::Circuit::Breaker.new(:api, half_open_requests: 2)
# Only 2 requests pass through in half-open; the rest are rejected

Metrics

Access counters for monitoring:

breaker.metrics
# => { success_count: 42, failure_count: 3, rejected_count: 7, state_changes: [...] }

Each state change entry contains { from:, to:, at: } with the transition timestamp.

Last Failure

Inspect the most recent failure recorded by the breaker for diagnostics. Returns nil when no failure has happened since the last reset.

breaker.call { raise Net::OpenTimeout, 'connection refused' } rescue nil

breaker.last_failure
# => { at: 2026-04-28 14:32:08 +0000, error_class: Net::OpenTimeout, message: "connection refused" }

last_failure is captured for both closed-state failures and half-open probe failures, and is cleared by #metrics_reset!.

Resetting metrics

Zero the counters and clear the state-change log without touching the current state (useful for periodic metric windows):

breaker.metrics_reset!
breaker.metrics
# => { success_count: 0, failure_count: 0, rejected_count: 0, state_changes: [] }
# breaker.state is unchanged

Exponential Backoff

Use exponential backoff for the open-to-half-open timeout:

breaker = Philiprehberger::Circuit::Breaker.new(
  :api,
  timeout_strategy: :exponential,
  base_timeout: 10,
  max_timeout: 300
)
# Timeouts double on each consecutive open: 20s, 40s, 80s, ... capped at 300s
# Resets to base_timeout when circuit closes

Event Callbacks

breaker.on_open { AlertService.notify("Circuit opened!") }
breaker.on_close { AlertService.notify("Circuit recovered") }
breaker.on_half_open { Logger.info("Circuit probing...") }

Force trip

Administratively force the circuit into the open state, regardless of current state or failure count:

breaker.trip!
breaker.state # => :open

# Subsequent calls short-circuit like any other open-state call
breaker.call(fallback: -> { :queued }) { PaymentGateway.charge(amount) }
# => :queued

State Predicates

Idiomatic predicate methods for testing the current state without comparing against state symbols:

breaker.closed?    # => true (fresh breaker)
breaker.open?      # => false
breaker.half_open? # => false

breaker.trip!
breaker.open?      # => true
breaker.closed?    # => false

Manual Control

Force the breaker into a fixed state for maintenance windows. While forced, automatic state transitions (failure-based opening, timeout-based half-open probes) are suspended until reset! is called:

# Force open during a maintenance window — rejects all calls regardless of timeout
breaker.force_open!
breaker.forced? # => true
breaker.call { :anything } # => raises OpenError

# Force closed to bypass the breaker entirely — accepts all calls, ignores failures
breaker.force_closed!
breaker.forced? # => true

# Clear the forced flag and return to normal automatic behavior
breaker.reset!
breaker.forced? # => false

Reset Callback

Hook into every state transition for logging or alerting:

breaker.on_reset do |from_state, to_state|
  Logger.info("Circuit #{breaker.name} transitioned from #{from_state} to #{to_state}")
end

API

Method Description
Breaker.new(name, **opts) Create a breaker (see options below)
#call(fallback: nil) { block } Execute with circuit protection
#state Current state (:closed, :open, :half_open)
#closed? / #open? / #half_open? State predicates
#reset! Force back to closed
#trip! Force open (administrative trip)
#force_open! Force open and suspend automatic transitions until #reset!
#force_closed! Force closed and suspend automatic transitions until #reset!
#forced? Whether the breaker is in a manually forced state
#metrics Returns { success_count:, failure_count:, rejected_count:, state_changes: [] }
#last_failure Returns { at:, error_class:, message: } for the most recent failure (or nil)
#metrics_reset! Zero counters and clear state-change log without altering current state
#on_open { } Callback when circuit opens
#on_close { } Callback when circuit closes
#on_half_open { } Callback when circuit enters half-open
#on_reset { |from, to| } Callback on every state transition

Constructor Options

Option Default Description
threshold: 5 Failures before opening
timeout: 30 Seconds before trying half-open (fixed strategy)
error_classes: [StandardError] Exception classes to count
half_open_requests: 1 Max probe requests in half-open
timeout_strategy: :fixed :fixed or :exponential
base_timeout: timeout Base timeout for exponential backoff
max_timeout: base * 32 Max timeout cap for exponential backoff

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT