Skip to content

Commit

Permalink
Merge pull request #196 from matthewshafer/2-0-upgrade
Browse files Browse the repository at this point in the history
2.0 Upgrade guide
  • Loading branch information
matthewshafer authored May 4, 2023
2 parents b6722f4 + 6fec862 commit 5f02e1d
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 58 deletions.
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
## Change log
### v2.0.0 (unreleased)
- When `Circuitbox::MemoryStore` is used use the monotonic system clock when calculating time windows [\#193](https://github.com/yammer/circuitbox/pull/193)
- `Circuitbox::CircuitBreaker`'s `:cache` option has been renamed to `:circuit_store` [\#190](https://github.com/yammer/circuitbox/pull/190)
- Improve circuit opening, fetch some keys in bulk [\#188](https://github.com/yammer/circuitbox/pull/188)
- Reduce amount of times a time window needs to be calculated [\#187](https://github.com/yammer/circuitbox/pull/187)
- Switch internals of `Circuitbox::CircuitBreaker` to use instance variables instead of `attr_reader` [#183](https://github.com/yammer/circuitbox/pull/183)
- Remove circuit's `logger` and `Circuitbox.default_logger` [\#182](https://github.com/yammer/circuitbox/pull/182)
- Rename `circuitbox_exceptions` to `exception` [\#181](https://github.com/yammer/circuitbox/pull/181)
- Rename notifications sent through ActiveSupport::Notifications [\#180](https://github.com/yammer/circuitbox/pull/180)
- Improve code documentation [\#177](https://github.com/yammer/circuitbox/pull/177)
- Improve circuit error messages [\#176](https://github.com/yammer/circuitbox/pull/176)
- Have the notifiers handle timing rather than circuitbox [\#169](https://github.com/yammer/circuitbox/pull/169)
- Improve the `Circuitbox.circuit` store when used in multi-threaded cases [\#167](https://github.com/yammer/circuitbox/pull/167)
- Always emit a runtime metric when running the circuit's block [\#163](https://github.com/yammer/circuitbox/pull/163)
- Remove timer option on a circuit, make timing (runtime metric) always enabled [\#159](https://github.com/yammer/circuitbox/pull/159)
- Rename execution_timer to timer and execution_time metric name to runtime [\#157](https://github.com/yammer/circuitbox/pull/157)
- Add frozen_string_literal to all files and enable lint rule for it [\#151](https://github.com/yammer/circuitbox/pull/151)
- Add linting [\#148](https://github.com/yammer/circuitbox/pull/148) [\#155](https://github.com/yammer/circuitbox/pull/155)
- Drop support for ruby 1.9.3 - 2.3. Support ruby 2.4 through 3.0 [\#66](https://github.com/yammer/circuitbox/pull/66) [\#92](https://github.com/yammer/circuitbox/pull/92) [\#123](https://github.com/yammer/circuitbox/pull/123) [\#144](https://github.com/yammer/circuitbox/pull/144) [\#148](https://github.com/yammer/circuitbox/pull/148) [\#166](https://github.com/yammer/circuitbox/pull/166)
- Drop support for ruby 1.9.3 - 2.5. Support ruby 2.6 through 3.2 [\#66](https://github.com/yammer/circuitbox/pull/66) [\#92](https://github.com/yammer/circuitbox/pull/92) [\#123](https://github.com/yammer/circuitbox/pull/123) [\#144](https://github.com/yammer/circuitbox/pull/144) [\#148](https://github.com/yammer/circuitbox/pull/148) [\#166](https://github.com/yammer/circuitbox/pull/166) [\#179](https://github.com/yammer/circuitbox/pull/179) [\#186](https://github.com/yammer/circuitbox/pull/186)
- Add custom in-memory store for circuit data [\#113](https://github.com/yammer/circuitbox/pull/113) [\#124](https://github.com/yammer/circuitbox/pull/124) [\#134](https://github.com/yammer/circuitbox/pull/134)
- Significant improvements to tracking circuit state and state changes [\#117](https://github.com/yammer/circuitbox/pull/117) [\#118](https://github.com/yammer/circuitbox/pull/118)
- Remove accessing Circuitbox circuits through ```Circuitbox[]``` [\#133](https://github.com/yammer/circuitbox/pull/133)
- Faraday middleware supports faraday 1.0 [\#143](https://github.com/yammer/circuitbox/pull/143)
- Faraday middleware supports faraday 1.0 and 2.0 [\#143](https://github.com/yammer/circuitbox/pull/143) [\#192](https://github.com/yammer/circuitbox/pull/192)
- Remove ```run!``` method and change ```run``` method (see upgrade guide) [\#119](https://github.com/yammer/circuitbox/pull/119) [\#126](https://github.com/yammer/circuitbox/pull/126)
- Correctly check a circuits volume threshold [\#116](https://github.com/yammer/circuitbox/pull/116)
- Stop overwriting sleep_window when it is less than time window [\#108](https://github.com/yammer/circuitbox/pull/108)
Expand Down
77 changes: 21 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

![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. Once more than 10 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for one minute. This helps your application gracefully degrade.
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)
* [https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker](https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker)

*Upgrading to 2.x? See [2.0 upgrade](docs/2.0-upgrade.md)*

## Usage

Expand Down Expand Up @@ -43,13 +49,17 @@ Using the `run` method will throw an exception when the circuit is open or the u
```

## Global Configuration
Circuitbox has defaults for circuit_store and notifier.
This can be configured through ```Circuitbox.configure```.
The circuit cache used by ```Circuitbox.circuit``` will be cleared after running ```Circuitbox.configure```.
This means when accessing the circuit through ```Circuitbox.circuit``` any custom configuration options should always be given.

Any circuit created manually through ```Circuitbox::CircuitBreaker``` before updating the configuration
will need to be recreated to pick up the new defaults.
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|
Expand Down Expand Up @@ -86,7 +96,6 @@ class ExampleServiceClient
error_threshold: 50,

# Customized notifier
# overrides the default
# this overrides what is set in the global configuration
notifier: Notifier.new
})
Expand All @@ -107,65 +116,21 @@ Circuitbox.circuit(:yammer, {

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/minad/moneta). As moneta is not a dependency of circuitbox
[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

Circuitbox has two built in notifiers, null and active support.
The active support notifier is used if `ActiveSupport::Notifications` is defined when circuitbox is loaded.
If `ActiveSupport::Notifications` is not defined the null notifier is used.
The null notifier does not send notifications anywhere.

The default notifier can be changed to use a specific built in notifier or a custom notifier when [configuring circuitbox](#global-configuration).

### ActiveSupport
Usage example:

**Circuit open/close:**

```ruby
ActiveSupport::Notifications.subscribe('open.circuitbox') do |_name, _start, _finish, _id, payload|
circuit_name = payload[:circuit]
Rails.logger.warn("Open circuit for: #{circuit_name}")
end
ActiveSupport::Notifications.subscribe('close.circuitbox') do |_name, _start, _finish, _id, payload|
circuit_name = payload[:circuit]
Rails.logger.info("Close circuit for: #{circuit_name}")
end
```

**Circuit run:**

```ruby
ActiveSupport::Notifications.subscribe('run.circuitbox') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
circuit_name = event.payload[:circuit_name]

Rails.logger.info("Circuit: #{circuit_name} Runtime: #{event.duration}")
end
```

**Circuit Warnings:**
In case of misconfiguration, circuitbox will fire a `warning.circuitbox`
notification.

```ruby
ActiveSupport::Notifications.subscribe('warning.circuitbox') do |_name, _start, _finish, _id, payload|
circuit_name = payload[:circuit]
warning = payload[:message]
Rails.logger.warning("Circuit warning for: #{circuit_name} Message: #{warning}")
end

```
See [Circuit Notifications](docs/circuit_notifications.md)

## Faraday

Expand Down
59 changes: 59 additions & 0 deletions docs/2.0-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Circuitbox 2.0 Upgrade Guide

## Requirements

Circuitbox 2.0 is tested against Ruby 2.6 through 3.2.
Since ruby 2.6 and 2.7 are EOL it's likely we'll drop support for those versions in future releases.

## Changes

* The `timeout_seconds` option when initializing a circuit has been removed.
* Circuitbox does not wrap the block to run with Ruby's `Timeout` anymore.
* If you would like to use Ruby's `Timeout` you would need to do this yourself.
* A `CircuitBreaker`'s `run` method has been changed and the `run!` method has been removed.
* Calling `run` is now like calling `run!` in circuitbox 1.x.
* If you were using `run` in circuitbox 1.x you can call `run(exception: false)` to have the same behavior.
* The `logger` option when initializing a circuit has been removed.
* If you would like to use a logger you can either
* If using the `ActiveSupport` notifier subscribe to it's notifications to add your own logger
* Implement your own notifier using the same interface, see the `ActiveSupport` or `Null` notifier.
* The `exceptions` option when initializing a circuit must be an array.
* The `exceptions` option when initializing a circuit does not default to `[Timeout::Error]` when the array is empty.
* The `cache` option when initializing a circuit as been renamed to `circuit_store`.
* A circuits defaults sleep_window has changed from 300 seconds to 90 seconds.
* The `partition` option when running a circuit has been removed.
* attr_accessor's on `CircuitBreaker` have been changed to attr_reader, or in some cases removed.
* `partition` has been removed
* `logger` has been removed (see above about logger option)
* `time_class` has been added
* The class level `reset` method on `Circuitbox` has been removed.
* If you need to reset circuits before/after running a test you can reconfigure circuitbox
```ruby
Circuitbox.configure do |config|
# Reset persisted state in the memory store so it doesn't leak between tests
# if using a store through moneta and want to reset it between tests you may need to do something else
config.default_circuit_store = Circuitbox::MemoryStore.new
end
```
* The class level `reset` method on `Circuitbox::CircuitBreaker` has been removed.
* This method would call `reset` on `Circuitbox`, see above about `Circuitbox.reset`.
* `Circuitbox.circuit` does not parse a host name out of a uri and use that as the circuit's service_name.
* If you rely on this functionality parse the host from the uri before calling `Circuitbox.circuit`
* Accessing/creating a circuit through `Circuitbox[:circuit_identifier]` has been removed.
* Circuitbox's notifications sent through `ActiveSupport::Notifications` have had their names changed.
* `circuit_open` changes to `open.circuitbox`
* `circuit_close` changes to `close.circuitbox`
* `circuit_success` changes to `success.circuitbox`
* `circuit_failure` changes to `failure.circuitbox`
* `circuit_skipped` changes to `skipped.circuitbox`
* `circuit_gauge` removed
* `circuit_warning` changes to `warning.circuitbox`
* add `run.circuitbox` to track runtime of the block the circuit is running
* Circuit's don't emit their error rate, success counts, failure counts through `ActiveSupport::Notifications` anymore.
* The methods `error_rate`, `success_count`, `failure_count` on a circuit instance can be used to obtain this information.
* During circuit initialization if `sleep_window` is less than `time_window` it is not set to `time_window`.
* Circuitbox warns about, and only checks this during circuit initialization
* Circuitbox's default circuit store has changed from `Moneta`'s memory store to `Circuitbox::MemoryStore`.
* `Circuitbox::MemoryStore` periodically removes expired keys from the store, allowing Ruby to reclaim memory
* When `Circuitbox::MemoryStore` is used a circuits time class is `Circuitbox::TimeHelper::Monotonic`
* If using `Moneta` as a circuit store, only adapters that support bulk read functionality are supported.
80 changes: 80 additions & 0 deletions docs/circuit_notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Circuit Notifications

Circuitbox supports sending notifications when various events occur.
The following events are sent to the notifier:

* Circuit block runs (this is where notifiers do timing)
* Circuit is skipped
* Circuit run is successful
* Circuit run is failed
* Circuit is opened
* Circuit is closed
* Circuit is not configured correctly

There are two types of notifiers built into circuitbox, null and active support.
The null notifier does not send notifications.
The active support notifier sends notifications through `ActiveSupport::Notifications`.

## Active Support Notifications

There are three different types of notification payloads which are defined below

All notifications contain `:circuit` in the payload.
The value of `:circuit` is the name of the circuit.

The first type of notifications are:

* `open.circuitbox` - Sent when the circuit moves to the open state.
* `close.circuitbox` - Sent when the circuit moves to the closed state.
* `skipped.circuitbox` - Sent when the circuit is run and in the open state.
* `success.circuitbox` - Sent when the circuit is run and the run succeeds.
* `failure.circuitbox` - Sent when the circuit is run and the run fails.

The second type of notifications are contain a `:message` in the payload, in addition to `:circuit`.
The value of `:message` is a string.

* `warning.circuitbox` - Sent when there is a misconfiguration of the circuit.

The third type of notifications can be used for timing of the circuit.
The timing is done by `ActiveSupport::Notifications`.

* `run.circuitbox` - Sent after the circuit is run.

### Examples

#### Open/Close/Skipped/Success/Failure

```ruby
ActiveSupport::Notifications.subscribe('open.circuitbox') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
circuit_name = event.payload[:circuit]
Rails.logger.warn("Open circuit for: #{circuit_name}")
end

ActiveSupport::Notifications.subscribe('close.circuitbox') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
circuit_name = event.payload[:circuit]
Rails.logger.info("Close circuit for: #{circuit_name}")
end
```

#### Warning

```ruby
ActiveSupport::Notifications.subscribe('warning.circuitbox') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
circuit_name = event.payload[:circuit]
warning = event.payload[:message]
Rails.logger.warning("Circuit warning for: #{circuit_name} Message: #{warning}")
end
```

#### Timing
```ruby
ActiveSupport::Notifications.subscribe('run.circuitbox') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
circuit_name = event.payload[:circuit_name]

Rails.logger.info("Circuit: #{circuit_name} Runtime: #{event.duration}")
end
```
22 changes: 22 additions & 0 deletions lib/circuitbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ 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)

Expand Down
14 changes: 14 additions & 0 deletions lib/circuitbox/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,30 @@ def self.extended(base)
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
Expand Down

0 comments on commit 5f02e1d

Please sign in to comment.