# 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