Class: BrainzLab::Utilities::CircuitBreaker

Inherits:
Object
  • Object
show all
Defined in:
lib/brainzlab/utilities/circuit_breaker.rb

Overview

Circuit breaker pattern implementation for resilient external calls Integrates with Flux for metrics and Reflex for error tracking

States:

  • :closed - Normal operation, requests pass through

  • :open - Failing, requests are rejected immediately

  • :half_open - Testing, limited requests allowed to check recovery

Examples:

Basic usage

breaker = BrainzLab::Utilities::CircuitBreaker.new(
  name: "external_api",
  failure_threshold: 5,
  recovery_timeout: 30
)

breaker.call do
  external_api.request
end

With fallback

breaker.call(fallback: -> { cached_value }) do
  external_api.request
end

Defined Under Namespace

Classes: CircuitOpenError

Constant Summary collapse

STATES =
%i[closed open half_open].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, failure_threshold: 5, success_threshold: 2, recovery_timeout: 30, timeout: nil, exclude_exceptions: []) ⇒ CircuitBreaker

Returns a new instance of CircuitBreaker.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 34

def initialize(name:, failure_threshold: 5, success_threshold: 2, recovery_timeout: 30, timeout: nil,
               exclude_exceptions: [])
  @name = name
  @failure_threshold = failure_threshold
  @success_threshold = success_threshold
  @recovery_timeout = recovery_timeout
  @timeout = timeout
  @exclude_exceptions = exclude_exceptions

  @state = :closed
  @failure_count = 0
  @success_count = 0
  @last_failure_at = nil
  @mutex = Mutex.new
end

Instance Attribute Details

#failure_countObject (readonly)

Returns the value of attribute failure_count.



32
33
34
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 32

def failure_count
  @failure_count
end

#last_failure_atObject (readonly)

Returns the value of attribute last_failure_at.



32
33
34
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 32

def last_failure_at
  @last_failure_at
end

#nameObject (readonly)

Returns the value of attribute name.



32
33
34
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 32

def name
  @name
end

#stateObject (readonly)

Returns the value of attribute state.



32
33
34
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 32

def state
  @state
end

#success_countObject (readonly)

Returns the value of attribute success_count.



32
33
34
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 32

def success_count
  @success_count
end

Class Method Details

.call(name, **options) ⇒ Object



127
128
129
130
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 127

def call(name, **options, &)
  breaker = get(name) || register(name, **options)
  breaker.call(**options.slice(:fallback), &)
end

.get(name) ⇒ Object



119
120
121
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 119

def get(name)
  registry[name.to_s]
end

.register(name) ⇒ Object



123
124
125
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 123

def register(name, **)
  registry[name.to_s] = new(name: name, **)
end

.registryObject



115
116
117
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 115

def registry
  @registry ||= {}
end

.reset_all!Object



132
133
134
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 132

def reset_all!
  registry.each_value(&:reset!)
end

.status_allObject



136
137
138
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 136

def status_all
  registry.transform_values(&:status)
end

Instance Method Details

#available?Boolean

Check if circuit is allowing requests

Returns:

  • (Boolean)


108
109
110
111
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 108

def available?
  check_state_transition!
  @state != :open
end

#call(fallback: nil) ⇒ Object

Execute a block with circuit breaker protection



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 51

def call(fallback: nil, &)
  check_state_transition!

  case @state
  when :open
    track_rejected
    raise CircuitOpenError.new(@name, failure_count: @failure_count, last_failure_at: @last_failure_at) unless fallback

    fallback.respond_to?(:call) ? fallback.call : fallback

  when :closed, :half_open
    execute_with_protection(fallback, &)
  end
end

#force_state!(new_state) ⇒ Object

Force the circuit to a specific state



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 67

def force_state!(new_state)
  unless STATES.include?(new_state)
    raise BrainzLab::ValidationError.new(
      "Invalid circuit breaker state: #{new_state}",
      hint: "Valid states are: #{STATES.join(', ')}",
      code: 'invalid_circuit_state',
      field: 'state',
      context: { provided: new_state, valid_values: STATES }
    )
  end

  @mutex.synchronize do
    @state = new_state
    @failure_count = 0 if new_state == :closed
    @success_count = 0 if new_state == :half_open
  end

  track_state_change(new_state)
end

#reset!Object

Reset the circuit breaker



88
89
90
91
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 88

def reset!
  force_state!(:closed)
  @last_failure_at = nil
end

#statusObject

Get circuit status



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/brainzlab/utilities/circuit_breaker.rb', line 94

def status
  {
    name: @name,
    state: @state,
    failure_count: @failure_count,
    success_count: @success_count,
    failure_threshold: @failure_threshold,
    success_threshold: @success_threshold,
    last_failure_at: @last_failure_at,
    recovery_timeout: @recovery_timeout
  }
end