circuitbox-2.0.0/0000755000175100017510000000000014452264245014056 5ustar vivekdebvivekdebcircuitbox-2.0.0/circuitbox.gemspec0000644000175100017510000000720614452264245017603 0ustar vivekdebvivekdeb######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: circuitbox 2.0.0 ruby lib Gem::Specification.new do |s| s.name = "circuitbox".freeze s.version = "2.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/yammer/circuitbox/issues", "changelog_uri" => "https://github.com/yammer/circuitbox/blob/main/CHANGELOG.md", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/yammer/circuitbox" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Fahim Ferdous".freeze, "Matthew Shafer".freeze] s.date = "2023-05-04" s.email = ["fahimfmf@gmail.com".freeze] s.files = ["LICENSE".freeze, "README.md".freeze, "lib/circuitbox.rb".freeze, "lib/circuitbox/circuit_breaker.rb".freeze, "lib/circuitbox/configuration.rb".freeze, "lib/circuitbox/errors/error.rb".freeze, "lib/circuitbox/errors/open_circuit_error.rb".freeze, "lib/circuitbox/errors/service_failure_error.rb".freeze, "lib/circuitbox/excon_middleware.rb".freeze, "lib/circuitbox/faraday_middleware.rb".freeze, "lib/circuitbox/memory_store.rb".freeze, "lib/circuitbox/memory_store/container.rb".freeze, "lib/circuitbox/notifier/active_support.rb".freeze, "lib/circuitbox/notifier/null.rb".freeze, "lib/circuitbox/time_helper/monotonic.rb".freeze, "lib/circuitbox/time_helper/real.rb".freeze, "lib/circuitbox/version.rb".freeze] s.homepage = "https://github.com/yammer/circuitbox".freeze s.licenses = ["Apache-2.0".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.6.0".freeze) s.rubygems_version = "3.3.15".freeze s.summary = "A robust circuit breaker that manages failing external services.".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, ["> 2.0"]) s.add_development_dependency(%q.freeze, ["~> 0.71"]) s.add_development_dependency(%q.freeze, [">= 0.17"]) s.add_development_dependency(%q.freeze, ["~> 0.5"]) s.add_development_dependency(%q.freeze, ["~> 5.14"]) s.add_development_dependency(%q.freeze, ["~> 2.0"]) s.add_development_dependency(%q.freeze, ["~> 1.12"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 2.0"]) s.add_development_dependency(%q.freeze, ["~> 13.0"]) s.add_development_dependency(%q.freeze, ["~> 0.9"]) s.add_development_dependency(%q.freeze, ["~> 1.4"]) s.add_development_dependency(%q.freeze, ["~> 1.7"]) s.add_development_dependency(%q.freeze, ["~> 0.9.26"]) else s.add_dependency(%q.freeze, ["> 2.0"]) s.add_dependency(%q.freeze, ["~> 0.71"]) s.add_dependency(%q.freeze, [">= 0.17"]) s.add_dependency(%q.freeze, ["~> 0.5"]) s.add_dependency(%q.freeze, ["~> 5.14"]) s.add_dependency(%q.freeze, ["~> 2.0"]) s.add_dependency(%q.freeze, ["~> 1.12"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 2.0"]) s.add_dependency(%q.freeze, ["~> 13.0"]) s.add_dependency(%q.freeze, ["~> 0.9"]) s.add_dependency(%q.freeze, ["~> 1.4"]) s.add_dependency(%q.freeze, ["~> 1.7"]) s.add_dependency(%q.freeze, ["~> 0.9.26"]) end end circuitbox-2.0.0/lib/0000755000175100017510000000000014452264245014624 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/0000755000175100017510000000000014452264245016777 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/version.rb0000644000175100017510000000011014452264245021001 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox VERSION = '2.0.0' end circuitbox-2.0.0/lib/circuitbox/time_helper/0000755000175100017510000000000014452264245021274 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/time_helper/real.rb0000644000175100017510000000027014452264245022543 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox module TimeHelper module Real module_function def current_second ::Time.now.to_i end end end end circuitbox-2.0.0/lib/circuitbox/time_helper/monotonic.rb0000644000175100017510000000034614452264245023631 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox module TimeHelper module Monotonic module_function def current_second Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) end end end end circuitbox-2.0.0/lib/circuitbox/notifier/0000755000175100017510000000000014452264245020616 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/notifier/null.rb0000644000175100017510000000040514452264245022114 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox module Notifier class Null def notify(_circuit_name, _event); end def notify_warning(_circuit_name, _message); end def notify_run(_circuit_name) yield end end end end circuitbox-2.0.0/lib/circuitbox/notifier/active_support.rb0000644000175100017510000000110114452264245024203 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox module Notifier class ActiveSupport def notify(circuit_name, event) ::ActiveSupport::Notifications.instrument("#{event}.circuitbox", circuit: circuit_name) end def notify_warning(circuit_name, message) ::ActiveSupport::Notifications.instrument('warning.circuitbox', circuit: circuit_name, message: message) end def notify_run(circuit_name, &block) ::ActiveSupport::Notifications.instrument('run.circuitbox', circuit: circuit_name, &block) end end end end circuitbox-2.0.0/lib/circuitbox/memory_store/0000755000175100017510000000000014452264245021523 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/memory_store/container.rb0000644000175100017510000000121614452264245024032 0ustar vivekdebvivekdeb# frozen_string_literal: true require_relative '../time_helper/monotonic' class Circuitbox class MemoryStore class Container include TimeHelper::Monotonic attr_accessor :value def initialize(value:, expiry: 0) @value = value expires_after(expiry) end def expired? @expires_after.positive? && @expires_after < current_second end def expired_at?(clock_second) @expires_after.positive? && @expires_after < clock_second end def expires_after(seconds = 0) @expires_after = seconds.zero? ? seconds : current_second + seconds end end end end circuitbox-2.0.0/lib/circuitbox/memory_store.rb0000644000175100017510000000406214452264245022052 0ustar vivekdebvivekdeb# frozen_string_literal: true require_relative 'time_helper/monotonic' require_relative 'memory_store/container' class Circuitbox class MemoryStore include TimeHelper::Monotonic def initialize(compaction_frequency: 60) @store = {} @mutex = Mutex.new @compaction_frequency = compaction_frequency @compact_after = current_second + compaction_frequency end def store(key, value, opts = {}) @mutex.synchronize do @store[key] = Container.new(value: value, expiry: opts.fetch(:expires, 0)) value end end def increment(key, amount = 1, opts = {}) seconds_to_expire = opts.fetch(:expires, 0) @mutex.synchronize do existing_container = fetch_container(key) # reusing the existing container is a small optimization # to reduce the amount of objects created if existing_container existing_container.expires_after(seconds_to_expire) existing_container.value += amount else @store[key] = Container.new(value: amount, expiry: seconds_to_expire) amount end end end def load(key, _opts = {}) @mutex.synchronize { fetch_container(key)&.value } end def values_at(*keys, **_opts) @mutex.synchronize do current_time = current_second keys.map! { |key| fetch_container(key, current_time)&.value } end end def key?(key) @mutex.synchronize { !fetch_container(key).nil? } end def delete(key) @mutex.synchronize { @store.delete(key) } end private def fetch_container(key, current_time = current_second) compact(current_time) if @compact_after < current_time container = @store[key] return unless container if container.expired_at?(current_time) @store.delete(key) nil else container end end def compact(current_time) @store.delete_if { |_, value| value.expired_at?(current_time) } @compact_after = current_time + @compaction_frequency end end end circuitbox-2.0.0/lib/circuitbox/faraday_middleware.rb0000644000175100017510000000560614452264245023137 0ustar vivekdebvivekdeb# frozen_string_literal: true require 'faraday' require 'circuitbox' class Circuitbox class FaradayMiddleware < Faraday::Middleware class RequestFailed < StandardError; end class NullResponse < Faraday::Response attr_reader :original_response, :original_exception def initialize(response = nil, exception = nil) @original_response = response @original_exception = exception super(status: 503, response_headers: {}) end end DEFAULT_OPTIONS = { open_circuit: lambda do |response| # response.status: # nil -> connection could not be established, or failed very hard # 5xx -> non recoverable server error, opposed to 4xx which are client errors response.status.nil? || (response.status >= 500 && response.status <= 599) end, default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) }, # It's possible for the URL object to not have a host at the time the middleware # is run. To not break circuitbox by creating a circuit with a nil service name # we can get the string representation of the URL object and use that as the service name. identifier: ->(env) { env[:url].host || env[:url].to_s }, # default circuit breaker options are merged in during initialization circuit_breaker_options: {} }.freeze DEFAULT_EXCEPTIONS = [ Faraday::TimeoutError, RequestFailed ].freeze DEFAULT_CIRCUIT_BREAKER_OPTIONS = { exceptions: DEFAULT_EXCEPTIONS }.freeze def initialize(app, opts = {}) @app = app @opts = DEFAULT_OPTIONS.merge(opts) @opts[:circuit_breaker_options] = DEFAULT_CIRCUIT_BREAKER_OPTIONS.merge(@opts[:circuit_breaker_options]) super(app) end def call(request_env) service_response = nil circuit(request_env).run do @app.call(request_env).on_complete do |env| service_response = Faraday::Response.new(env) raise RequestFailed if open_circuit?(service_response) end end rescue Circuitbox::Error => e circuit_open_value(request_env, service_response, e) end private def call_default_value(response, exception) default_value = @opts[:default_value] default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value end def open_circuit?(response) @opts[:open_circuit].call(response) end def circuit_open_value(env, service_response, exception) env[:circuit_breaker_default_value] || call_default_value(service_response, exception) end def circuit(env) identifier = @opts[:identifier] id = identifier.respond_to?(:call) ? identifier.call(env) : identifier Circuitbox.circuit(id, @opts[:circuit_breaker_options]) end end end Faraday::Middleware.register_middleware(circuitbox: Circuitbox::FaradayMiddleware) circuitbox-2.0.0/lib/circuitbox/excon_middleware.rb0000644000175100017510000000477114452264245022646 0ustar vivekdebvivekdeb# frozen_string_literal: true require 'excon' require 'circuitbox' class Circuitbox class ExconMiddleware < Excon::Middleware::Base class RequestFailed < StandardError; end DEFAULT_EXCEPTIONS = [ Excon::Errors::Timeout, RequestFailed ].freeze class NullResponse < Excon::Response def initialize(response, exception) @original_response = response @original_exception = exception super(status: 503, response_headers: {}) end def []=(key, value) @data[key] = value end end attr_reader :opts def initialize(stack, opts = {}) @stack = stack default_options = { open_circuit: ->(response) { response[:status] >= 400 } } @opts = default_options.merge(opts) super(stack) end def error_call(datum) circuit(datum).run do raise RequestFailed end rescue Circuitbox::Error => e circuit_open_value(datum, datum[:response], e) end def request_call(datum) circuit(datum).run do @stack.request_call(datum) end end def response_call(datum) circuit(datum).run do raise RequestFailed if open_circuit?(datum[:response]) end @stack.response_call(datum) rescue Circuitbox::Error => e circuit_open_value(datum, datum[:response], e) end def identifier @identifier ||= opts.fetch(:identifier, ->(env) { env[:host] }) end def exceptions circuit_breaker_options[:exceptions] end private def circuit(datum) id = identifier.respond_to?(:call) ? identifier.call(datum) : identifier circuitbox.circuit id, circuit_breaker_options end def open_circuit?(response) opts[:open_circuit].call(response) end def circuitbox @circuitbox ||= opts.fetch(:circuitbox, Circuitbox) end def circuit_open_value(env, response, exception) env[:circuit_breaker_default_value] || default_value.call(response, exception) end def circuit_breaker_options @circuit_breaker_options ||= begin options = opts.fetch(:circuit_breaker_options, {}) options.merge!( exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS) ) end end def default_value @default_value ||= begin default = opts.fetch(:default_value) do ->(response, exception) { NullResponse.new(response, exception) } end default.respond_to?(:call) ? default : ->(*) { default } end end end end circuitbox-2.0.0/lib/circuitbox/errors/0000755000175100017510000000000014452264245020313 5ustar vivekdebvivekdebcircuitbox-2.0.0/lib/circuitbox/errors/service_failure_error.rb0000644000175100017510000000102514452264245025216 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox class ServiceFailureError < Circuitbox::Error attr_reader :service, :original def initialize(service, exception) super() @service = service @original = exception # We copy over the original exceptions backtrace if there is one backtrace = exception.backtrace set_backtrace(backtrace) unless backtrace.empty? end def to_s "#{self.class}: Service #{service.inspect} was unavailable (original: #{original})" end end end circuitbox-2.0.0/lib/circuitbox/errors/open_circuit_error.rb0000644000175100017510000000045014452264245024533 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox class OpenCircuitError < Circuitbox::Error attr_reader :service def initialize(service) super() @service = service end def to_s "#{self.class}: Service #{service.inspect} has an open circuit" end end end circuitbox-2.0.0/lib/circuitbox/errors/error.rb0000644000175100017510000000012714452264245021771 0ustar vivekdebvivekdeb# frozen_string_literal: true class Circuitbox class Error < StandardError; end end circuitbox-2.0.0/lib/circuitbox/configuration.rb0000644000175100017510000000372214452264245022177 0ustar vivekdebvivekdeb# frozen_string_literal: true require_relative 'memory_store' require_relative 'notifier/active_support' require_relative 'notifier/null' class Circuitbox module Configuration attr_writer :default_circuit_store, :default_notifier def self.extended(base) base.instance_eval do @cached_circuits_mutex = Mutex.new @cached_circuits = {} # preload circuit_store because it has no other dependencies default_circuit_store end end # Configure Circuitbox's defaults # After configuring the cached circuits are cleared # # @yieldparam [Circuitbox::Configuration] Circuitbox configuration # def configure yield self clear_cached_circuits! nil end # Circuit store used by circuits that are not configured with a specific circuit store # Defaults to Circuitbox::MemoryStore # # @return [Circuitbox::MemoryStore, Moneta] Circuit store def default_circuit_store @default_circuit_store ||= MemoryStore.new end # Notifier used by circuits that are not configured with a specific notifier. # If ActiveSupport::Notifications is defined it defaults to Circuitbox::Notifier::ActiveSupport # Otherwise it defaults to Circuitbox::Notifier::Null # # @return [Circuitbox::Notifier::ActiveSupport, Circuitbox::Notifier::Null] Notifier def default_notifier @default_notifier ||= if defined?(ActiveSupport::Notifications) Notifier::ActiveSupport.new else Notifier::Null.new end end private def find_or_create_circuit_breaker(service_name, options) @cached_circuits_mutex.synchronize do @cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options) end end def clear_cached_circuits! @cached_circuits_mutex.synchronize { @cached_circuits = {} } end end end circuitbox-2.0.0/lib/circuitbox/circuit_breaker.rb0000644000175100017510000002311614452264245022464 0ustar vivekdebvivekdeb# frozen_string_literal: true require_relative 'time_helper/monotonic' require_relative 'time_helper/real' class Circuitbox class CircuitBreaker attr_reader :service, :circuit_options, :exceptions, :circuit_store, :notifier, :time_class DEFAULTS = { sleep_window: 90, volume_threshold: 5, error_threshold: 50, time_window: 60 }.freeze # Initialize a CircuitBreaker # # @param service [String, Symbol] Name of the circuit for notifications and metrics store # @param options [Hash] Options to create the circuit with # @option options [Integer] :time_window (60) Interval of time, in seconds, used to calculate the error_rate # @option options [Integer, Proc] :sleep_window (90) Seconds for the circuit to stay open when tripped # @option options [Integer, Proc] :volume_threshold (5) Number of requests before error rate is first calculated # @option options [Integer, Proc] :error_threshold (50) Percentage of failed requests needed to trip the circuit # @option options [Array] :exceptions The exceptions that should be monitored and counted as failures # @option options [Circuitbox::MemoryStore, Moneta] :circuit_store (Circuitbox.default_circuit_store) Class to store circuit open/close statistics # @option options [Object] :notifier (Circuitbox.default_notifier) Class notifications are sent to # # @raise [ArgumentError] If the exceptions option is not an Array def initialize(service, options = {}) @service = service.to_s @circuit_options = DEFAULTS.merge(options) @circuit_store = options.fetch(:circuit_store) { Circuitbox.default_circuit_store } @notifier = options.fetch(:notifier) { Circuitbox.default_notifier } if @circuit_options[:timeout_seconds] warn('timeout_seconds was removed in circuitbox 2.0. '\ 'Check the upgrade guide at https://github.com/yammer/circuitbox') end if @circuit_options[:cache] warn('cache was changed to circuit_store in circuitbox 2.0. '\ 'Check the upgrade guide at https://github.com/yammer/circuitbox') end @exceptions = options.fetch(:exceptions) raise ArgumentError.new('exceptions must be an array') unless @exceptions.is_a?(Array) @time_class = options.fetch(:time_class) { default_time_klass } @state_change_mutex = Mutex.new @open_storage_key = "circuits:#{@service}:open" @half_open_storage_key = "circuits:#{@service}:half_open" check_sleep_window end def option_value(name) value = @circuit_options[name] value.is_a?(Proc) ? value.call : value end # Run the circuit with the given block. # If the circuit is closed or half_open the block will run. # If the circuit is open the block will not be run. # # @param exception [Boolean] If exceptions should be raised when the circuit is open # or when a watched exception is raised from the block # @yield Block to run if circuit is not open # # @raise [Circuitbox::OpenCircuitError] If the circuit is open and exception is true # @raise [Circuitbox::ServiceFailureError] If a tracked exception is raised from the block and exception is true # # @return [Object] The result from the block # @return [Nil] If the circuit is open and exception is false # In cases where an exception that circuitbox is watching is raised from either a notifier # or from a custom circuit store nil can be returned even though the block ran successfully def run(exception: true, &block) if open? skipped! raise Circuitbox::OpenCircuitError.new(@service) if exception else begin response = @notifier.notify_run(@service, &block) success! rescue *@exceptions => e # Other stores could raise an exception that circuitbox is asked to watch. # setting to nil keeps the same behavior as the previous definition of run. response = nil failure! raise Circuitbox::ServiceFailureError.new(@service, e) if exception end end response end # Check if the circuit is open # # @return [Boolean] True if circuit is open, False if closed def open? @circuit_store.key?(@open_storage_key) end # Calculates the current error rate of the circuit # # @return [Float] Error Rate def error_rate(failures = failure_count, success = success_count) all_count = failures + success return 0.0 unless all_count.positive? (failures / all_count.to_f) * 100 end # Number of Failures the circuit has encountered in the current time window # # @return [Integer] Number of failures def failure_count @circuit_store.load(stat_storage_key('failure'), raw: true).to_i end # Number of successes the circuit has encountered in the current time window # # @return [Integer] Number of successes def success_count @circuit_store.load(stat_storage_key('success'), raw: true).to_i end # If the circuit is open the key indicating that the circuit is open # On the next call to run the circuit would run as if it were in the half open state # # This does not reset any of the circuit success/failure state so future failures # in the same time window may cause the circuit to open sooner def try_close_next_time @circuit_store.delete(@open_storage_key) end private def should_open? aligned_time = align_time_to_window failures, successes = @circuit_store.values_at(stat_storage_key('failure', aligned_time), stat_storage_key('success', aligned_time), raw: true) # Calling to_i is only needed for moneta stores which can return a string representation of an integer. # While readability could increase by adding .map(&:to_i) to the end of the values_at call it's also slightly # less performant when we only have two values to convert. failures = failures.to_i successes = successes.to_i passed_volume_threshold?(failures, successes) && passed_rate_threshold?(failures, successes) end def passed_volume_threshold?(failures, successes) failures + successes >= option_value(:volume_threshold) end def passed_rate_threshold?(failures, successes) error_rate(failures, successes) >= option_value(:error_threshold) end def half_open_failure @state_change_mutex.synchronize do return if open? || !half_open? trip end # Running event outside of the synchronize block to allow other threads # that may be waiting to become unblocked notify_opened end def open! @state_change_mutex.synchronize do return if open? trip end # Running event outside of the synchronize block to allow other threads # that may be waiting to become unblocked notify_opened end def notify_opened notify_event('open') end def trip @circuit_store.store(@open_storage_key, true, expires: option_value(:sleep_window)) @circuit_store.store(@half_open_storage_key, true) end def close! @state_change_mutex.synchronize do # If the circuit is not open, the half_open key will be deleted from the store # if half_open exists the deleted value is returned and allows us to continue # if half_open doesn't exist nil is returned, causing us to return early return unless !open? && @circuit_store.delete(@half_open_storage_key) end # Running event outside of the synchronize block to allow other threads # that may be waiting to become unblocked notify_event('close') end def half_open? @circuit_store.key?(@half_open_storage_key) end def success! increment_and_notify_event('success') close! if half_open? end def failure! increment_and_notify_event('failure') if half_open? half_open_failure elsif should_open? open! end end def skipped! notify_event('skipped') end # Send event notification to notifier def notify_event(event) @notifier.notify(@service, event) end # Increment stat store and send notification def increment_and_notify_event(event) time_window = option_value(:time_window) aligned_time = align_time_to_window(time_window) @circuit_store.increment(stat_storage_key(event, aligned_time), 1, expires: time_window) notify_event(event) end def stat_storage_key(event, aligned_time = align_time_to_window) "circuits:#{@service}:stats:#{aligned_time}:#{event}" end # return time representation in seconds def align_time_to_window(window = option_value(:time_window)) time = @time_class.current_second time - (time % window) # remove rest of integer division end def check_sleep_window sleep_window = option_value(:sleep_window) time_window = option_value(:time_window) return unless sleep_window < time_window warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\ "the error_rate would not be reset after a sleep." @notifier.notify_warning(@service, warning_message) warn("Circuit: #{@service}, Warning: #{warning_message}") end def default_time_klass if @circuit_store.is_a?(Circuitbox::MemoryStore) Circuitbox::TimeHelper::Monotonic else Circuitbox::TimeHelper::Real end end end end circuitbox-2.0.0/lib/circuitbox.rb0000644000175100017510000000361014452264245017324 0ustar vivekdebvivekdeb# frozen_string_literal: true require_relative 'circuitbox/version' require_relative 'circuitbox/circuit_breaker' require_relative 'circuitbox/errors/error' require_relative 'circuitbox/errors/open_circuit_error' require_relative 'circuitbox/errors/service_failure_error' require_relative 'circuitbox/configuration' class Circuitbox extend Configuration class << self # @overload circuit(service_name, options = {}) # Returns a Circuitbox::CircuitBreaker for the given service_name # # @param service_name [String, Symbol] Name of the service # Mixing Symbols/Strings for the same service (:test/'test') will result in # multiple circuits being created that point to the same service. # @param options [Hash] Options for the circuit (See Circuitbox::CircuitBreaker#initialize options) # Any configuration options should always be passed when calling this method. # @return [Circuitbox::CircuitBreaker] CircuitBreaker for the given service_name # # @overload circuit(service_name, options = {}, &block) # Runs the circuit with the given block # The circuit's run method is called with `exception` set to false # # @param service_name [String, Symbol] Name of the service # Mixing Symbols/Strings for the same service (:test/'test') will result in # multiple circuits being created that point to the same service. # @param options [Hash] Options for the circuit (See Circuitbox::CircuitBreaker#initialize options) # Any configuration options should always be passed when calling this method. # # @return [Object] The result of the block # @return [nil] If the circuit is open def circuit(service_name, options, &block) circuit = find_or_create_circuit_breaker(service_name, options) return circuit unless block circuit.run(exception: false, &block) end end end circuitbox-2.0.0/README.md0000644000175100017510000001613414452264245015342 0ustar vivekdebvivekdeb# Circuitbox ![Tests](https://github.com/yammer/circuitbox/workflows/Tests/badge.svg) [![Gem Version](https://badge.fury.io/rb/circuitbox.svg)](https://badge.fury.io/rb/circuitbox) Circuitbox is a Ruby circuit breaker gem. It protects your application from failures of its service dependencies. It wraps calls to external services and monitors for failures in one minute intervals. Using a circuit's defaults once more than 5 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for 90 seconds. This helps your application gracefully degrade. Resources about the circuit breaker pattern: * [http://martinfowler.com/bliki/CircuitBreaker.html](http://martinfowler.com/bliki/CircuitBreaker.html) *Upgrading to 2.x? See [2.0 upgrade](docs/2.0-upgrade.md)* ## Usage ```ruby Circuitbox.circuit(:your_service, exceptions: [Net::ReadTimeout]) do Net::HTTP.get URI('http://example.com/api/messages') end ``` Circuitbox will return nil for failed requests and open circuits. If your HTTP client has its own conditions for failure, you can pass an `exceptions` option. ```ruby class ExampleServiceClient def circuit Circuitbox.circuit(:yammer, exceptions: [Zephyr::FailedRequest]) end def http_get circuit.run(exception: false) do Zephyr.new("http://example.com").get(200, 1000, "/api/messages") end end end ``` Using the `run` method will throw an exception when the circuit is open or the underlying service fails. ```ruby def http_get circuit.run do Zephyr.new("http://example.com").get(200, 1000, "/api/messages") end end ``` ## Global Configuration Circuitbox defaults can be configured through ```Circuitbox.configure```. There are two defaults that can be configured: * `default_circuit_store` - Defaults to a `Circuitbox::MemoryStore`. This can be changed to a compatible Moneta store. * `default_notifier` - Defaults to `Circuitbox::Notifier::ActiveSupport` if `ActiveSupport::Notifications` is defined, otherwise defaults to `Circuitbox::Notifier::Null` After configuring circuitbox through `Circuitbox.configure`, the internal circuit cache of `Circuitbox.circuit` is cleared. Any circuit created manually through ```Circuitbox::CircuitBreaker``` before updating the configuration will need to be recreated to pick up the new defaults. The following is an example Circuitbox configuration: ```ruby Circuitbox.configure do |config| config.default_circuit_store = Circuitbox::MemoryStore.new config.default_notifier = Circuitbox::Notifier::Null.new end ``` ## Per-Circuit Configuration ```ruby class ExampleServiceClient def circuit Circuitbox.circuit(:your_service, { # exceptions circuitbox tracks for counting failures (required) exceptions: [YourCustomException], # seconds the circuit stays open once it has passed the error threshold sleep_window: 300, # length of interval (in seconds) over which it calculates the error rate time_window: 60, # number of requests within `time_window` seconds before it calculates error rates (checked on failures) volume_threshold: 10, # the store you want to use to save the circuit state so it can be # tracked, this needs to be Moneta compatible, and support increment # this overrides what is set in the global configuration circuit_store: Circuitbox::MemoryStore.new, # exceeding this rate will open the circuit (checked on failures) error_threshold: 50, # Customized notifier # this overrides what is set in the global configuration notifier: Notifier.new }) end end ``` You can also pass a Proc as an option value which will evaluate each time the circuit breaker is used. This lets you configure the circuit breaker without having to restart the processes. ```ruby Circuitbox.circuit(:yammer, { sleep_window: Proc.new { Configuration.get(:sleep_window) }, exceptions: [Net::ReadTimeout] }) ``` ## Circuit Store Holds all the relevant data to trip the circuit if a given number of requests fail in a specified period of time. Circuitbox also supports [Moneta](https://github.com/moneta-rb/moneta). As moneta is not a dependency of circuitbox it needs to be loaded prior to use. There are a lot of moneta stores to choose from but some pre-requisits need to be satisfied first: - Needs to support increment, this is true for most but not all available stores. - Needs to support expiry. - Needs to support bulk read. - Needs to support concurrent access if you share them. For example sharing a KyotoCabinet store across process fails because the store is single writer multiple readers, and all circuits sharing the store need to be able to write. ## Notifications See [Circuit Notifications](docs/circuit_notifications.md) ## Faraday Circuitbox ships with a [Faraday HTTP client](https://github.com/lostisland/faraday) middleware. The versions of faraday the middleware has been tested against is `>= 0.17` through `~> 2.0`. The middleware does not support parallel requests through a connections `in_parallel` method. ```ruby require 'faraday' require 'circuitbox/faraday_middleware' conn = Faraday.new(:url => "http://example.com") do |c| c.use Circuitbox::FaradayMiddleware end response = conn.get("/api") if response.success? # success else # failure or open circuit end ``` By default the Faraday middleware returns a `503` response when the circuit is open, but this as many other things can be configured via middleware options * `default_value` value to return for open circuits, defaults to 503 response wrapping the original response given by the service and stored as `original_response` property of the returned 503, this can be overwritten with either * a static value * a `lambda` which is passed the `original_response` and `original_error`. `original_response` will be populated if Faraday returns an error response, `original_error` will be populated if an error was thrown before Faraday returned a response. ```ruby c.use Circuitbox::FaradayMiddleware, default_value: lambda { |response, error| ... } ``` * `identifier` circuit id, defaults to request url ```ruby c.use Circuitbox::FaradayMiddleware, identifier: "service_name_circuit" ``` * `circuit_breaker_options` options to initialize the circuit with defaults to `{ exceptions: Circuitbox::FaradayMiddleware::DEFAULT_EXCEPTIONS }`. Accepts same options as Circuitbox:CircuitBreaker#new ```ruby c.use Circuitbox::FaradayMiddleware, circuit_breaker_options: {} ``` * `open_circuit` lambda determining what response is considered a failure, counting towards the opening of the circuit ```ruby c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.status >= 500 } ``` ## Installation Add this line to your application's Gemfile: gem 'circuitbox' And then execute: $ bundle Or install it yourself as: $ gem install circuitbox ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request circuitbox-2.0.0/LICENSE0000644000175100017510000000124114452264245015061 0ustar vivekdebvivekdebCopyright (c) 2014. Microsoft Corporation. All Rights Reserved All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.