class Shift::CircuitBreaker::CircuitHandler
Overview¶ ↑
Implements a generic mechanism for detecting external service call timeouts and reduces the time spent waiting for further requests that will most-likely fail (e.g. timeout) and cause request queueing.
Similar to a conventional circuit breaker, when a circuit is closed it allows operations to flow through. When the error_threshold
is exceeded (tripped), the circuit is then opened for the defined skip_duration
, ie. no operations are executed and the provided fallback is called.
Constants
- DEFAULT_ERROR_LOGGING_STATE
- DEFAULT_EXCEPTION_CLASSES
Attributes
Public Class Methods
Initializer creates an instance of the service
@param [Symbol] name - the name used to identify the circuit breaker @param [Integer] error_threshold
- The minimum error threshold required for the circuit to be opened/tripped @param [Integer] skip_duration
- The duration in seconds the circuit should be open for before operations are allowed through/executed @param [Array] additional_exception_classes - Any additional exception classes to rescue along the DEFAULT_EXCEPTION_CLASSES
@param [Boolean] error_logging_enabled
- Decided whether to log errors or not. Still they will be monitored @param [Object] logger - service to handle error logging @param [Object] monitor - service to monitor metric
# File lib/shift/circuit_breaker/circuit_handler.rb, line 31 def initialize(name, error_threshold:, skip_duration:, additional_exception_classes: [], error_logging_enabled: DEFAULT_ERROR_LOGGING_STATE, logger: Shift::CircuitBreaker::CircuitLogger.new, monitor: Shift::CircuitBreaker::CircuitMonitor.new) self.name = name self.error_threshold = error_threshold self.skip_duration = skip_duration self.error_logging_enabled = error_logging_enabled self.exception_classes = (additional_exception_classes | DEFAULT_EXCEPTION_CLASSES) self.logger = logger self.monitor = monitor self.error_count = 0 self.last_error_time = nil self.state = :closed end
Public Instance Methods
Performs the given operation within the circuit @param [Proc] operation - the operation to be performed @param [Proc] fallback - The result returned if the operation is not performed or raises an exception
# File lib/shift/circuit_breaker/circuit_handler.rb, line 54 def call(operation:, fallback:) raise ArgumentError unless operation.respond_to?(:call) && fallback.respond_to?(:call) set_state if state == :open monitor.record_metric(name, state) return fallback.call end perform_operation(operation, fallback) end
Private Instance Methods
# File lib/shift/circuit_breaker/circuit_handler.rb, line 103 def handle_exception(exception, fallback) record_error set_state log_errors(exception) monitor.record_metric(name, state) fallback.call end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 111 def log_errors(exception) logger.error(circuit_name: name, state: state, error_message: exception.message) if error_logging_enabled end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 94 def perform_operation(operation, fallback) response = operation.call reset_state monitor.record_metric(name, state) response rescue *exception_classes handle_exception($!, fallback) end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 87 def record_error # Increment the error_count self.error_count += 1 # Set the time the error occured. self.last_error_time = Time.now end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 80 def reset_state # Reset the error attributes to default values. self.error_count = 0 self.last_error_time = nil self.state = :closed end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 66 def set_state # The curcuit is opened/tripped if the error_threshold is met or exceeded # (error_count >= error_threshold) and the last_error_time is within # the skip_duration (see comments in #skip_duration_expired?). self.state = (error_count >= error_threshold) && !skip_duration_expired? ? :open : :closed end
# File lib/shift/circuit_breaker/circuit_handler.rb, line 73 def skip_duration_expired? return true unless last_error_time.present? # IF the difference in time between now and the last_error_time # is greater than the skip_duration, then it will have expired. (Time.now - last_error_time) > skip_duration end