Class: Woods::Resilience::CircuitBreaker

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/resilience/circuit_breaker.rb

Overview

Circuit breaker pattern for protecting external service calls.

Tracks failures and transitions between three states:

  • :closed — normal operation, calls pass through
  • :open — too many failures, calls are rejected immediately
  • :half_open — testing recovery, one call is allowed through

Examples:

Basic usage

breaker = CircuitBreaker.new(threshold: 5, reset_timeout: 60)
result = breaker.call { external_service.request }

With retry logic

breaker = CircuitBreaker.new(threshold: 3, reset_timeout: 30)
begin
  breaker.call { api.embed(text) }
rescue CircuitOpenError
  # Service is down, use fallback
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(threshold: 5, reset_timeout: 60, success_threshold: 1) ⇒ CircuitBreaker

Returns a new instance of CircuitBreaker.

Parameters:

  • threshold (Integer) (defaults to: 5)

    Number of consecutive failures before opening the circuit

  • reset_timeout (Numeric) (defaults to: 60)

    Seconds to wait before transitioning from open to half_open

  • success_threshold (Integer) (defaults to: 1)

    Number of consecutive successful probes required to close the circuit from half_open (default 1). Higher values reduce flapping when a recovering service is still intermittently failing.



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/woods/resilience/circuit_breaker.rb', line 42

def initialize(threshold: 5, reset_timeout: 60, success_threshold: 1)
  @threshold = threshold
  @reset_timeout = reset_timeout
  @success_threshold = success_threshold
  @state = :closed
  @failure_count = 0
  @success_count = 0
  @last_failure_time = nil
  @half_open_probe_in_flight = false
  @mutex = Mutex.new
end

Instance Attribute Details

#stateSymbol (readonly)

Returns Current state — :closed, :open, or :half_open.

Returns:

  • (Symbol)

    Current state — :closed, :open, or :half_open



35
36
37
# File 'lib/woods/resilience/circuit_breaker.rb', line 35

def state
  @state
end

Instance Method Details

#call { ... } ⇒ Object

Execute a block through the circuit breaker.

In half_open, only a SINGLE probe is admitted at a time: concurrent calls are rejected with Woods::Resilience::CircuitOpenError until the probe resolves. This prevents a thundering herd of probes against a still-recovering service, and — because probes can't overlap — removes the race where a slow probe's success would wipe failures recorded by a concurrent one.

Yields:

  • The block to execute

Returns:

  • (Object)

    The return value of the block

Raises:

  • (CircuitOpenError)

    if the circuit is open, or half_open with a probe already in flight

  • (StandardError)

    re-raises any error from the block



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/woods/resilience/circuit_breaker.rb', line 66

def call(&block)
  probing = admit_call!

  begin
    result = block.call
  rescue CircuitOpenError
    # A nested breaker tripped — release our probe slot but don't count
    # it as this breaker's own failure.
    @mutex.synchronize { @half_open_probe_in_flight = false if probing }
    raise
  rescue StandardError => e
    @mutex.synchronize do
      @half_open_probe_in_flight = false if probing
      record_failure(probing)
    end
    raise e
  end

  @mutex.synchronize do
    @half_open_probe_in_flight = false if probing
    record_success(probing)
  end
  result
end