class RedisClient::CircuitBreaker
def initialize(error_threshold:, error_timeout:, error_threshold_timeout: error_timeout, success_threshold: 0)
def initialize(error_threshold:, error_timeout:, error_threshold_timeout: error_timeout, success_threshold: 0) @error_threshold = Integer(error_threshold) @error_threshold_timeout = Float(error_threshold_timeout) @error_timeout = Float(error_timeout) @success_threshold = Integer(success_threshold) @errors = [] @successes = 0 @state = :closed @lock = Mutex.new end
def protect
def protect if @state == :open refresh_state end case @state when :open raise OpenCircuitError, "Too many connection errors happened recently" when :closed begin yield rescue ConnectionError record_error raise end when :half_open begin result = yield record_success result rescue ConnectionError record_error raise end else raise "[BUG] RedisClient::CircuitBreaker unexpected @state (#{@state.inspect}})" end end
def record_error
def record_error now = Process.clock_gettime(Process::CLOCK_MONOTONIC) expiry = now - @error_timeout @lock.synchronize do if @state == :closed @errors.reject! { |t| t < expiry } end @errors << now @successes = 0 if @state == :half_open || (@state == :closed && @errors.size >= @error_threshold) @state = :open end end end
def record_success
def record_success return unless @state == :half_open @lock.synchronize do return unless @state == :half_open @successes += 1 if @successes >= @success_threshold @state = :closed end end end
def refresh_state
def refresh_state now = Process.clock_gettime(Process::CLOCK_MONOTONIC) @lock.synchronize do if @errors.last < (now - @error_timeout) if @success_threshold > 0 @state = :half_open @successes = 0 else @errors.clear @state = :closed end end end end