The project is in a healthy, maintained state
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 License

Minimal circuit breaker — three states (closed/open/half-open), configurable thresholds, error class filtering, event callbacks, metrics, and exponential backoff.

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-circuit"

Then run:

bundle install

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.

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...") }

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)
#reset! Force back to closed
#metrics Returns { success_count:, failure_count:, rejected_count:, state_changes: [] }
#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      # Run tests
bundle exec rubocop    # Check code style

License

MIT