Class: A2A::Client::Middleware::CircuitBreakerInterceptor

Inherits:
Object
  • Object
show all
Defined in:
lib/a2a/client/middleware/circuit_breaker_interceptor.rb

Constant Summary collapse

CLOSED =

Circuit breaker states

:closed
OPEN =
:open
HALF_OPEN =
:half_open

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(failure_threshold: 5, timeout: 60, expected_errors: nil) ⇒ CircuitBreakerInterceptor

Initialize circuit breaker interceptor

Parameters:

  • (defaults to: 5)

    Number of failures before opening circuit (default: 5)

  • (defaults to: 60)

    Timeout in seconds before trying half-open (default: 60)

  • (defaults to: nil)

    Error classes that should trigger circuit breaker



27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 27

def initialize(failure_threshold: 5, timeout: 60, expected_errors: nil)
  @failure_threshold = failure_threshold
  @timeout = timeout
  @expected_errors = expected_errors || default_expected_errors
  @state = CLOSED
  @failure_count = 0
  @success_count = 0
  @last_failure_time = nil
  @mutex = Mutex.new

  validate_configuration!
end

Instance Attribute Details

#expected_errorsObject (readonly)

Returns the value of attribute expected_errors.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def expected_errors
  @expected_errors
end

#failure_countObject (readonly)

Returns the value of attribute failure_count.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def failure_count
  @failure_count
end

#failure_thresholdObject (readonly)

Returns the value of attribute failure_threshold.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def failure_threshold
  @failure_threshold
end

#last_failure_timeObject (readonly)

Returns the value of attribute last_failure_time.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def last_failure_time
  @last_failure_time
end

#stateObject (readonly)

Returns the value of attribute state.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def state
  @state
end

#success_countObject (readonly)

Returns the value of attribute success_count.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def success_count
  @success_count
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



18
19
20
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 18

def timeout
  @timeout
end

Instance Method Details

#call(request, context, next_middleware) ⇒ Object

Execute request with circuit breaker logic

Parameters:

  • The request object

  • Request context

  • Next middleware in chain

Returns:

  • Response from next middleware



47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 47

def call(request, context, next_middleware)
  @mutex.synchronize do
    case @state
    when CLOSED
      execute_request(request, context, next_middleware)
    when OPEN
      check_timeout_and_execute(request, context, next_middleware)
    when HALF_OPEN
      execute_half_open_request(request, context, next_middleware)
    end
  end
end

#check_timeout_and_execute(request, context, next_middleware) ⇒ Object (private)

Check timeout and execute request when circuit is open

Raises:



137
138
139
140
141
142
143
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 137

def check_timeout_and_execute(request, context, next_middleware)
  raise A2A::Errors::AgentUnavailable, "Circuit breaker is OPEN. Service unavailable." unless timeout_expired?

  @state = HALF_OPEN
  @success_count = 0
  execute_half_open_request(request, context, next_middleware)
end

#circuit_breaker_error?(error) ⇒ Boolean (private)

Check if error should trigger circuit breaker

Returns:



212
213
214
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 212

def circuit_breaker_error?(error)
  @expected_errors.any? { |error_class| error.is_a?(error_class) }
end

#closed?Boolean

Check if the circuit breaker is closed

Returns:

  • True if circuit is closed



90
91
92
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 90

def closed?
  @state == CLOSED
end

#default_expected_errorsObject (private)

Default error classes that should trigger circuit breaker



218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 218

def default_expected_errors
  [
    A2A::Errors::TimeoutError,
    A2A::Errors::HTTPError,
    A2A::Errors::TransportError,
    A2A::Errors::AgentUnavailable,
    A2A::Errors::ResourceExhausted,
    A2A::Errors::InternalError,
    Faraday::TimeoutError,
    Faraday::ConnectionFailed
  ]
end

#execute_half_open_request(request, context, next_middleware) ⇒ Object (private)

Execute request in half-open state



147
148
149
150
151
152
153
154
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 147

def execute_half_open_request(request, context, next_middleware)
  response = next_middleware.call(request, context)
  on_half_open_success
  response
rescue StandardError => e
  on_half_open_failure if circuit_breaker_error?(e)
  raise e
end

#execute_request(request, context, next_middleware) ⇒ Object (private)

Execute request in closed state



126
127
128
129
130
131
132
133
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 126

def execute_request(request, context, next_middleware)
  response = next_middleware.call(request, context)
  on_success
  response
rescue StandardError => e
  on_failure if circuit_breaker_error?(e)
  raise e
end

#half_open?Boolean

Check if the circuit breaker is half-open

Returns:

  • True if circuit is half-open



98
99
100
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 98

def half_open?
  @state == HALF_OPEN
end

#on_failureObject (private)

Handle failed request



164
165
166
167
168
169
170
171
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 164

def on_failure
  @failure_count += 1
  @last_failure_time = Time.now

  return unless @failure_count >= @failure_threshold

  @state = OPEN
end

#on_half_open_failureObject (private)

Handle failed request in half-open state



186
187
188
189
190
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 186

def on_half_open_failure
  @state = OPEN
  @failure_count += 1
  @last_failure_time = Time.now
end

#on_half_open_successObject (private)

Handle successful request in half-open state



175
176
177
178
179
180
181
182
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 175

def on_half_open_success
  @success_count += 1

  # Close circuit after successful request in half-open state
  @state = CLOSED
  @failure_count = 0
  @success_count = 0
end

#on_successObject (private)

Handle successful request



158
159
160
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 158

def on_success
  @failure_count = 0
end

#open?Boolean

Check if the circuit breaker is open

Returns:

  • True if circuit is open



82
83
84
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 82

def open?
  @state == OPEN
end

#reset!Object

Reset the circuit breaker to closed state



104
105
106
107
108
109
110
111
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 104

def reset!
  @mutex.synchronize do
    @state = CLOSED
    @failure_count = 0
    @success_count = 0
    @last_failure_time = nil
  end
end

#statusHash

Get current circuit breaker status

Returns:

  • Circuit breaker status information



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 64

def status
  @mutex.synchronize do
    {
      state: @state,
      failure_count: @failure_count,
      success_count: @success_count,
      failure_threshold: @failure_threshold,
      timeout: @timeout,
      last_failure_time: @last_failure_time,
      time_until_half_open: time_until_half_open
    }
  end
end

#time_until_half_openObject (private)

Calculate time until circuit can be half-open



202
203
204
205
206
207
208
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 202

def time_until_half_open
  return 0 unless @state == OPEN && @last_failure_time

  elapsed = Time.now - @last_failure_time
  remaining = @timeout - elapsed
  [remaining, 0].max
end

#timeout_expired?Boolean (private)

Check if timeout has expired for opening circuit

Returns:



194
195
196
197
198
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 194

def timeout_expired?
  return false unless @last_failure_time

  Time.now - @last_failure_time >= @timeout
end

#trip!Object

Force the circuit breaker to open state



115
116
117
118
119
120
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 115

def trip!
  @mutex.synchronize do
    @state = OPEN
    @last_failure_time = Time.now
  end
end

#validate_configuration!Object (private)

Validate configuration parameters

Raises:



233
234
235
236
237
238
239
240
241
# File 'lib/a2a/client/middleware/circuit_breaker_interceptor.rb', line 233

def validate_configuration!
  raise ArgumentError, "failure_threshold must be positive" if @failure_threshold <= 0

  raise ArgumentError, "timeout must be positive" if @timeout <= 0

  return if @expected_errors.is_a?(Array)

  raise ArgumentError, "expected_errors must be an array"
end