Class: Attio::CircuitBreaker Private

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

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Circuit breaker pattern for fault tolerance

Examples:

Basic usage

breaker = CircuitBreaker.new(
  threshold: 5,
  timeout: 60,
  half_open_requests: 3
)

breaker.call do
  # API call that might fail
  client.records.list
end

Since:

  • 1.0.0

Defined Under Namespace

Classes: OpenCircuitError

Constant Summary collapse

CLOSED =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Circuit states

Since:

  • 1.0.0

:closed
OPEN =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0

:open
HALF_OPEN =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0

:half_open

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(threshold: 5, timeout: 60, half_open_requests: 3, exceptions: [StandardError]) ⇒ CircuitBreaker

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initialize circuit breaker

Parameters:

  • threshold (Integer) (defaults to: 5)

    Number of failures before opening circuit

  • timeout (Integer) (defaults to: 60)

    Seconds before attempting to close circuit

  • half_open_requests (Integer) (defaults to: 3)

    Successful requests needed to close circuit

  • exceptions (Array<Class>) (defaults to: [StandardError])

    Exception classes to catch

Since:

  • 1.0.0



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/attio/circuit_breaker.rb', line 34

def initialize(
  threshold: 5,
  timeout: 60,
  half_open_requests: 3,
  exceptions: [StandardError]
)
  @threshold = threshold
  @timeout = timeout
  @half_open_requests = half_open_requests
  @exceptions = exceptions

  @state = CLOSED
  @failure_count = 0
  @success_count = 0
  @last_failure_time = nil
  @half_open_successes = 0

  @mutex = Mutex.new
  @on_state_change = nil

  # Statistics
  @stats = {
    requests: 0,
    failures: 0,
    successes: 0,
    rejections: 0,
    state_changes: 0,
  }
end

Instance Attribute Details

#failure_countObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



25
26
27
# File 'lib/attio/circuit_breaker.rb', line 25

def failure_count
  @failure_count
end

#last_failure_timeObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



25
26
27
# File 'lib/attio/circuit_breaker.rb', line 25

def last_failure_time
  @last_failure_time
end

#on_state_changeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



26
27
28
# File 'lib/attio/circuit_breaker.rb', line 26

def on_state_change
  @on_state_change
end

#stateObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



25
26
27
# File 'lib/attio/circuit_breaker.rb', line 25

def state
  @state
end

#success_countObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



25
26
27
# File 'lib/attio/circuit_breaker.rb', line 25

def success_count
  @success_count
end

Instance Method Details

#allow_request?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check if circuit allows requests

Returns:

  • (Boolean)

    True if requests are allowed

Since:

  • 1.0.0



122
123
124
# File 'lib/attio/circuit_breaker.rb', line 122

def allow_request?
  @state != OPEN || can_attempt_reset?
end

#call { ... } ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Execute a block with circuit breaker protection

Yields:

  • Block to execute

Returns:

  • Result of the block

Raises:

Since:

  • 1.0.0



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/attio/circuit_breaker.rb', line 69

def call
  @mutex.synchronize do
    @stats[:requests] += 1

    case @state
    when OPEN
      if can_attempt_reset?
        transition_to(HALF_OPEN)
      else
        @stats[:rejections] += 1
        raise OpenCircuitError, "Circuit breaker is open (#{time_until_retry}s until retry)"
      end
    when HALF_OPEN
      # Allow limited requests through
      if @half_open_successes >= @half_open_requests
        # Already proven stable, close the circuit
        transition_to(CLOSED)
      end
    end
  end

  # Execute the block outside the mutex
  begin
    result = yield
    record_success
    result
  rescue *@exceptions => e
    record_failure
    raise e
  end
end

#can_attempt_reset?Boolean (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)

Since:

  • 1.0.0



188
189
190
191
192
# File 'lib/attio/circuit_breaker.rb', line 188

private def can_attempt_reset?
  return false unless @last_failure_time

  Time.now - @last_failure_time >= @timeout
end

#record_failureObject (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/attio/circuit_breaker.rb', line 172

private def record_failure
  @mutex.synchronize do
    @stats[:failures] += 1
    @failure_count += 1
    @last_failure_time = Time.now

    case @state
    when CLOSED
      transition_to(OPEN) if @failure_count >= @threshold
    when HALF_OPEN
      # Single failure in half-open state reopens circuit
      transition_to(OPEN)
    end
  end
end

#record_successObject (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/attio/circuit_breaker.rb', line 153

private def record_success
  @mutex.synchronize do
    @stats[:successes] += 1
    @success_count += 1

    case @state
    when HALF_OPEN
      @half_open_successes += 1
      if @half_open_successes >= @half_open_requests
        # Circuit has proven stable
        transition_to(CLOSED)
      end
    when CLOSED
      # Reset failure count on success
      @failure_count = 0
    end
  end
end

#reset!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Manually reset the circuit breaker

Since:

  • 1.0.0



109
110
111
112
113
114
115
116
117
# File 'lib/attio/circuit_breaker.rb', line 109

def reset!
  @mutex.synchronize do
    @failure_count = 0
    @success_count = 0
    @half_open_successes = 0
    @last_failure_time = nil
    transition_to(CLOSED)
  end
end

#statsHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get circuit breaker statistics

Returns:

  • (Hash)

    Statistics

Since:

  • 1.0.0



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/attio/circuit_breaker.rb', line 129

def stats
  @mutex.synchronize do
    @stats.merge(
      state: @state,
      failure_count: @failure_count,
      success_count: @success_count,
      threshold: @threshold,
      time_until_retry: @state == OPEN ? time_until_retry : nil
    )
  end
end

#time_until_retryInteger

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Time remaining until circuit can attempt reset

Returns:

  • (Integer)

    Seconds until retry, 0 if ready

Since:

  • 1.0.0



144
145
146
147
148
149
150
151
# File 'lib/attio/circuit_breaker.rb', line 144

def time_until_retry
  return 0 unless @state == OPEN
  return 0 unless @last_failure_time

  elapsed = Time.now - @last_failure_time
  remaining = @timeout - elapsed
  remaining > 0 ? remaining.to_i : 0
end

#transition_to(new_state) ⇒ Object (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 1.0.0



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/attio/circuit_breaker.rb', line 194

private def transition_to(new_state)
  return if @state == new_state

  old_state = @state
  @state = new_state
  @stats[:state_changes] += 1

  # Reset counters for new state
  case new_state
  when CLOSED
    @failure_count = 0
    @half_open_successes = 0
  when HALF_OPEN
    @half_open_successes = 0
  end

  # Notify state change
  @on_state_change&.call(old_state, new_state)
end

#trip!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Manually trip the circuit breaker

Since:

  • 1.0.0



102
103
104
105
106
# File 'lib/attio/circuit_breaker.rb', line 102

def trip!
  @mutex.synchronize do
    transition_to(OPEN)
  end
end