Class: Parse::LiveQuery::CircuitBreaker

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/live_query/circuit_breaker.rb

Overview

Circuit breaker pattern for connection failure handling.

Prevents repeated connection attempts when the server is unavailable, allowing time for recovery before retrying.

States:

  • :closed - Normal operation, requests allowed

  • :open - Too many failures, requests blocked

  • :half_open - Testing if service recovered

Examples:

breaker = CircuitBreaker.new(failure_threshold: 5, reset_timeout: 60.0)

if breaker.allow_request?
  begin
    connect_to_server
    breaker.record_success
  rescue => e
    breaker.record_failure
  end
else
  # Circuit is open, wait before retrying
end

Constant Summary collapse

STATES =

Valid circuit states

[:closed, :open, :half_open].freeze
DEFAULT_FAILURE_THRESHOLD =

Default number of failures before opening circuit

5
DEFAULT_RESET_TIMEOUT =

Default seconds before transitioning from open to half_open

60.0
DEFAULT_HALF_OPEN_REQUESTS =

Default number of successful requests in half_open before closing

1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(failure_threshold: DEFAULT_FAILURE_THRESHOLD, reset_timeout: DEFAULT_RESET_TIMEOUT, half_open_requests: DEFAULT_HALF_OPEN_REQUESTS, on_state_change: nil) ⇒ CircuitBreaker

Create a new circuit breaker

Parameters:

  • failure_threshold (Integer) (defaults to: DEFAULT_FAILURE_THRESHOLD)

    failures before opening circuit

  • reset_timeout (Float) (defaults to: DEFAULT_RESET_TIMEOUT)

    seconds before testing recovery

  • half_open_requests (Integer) (defaults to: DEFAULT_HALF_OPEN_REQUESTS)

    successes needed to close

  • on_state_change (Proc, nil) (defaults to: nil)

    callback for state changes



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/parse/live_query/circuit_breaker.rb', line 68

def initialize(failure_threshold: DEFAULT_FAILURE_THRESHOLD,
               reset_timeout: DEFAULT_RESET_TIMEOUT,
               half_open_requests: DEFAULT_HALF_OPEN_REQUESTS,
               on_state_change: nil)
  @failure_threshold = failure_threshold
  @reset_timeout = reset_timeout
  @half_open_requests = half_open_requests
  @on_state_change = on_state_change

  @monitor = Monitor.new
  @state = :closed
  @failure_count = 0
  @success_count = 0
  @last_failure_at = nil
end

Instance Attribute Details

#failure_countInteger (readonly)

Returns number of consecutive failures.

Returns:

  • (Integer)

    number of consecutive failures



49
50
51
# File 'lib/parse/live_query/circuit_breaker.rb', line 49

def failure_count
  @failure_count
end

#failure_thresholdInteger (readonly)

Returns failure threshold before opening.

Returns:

  • (Integer)

    failure threshold before opening



58
59
60
# File 'lib/parse/live_query/circuit_breaker.rb', line 58

def failure_threshold
  @failure_threshold
end

#last_failure_atTime? (readonly)

Returns when the last failure occurred.

Returns:

  • (Time, nil)

    when the last failure occurred



55
56
57
# File 'lib/parse/live_query/circuit_breaker.rb', line 55

def last_failure_at
  @last_failure_at
end

#reset_timeoutFloat (readonly)

Returns seconds before half_open transition.

Returns:

  • (Float)

    seconds before half_open transition



61
62
63
# File 'lib/parse/live_query/circuit_breaker.rb', line 61

def reset_timeout
  @reset_timeout
end

#stateSymbol (readonly)

Returns current state (:closed, :open, :half_open).

Returns:

  • (Symbol)

    current state (:closed, :open, :half_open)



46
47
48
# File 'lib/parse/live_query/circuit_breaker.rb', line 46

def state
  @state
end

#success_countInteger (readonly)

Returns number of successful requests in half_open.

Returns:

  • (Integer)

    number of successful requests in half_open



52
53
54
# File 'lib/parse/live_query/circuit_breaker.rb', line 52

def success_count
  @success_count
end

Instance Method Details

#allow_request?Boolean

Note:

Thread-safe. Callbacks are invoked outside the synchronized block.

Check if a request is allowed

Returns:

  • (Boolean)


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/parse/live_query/circuit_breaker.rb', line 87

def allow_request?
  state_change = nil

  result = @monitor.synchronize do
    case @state
    when :closed
      true
    when :open
      if Time.now - @last_failure_at >= @reset_timeout
        state_change = transition_to_internal(:half_open)
        true
      else
        false
      end
    when :half_open
      @success_count < @half_open_requests
    end
  end

  # Invoke callback outside synchronized block to prevent deadlocks
  notify_state_change(state_change) if state_change

  result
end

#closed?Boolean

Check if circuit is closed (allowing requests)

Returns:

  • (Boolean)


179
180
181
# File 'lib/parse/live_query/circuit_breaker.rb', line 179

def closed?
  @monitor.synchronize { @state == :closed }
end

#half_open?Boolean

Check if circuit is half_open (testing recovery)

Returns:

  • (Boolean)


185
186
187
# File 'lib/parse/live_query/circuit_breaker.rb', line 185

def half_open?
  @monitor.synchronize { @state == :half_open }
end

#infoHash

Get circuit breaker info as hash

Returns:



201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/parse/live_query/circuit_breaker.rb', line 201

def info
  @monitor.synchronize do
    {
      state: @state,
      failure_count: @failure_count,
      success_count: @success_count,
      failure_threshold: @failure_threshold,
      reset_timeout: @reset_timeout,
      last_failure_at: @last_failure_at,
      time_until_half_open: time_until_half_open,
    }
  end
end

#open?Boolean

Check if circuit is open (blocking requests)

Returns:

  • (Boolean)


173
174
175
# File 'lib/parse/live_query/circuit_breaker.rb', line 173

def open?
  @monitor.synchronize { @state == :open }
end

#record_failurevoid

Note:

Thread-safe. Callbacks are invoked outside the synchronized block.

This method returns an undefined value.

Record a failed request



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/parse/live_query/circuit_breaker.rb', line 138

def record_failure
  state_change = nil

  @monitor.synchronize do
    @failure_count += 1
    @last_failure_at = Time.now

    case @state
    when :closed
      if @failure_count >= @failure_threshold
        Logging.warn("Circuit breaker opening", failures: @failure_count)
        state_change = transition_to_internal(:open)
      end
    when :half_open
      Logging.warn("Circuit breaker re-opening from half_open")
      state_change = transition_to_internal(:open)
    end
  end

  # Invoke callback outside synchronized block to prevent deadlocks
  notify_state_change(state_change) if state_change
end

#record_successvoid

Note:

Thread-safe. Callbacks are invoked outside the synchronized block.

This method returns an undefined value.

Record a successful request



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/parse/live_query/circuit_breaker.rb', line 115

def record_success
  state_change = nil

  @monitor.synchronize do
    case @state
    when :half_open
      @success_count += 1
      if @success_count >= @half_open_requests
        Logging.info("Circuit breaker closing after successful recovery")
        state_change = reset_internal!
      end
    when :closed
      @failure_count = 0
    end
  end

  # Invoke callback outside synchronized block to prevent deadlocks
  notify_state_change(state_change) if state_change
end

#reset!void

Note:

Thread-safe. Callbacks are invoked outside the synchronized block.

This method returns an undefined value.

Reset the circuit breaker to closed state



164
165
166
167
168
169
# File 'lib/parse/live_query/circuit_breaker.rb', line 164

def reset!
  state_change = @monitor.synchronize { reset_internal! }

  # Invoke callback outside synchronized block to prevent deadlocks
  notify_state_change(state_change) if state_change
end

#time_until_half_openFloat?

Seconds until circuit transitions to half_open

Returns:

  • (Float, nil)

    nil if not open



191
192
193
194
195
196
197
# File 'lib/parse/live_query/circuit_breaker.rb', line 191

def time_until_half_open
  @monitor.synchronize do
    return nil unless @state == :open && @last_failure_at
    remaining = @reset_timeout - (Time.now - @last_failure_at)
    [remaining, 0].max
  end
end