class Semian::CircuitBreaker
:nodoc:
def acquire(resource = nil, &block)
def acquire(resource = nil, &block) return yield if disabled? transition_to_half_open if transition_to_half_open? raise OpenCircuitError unless request_allowed? result = nil begin result = maybe_with_half_open_resource_timeout(resource, &block) rescue *@exceptions => error if !error.respond_to?(:marks_semian_circuits?) || error.marks_semian_circuits? mark_failed(error) end raise error else mark_success end result end
def destroy
def destroy @errors.destroy @successes.destroy @state.destroy end
def disabled?
def disabled? ENV["SEMIAN_CIRCUIT_BREAKER_DISABLED"] || ENV["SEMIAN_DISABLED"] end
def error_threshold_reached?
def error_threshold_reached? @errors.size == @error_count_threshold end
def error_timeout_expired?
def error_timeout_expired? last_error_time = @errors.last return false unless last_error_time Time.at(last_error_time) + @error_timeout < Time.now end
def in_use?
def in_use? !error_timeout_expired? && !@errors.empty? end
def initialize(name, exceptions:, success_threshold:, error_threshold:,
def initialize(name, exceptions:, success_threshold:, error_threshold:, error_timeout:, implementation:, half_open_resource_timeout: nil, error_threshold_timeout: nil) @name = name.to_sym @success_count_threshold = success_threshold @error_count_threshold = error_threshold @error_threshold_timeout = error_threshold_timeout || error_timeout @error_timeout = error_timeout @exceptions = exceptions @half_open_resource_timeout = half_open_resource_timeout @errors = implementation::SlidingWindow.new(max_size: @error_count_threshold) @successes = implementation::Integer.new @state = implementation::State.new reset end
def log_state_transition(new_state)
def log_state_transition(new_state) return if @state.nil? || new_state == @state.value str = "[#{self.class.name}] State transition from #{@state.value} to #{new_state}." str += " success_count=#{@successes.value} error_count=#{@errors.size}" str += " success_count_threshold=#{@success_count_threshold} error_count_threshold=#{@error_count_threshold}" str += " error_timeout=#{@error_timeout} error_last_at=\"#{@errors.last}\"" str += " name=\"#{@name}\"" if new_state == :open && @last_error str += " last_error_message=#{@last_error.message.inspect}" end Semian.logger.info(str) end
def mark_failed(error)
def mark_failed(error) push_error(error) push_time(@errors) if closed? transition_to_open if error_threshold_reached? elsif half_open? transition_to_open end end
def mark_success
def mark_success return unless half_open? @successes.increment transition_to_close if success_threshold_reached? end
def maybe_with_half_open_resource_timeout(resource, &block)
def maybe_with_half_open_resource_timeout(resource, &block) result = if half_open? && @half_open_resource_timeout && resource.respond_to?(:with_resource_timeout) resource.with_resource_timeout(@half_open_resource_timeout) do block.call end else block.call end result end
def notify_state_transition(new_state)
def notify_state_transition(new_state) Semian.notify(:state_change, self, nil, nil, state: new_state) end
def push_error(error)
def push_error(error) @last_error = error end
def push_time(window, time: Time.now)
def push_time(window, time: Time.now) window.reject! { |err_time| err_time + @error_threshold_timeout < time.to_i } window << time.to_i end
def request_allowed?
def request_allowed? closed? || half_open? || transition_to_half_open? end
def reset
def reset @errors.clear @successes.reset transition_to_close end
def success_threshold_reached?
def success_threshold_reached? @successes.value >= @success_count_threshold end
def transition_to_close
def transition_to_close notify_state_transition(:closed) log_state_transition(:closed) @state.close! @errors.clear end
def transition_to_half_open
def transition_to_half_open notify_state_transition(:half_open) log_state_transition(:half_open) @state.half_open! @successes.reset end
def transition_to_half_open?
def transition_to_half_open? open? && error_timeout_expired? && !half_open? end
def transition_to_open
def transition_to_open notify_state_transition(:open) log_state_transition(:open) @state.open! end