Class: DilisensePepClient::CircuitBreaker

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

Overview

Circuit breaker implementation for API resilience and fault tolerance

This class implements the Circuit Breaker pattern to protect against cascading failures when the Dilisense API becomes unavailable or starts returning errors frequently. It prevents unnecessary load on a failing service by temporarily blocking requests and allowing the service time to recover.

States:

  • CLOSED: Normal operation, requests pass through

  • OPEN: Service is failing, all requests are blocked

  • HALF_OPEN: Testing if service has recovered, limited requests allowed

The circuit breaker automatically transitions between states based on:

  • Failure threshold: Number of consecutive failures before opening

  • Recovery timeout: Time to wait before attempting to recover

  • Success criteria: Requirements to close the circuit after half-open

Features:

  • Thread-safe operation using concurrent-ruby primitives

  • Configurable failure thresholds and recovery timeouts

  • Timeout protection for individual requests

  • Comprehensive metrics and logging

  • Security event logging for monitoring

Examples:

Basic usage with default settings

breaker = CircuitBreaker.new(service_name: "dilisense_api")
result = breaker.call do
  # Make API request here
  api_client.get("/endpoint")
end

Custom configuration for high-availability requirements

breaker = CircuitBreaker.new(
  service_name: "dilisense_api",
  failure_threshold: 3,     # Open after 3 failures
  recovery_timeout: 30,     # Try again after 30 seconds
  timeout: 15,              # Individual request timeout
  exceptions: [APIError, NetworkError]  # Only these errors count as failures
)

Defined Under Namespace

Classes: CircuitOpenError

Constant Summary collapse

STATES =

Valid circuit breaker states following the standard pattern

%i[closed open half_open].freeze

Instance Method Summary collapse

Constructor Details

#initialize(service_name:, failure_threshold: 5, recovery_timeout: 60, timeout: 30, exceptions: [StandardError]) ⇒ CircuitBreaker

Initialize a new circuit breaker with specified configuration

Parameters:

  • service_name (String)

    Name of the protected service (for logging and metrics)

  • failure_threshold (Integer) (defaults to: 5)

    Number of failures before opening the circuit (default: 5)

  • recovery_timeout (Integer) (defaults to: 60)

    Seconds to wait before attempting recovery (default: 60)

  • timeout (Integer) (defaults to: 30)

    Timeout for individual requests in seconds (default: 30)

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

    Exception types that count as failures (default: [StandardError])



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

def initialize(
  service_name:,
  failure_threshold: 5,
  recovery_timeout: 60,
  timeout: 30,
  exceptions: [StandardError]
)
  @service_name = service_name
  @failure_threshold = failure_threshold
  @recovery_timeout = recovery_timeout
  @timeout = timeout
  @exceptions = exceptions
  
  # Initialize state - circuit starts in CLOSED (normal) state
  @state = :closed
  @failure_count = Concurrent::AtomicFixnum.new(0)  # Thread-safe failure counter
  @last_failure_time = Concurrent::AtomicReference.new  # Track when last failure occurred
  @next_attempt_time = Concurrent::AtomicReference.new  # When to allow next attempt after opening
  @success_count = Concurrent::AtomicFixnum.new(0)  # Track successful requests
  @mutex = Mutex.new  # Synchronize state transitions
end

Instance Method Details

#call(&block) ⇒ Object

Execute a block of code with circuit breaker protection This is the main method that wraps your API calls or other potentially failing operations

Examples:

Protect an API call

result = circuit_breaker.call do
  http_client.get("/api/endpoint")
end

Parameters:

  • block (Proc)

    The code to execute (typically an API call)

Returns:

  • (Object)

    Result of the executed block

Raises:

  • (CircuitOpenError)

    When circuit is open and blocking requests

  • (Exception)

    Any exception raised by the protected code



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 102

def call(&block)
  case state
  when :open
    # Circuit is open - check if enough time has passed to allow a test request
    check_if_half_open_allowed
    raise CircuitOpenError.new(@service_name, @next_attempt_time.value)
  when :half_open
    # Circuit is testing recovery - attempt the request and reset if successful
    attempt_reset(&block)
  when :closed
    # Circuit is closed - normal operation, execute the request
    execute(&block)
  end
rescue *@exceptions => e
  # Catch configured exception types and record as failures
  record_failure(e)
  raise
end

#failure_countObject



125
126
127
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 125

def failure_count
  @failure_count.value
end

#force_open!Object



158
159
160
161
162
163
164
165
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 158

def force_open!
  @mutex.synchronize do
    @state = :open
    @next_attempt_time.value = Time.now + @recovery_timeout
  end
  
  Logger.logger.warn("Circuit breaker forced open", service_name: @service_name)
end

#metricsObject



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 133

def metrics
  {
    service_name: @service_name,
    state: state,
    failure_count: failure_count,
    success_count: success_count,
    failure_threshold: @failure_threshold,
    recovery_timeout: @recovery_timeout,
    last_failure_time: @last_failure_time.value,
    next_attempt_time: @next_attempt_time.value
  }
end

#reset!Object



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 146

def reset!
  @mutex.synchronize do
    @state = :closed
    @failure_count.value = 0
    @success_count.value = 0
    @last_failure_time.value = nil
    @next_attempt_time.value = nil
  end
  
  Logger.logger.info("Circuit breaker reset", service_name: @service_name)
end

#stateObject



121
122
123
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 121

def state
  @mutex.synchronize { @state }
end

#success_countObject



129
130
131
# File 'lib/dilisense_pep_client/circuit_breaker.rb', line 129

def success_count
  @success_count.value
end