faulty 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
4
- data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
3
+ metadata.gz: 1fe02d7203e9d26a8f859d55efa9ffbe099ab6c2cf025fe7ca831c6ccdfd684f
4
+ data.tar.gz: bed9fe698b66b367dab9066ffec98567c60fd8dd57f9ea3ed7ea3a2a3a4a3b48
5
5
  SHA512:
6
- metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
7
- data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
6
+ metadata.gz: c69668b5b99fc2dad979031bd3a61e61d608c8f1c8ba095924709f3cea1b653a4116edcc45a72fb4d3b72e367a3e8b56d7c3cd3d56ed03aadb0ae48be6f3d7bc
7
+ data.tar.gz: 97d9ff9d9f6bb368ca395b9338bc52b83ef545f67a1f412729b52aef997f6187d57f28fbfda989584e7d26e5e300127f075699f60b888746eaa0c48a2e3b3656
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: CI
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+ branches: [master]
7
+ pull_request:
8
+ branches: ['**']
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ redis: [4]
16
+ ruby: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0, jruby-9.2.10, truffleruby-20.2.0]
17
+ include:
18
+ - redis: 3
19
+ ruby: 2.7
20
+ services:
21
+ redis:
22
+ image: redis
23
+ ports:
24
+ - 6379:6379
25
+ steps:
26
+ - uses: actions/checkout@v2
27
+ - uses: ruby/setup-ruby@v1
28
+ env:
29
+ REDIS_VERSION: ${{ matrix.redis }}
30
+ with:
31
+ ruby-version: ${{ matrix.ruby }}
32
+ bundler-cache: true
33
+ - run: bundle exec rubocop
34
+ if: matrix.ruby == '2.7'
35
+ - name: start MySQL
36
+ run: sudo /etc/init.d/mysql start
37
+ - run: bundle exec rspec --format doc
38
+ env:
39
+ MYSQL_USER: root
40
+ MYSQL_PASSWORD: root
41
+ - name: Run codacy-coverage-reporter
42
+ uses: codacy/codacy-coverage-reporter-action@master
43
+ with:
44
+ project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
45
+ coverage-reports: coverage/lcov/faulty.lcov
46
+ - run: bin/check-version
47
+
48
+ release:
49
+ needs: test
50
+ if: startsWith(github.ref, 'refs/tags/v')
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v2
54
+ - uses: dawidd6/action-publish-gem@v1
55
+ with:
56
+ api_key: ${{secrets.RUBYGEMS_API_KEY}}
data/.rubocop.yml CHANGED
@@ -8,6 +8,9 @@ AllCops:
8
8
  Layout/ArgumentAlignment:
9
9
  EnforcedStyle: with_fixed_indentation
10
10
 
11
+ Layout/CaseIndentation:
12
+ EnforcedStyle: end
13
+
11
14
  Layout/ParameterAlignment:
12
15
  EnforcedStyle: with_fixed_indentation
13
16
 
@@ -47,6 +50,9 @@ RSpec/FilePath:
47
50
  RSpec/NamedSubject:
48
51
  Enabled: false
49
52
 
53
+ RSpec/MessageSpies:
54
+ Enabled: false
55
+
50
56
  RSpec/MultipleExpectations:
51
57
  Enabled: false
52
58
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,53 @@
1
+ ## Release v0.6.0
2
+
3
+ * docs, use correct state in description for skipped event #27 senny
4
+ * Fix CI to set REDIS_VERSION correctly #31 justinhoward
5
+ * Fix a potential memory leak in patches #32 justinhoward
6
+ * Capture an error for BUSY redis backend when patched #30 justinhoward
7
+ * Add a patch for mysql2 #28 justinhoward
8
+
9
+ ## Release v0.5.1
10
+
11
+ * Fix Storage::FaultTolerantProxy to return empty history on entries fail #26 justinhoward
12
+
13
+ ## Release v0.5.0
14
+
15
+ * Allow creating a new Faulty instance in Faulty#register #24 justinhoward
16
+ * Add support for patches to core dependencies starting with redis #14 justinhoward
17
+ * Improve storage #entries performance by returning entries #23 justinhoward
18
+
19
+ ### Breaking Changes
20
+
21
+ * Faulty #[] no longer differentiates between symbols and strings when accessing
22
+ Faulty instances
23
+ * Faulty::Storage::Interface must now return a history array instead of a
24
+ circuit status object. Custom storage backends must be updated.
25
+
26
+ ## Release v0.4.0
27
+
28
+ * Switch from Travis CI to GitHub actions #11 justinhoward
29
+ * Only run rubocop for Ruby 2.7 in CI #12 justinhoward
30
+ * Explicitly add support for Redis 3 and 4 #15 justinhoward
31
+ * Allow setting default circuit options on Faulty instances #16 justinhoward
32
+ * Switch to codacy for quality metrics #17 justinhoward
33
+ * Small logic fix to README #19 silasb
34
+ * Fix Redis storage dependency on ConnectionPool #21 justinhoward
35
+ * Allow passing custom circuit to AutoWire #22 justinhoward
36
+
37
+ ### Breaking Changes
38
+
39
+ AutoWire.new is replaced with AutoWire.wrap and no longer creates an instance
40
+ of AutoWire.
41
+
42
+ ## Release v0.3.0
43
+
44
+ * Add tools for backend fault-tolerance #10
45
+ * CircuitProxy for wrapping storage in an internal circuit
46
+ * FallbackChain storage backend for falling back to stable storage
47
+ * Timeout warnings for Redis backend
48
+ * AutoWire wrappers for automatically configuring storage and cache
49
+ * Better documentation for fault-tolerance
50
+
1
51
  ## Release v0.2.0
2
52
 
3
53
  * Remove Scopes and replace them with Faulty instances #9
data/Gemfile CHANGED
@@ -13,10 +13,18 @@ not_jruby = %i[ruby mingw x64_mingw].freeze
13
13
  gem 'activesupport', '>= 4.2'
14
14
  gem 'bundler', '>= 1.17', '< 3'
15
15
  gem 'byebug', platforms: not_jruby
16
+ gem 'honeybadger', '>= 2.0'
16
17
  gem 'irb', '~> 1.0'
18
+ # Minimum of 0.5.0 for specific error classes
19
+ gem 'mysql2', '>= 0.5.0', platforms: not_jruby
17
20
  gem 'redcarpet', '~> 3.5', platforms: not_jruby
18
21
  gem 'rspec_junit_formatter', '~> 0.4'
19
- # For now, code climate doesn't support simplecov 0.18
20
- # https://github.com/codeclimate/test-reporter/issues/413
21
- gem 'simplecov', '>= 0.17.1', '< 0.18'
22
+ gem 'simplecov', '>= 0.17.1'
23
+ # 0.8 is incompatible with simplecov < 0.18
24
+ # https://github.com/fortissimo1997/simplecov-lcov/pull/25
25
+ gem 'simplecov-lcov', '~> 0.7', '< 0.8'
22
26
  gem 'yard', '~> 0.9.25', platforms: not_jruby
27
+
28
+ if ENV['REDIS_VERSION']
29
+ gem 'redis', "~> #{ENV['REDIS_VERSION']}"
30
+ end
data/README.md CHANGED
@@ -1,19 +1,103 @@
1
1
  # Faulty
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/faulty.svg)](https://badge.fury.io/rb/faulty)
4
- [![Build Status](https://travis-ci.org/ParentSquare/faulty.svg?branch=master)](https://travis-ci.org/ParentSquare/faulty)
5
- [![Code Climate](https://codeclimate.com/github/ParentSquare/faulty/badges/gpa.svg)](https://codeclimate.com/github/ParentSquare/faulty)
6
- [![Test Coverage](https://codeclimate.com/github/ParentSquare/faulty/badges/coverage.svg)](https://codeclimate.com/github/ParentSquare/faulty)
4
+ [![CI](https://github.com/ParentSquare/faulty/workflows/CI/badge.svg)](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
5
+ [![Code Quality](https://app.codacy.com/project/badge/Grade/16bb1df1569a4ddba893a866673dac2a)](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=ParentSquare/faulty&amp;utm_campaign=Badge_Grade)
6
+ [![Code Coverage](https://app.codacy.com/project/badge/Coverage/16bb1df1569a4ddba893a866673dac2a)](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ParentSquare/faulty&utm_campaign=Badge_Coverage)
7
7
  [![Inline docs](http://inch-ci.org/github/ParentSquare/faulty.svg?branch=master)](http://inch-ci.org/github/ParentSquare/faulty)
8
8
 
9
9
  Fault-tolerance tools for ruby based on [circuit-breakers][martin fowler].
10
10
 
11
+ **Without Faulty**
12
+
13
+ External dependencies like APIs can start failing at any time When they do, it
14
+ could cause cascading failures in your application.
15
+
11
16
  ```ruby
12
- users = Faulty.circuit(:api).try_run do
17
+ # The application will always try to execute this even if the API
18
+ # fails repeatedly
19
+ api.users
20
+ ```
21
+
22
+ **With Faulty**
23
+
24
+ Faulty monitors errors inside this block and will "trip" a circuit if your
25
+ threshold is passed. Once a circuit is tripped, Faulty stops executing this
26
+ block until it recovers. Your application can detect external failures, and
27
+ prevent their effects from degrading overall performance.
28
+
29
+ ```ruby
30
+ users = Faulty.circuit('api').try_run do
31
+
32
+ # If this raises an exception, it counts towards the failure rate
33
+ # The exceptions that count as failures are configurable
34
+ # All failures will be sent to your event listeners for monitoring
13
35
  api.users
36
+
14
37
  end.or_default([])
38
+ # Here we return a stubbed value so the app can continue to function
39
+ # Another strategy is just to re-raise the exception so the app can handle it
40
+ # or use its default error handler
15
41
  ```
16
42
 
43
+ See [What is this for?](#what-is-this-for) for a more detailed explanation.
44
+ Also see "Release It!: Design and Deploy Production-Ready Software" by
45
+ [Michael T. Nygard][michael nygard] and the
46
+ [Martin Fowler Article][martin fowler] post on circuit breakers.
47
+
48
+ ## Contents
49
+
50
+ * [Installation](#installation)
51
+ * [API Docs](#api-docs)
52
+ * [Setup](#setup)
53
+ * [Basic Usage](#basic-usage)
54
+ * [What is this for?](#what-is-this-for-)
55
+ * [Configuration](#configuration)
56
+ + [Configuring the Storage Backend](#configuring-the-storage-backend)
57
+ - [Memory](#memory)
58
+ - [Redis](#redis)
59
+ - [FallbackChain](#fallbackchain)
60
+ - [Storage::FaultTolerantProxy](#storagefaulttolerantproxy)
61
+ - [Storage::CircuitProxy](#storagecircuitproxy)
62
+ + [Configuring the Cache Backend](#configuring-the-cache-backend)
63
+ - [Null](#null)
64
+ - [Rails](#rails)
65
+ - [Cache::FaultTolerantProxy](#cachefaulttolerantproxy)
66
+ - [Cache::CircuitProxy](#cachecircuitproxy)
67
+ + [Multiple Configurations](#multiple-configurations)
68
+ - [The default instance](#the-default-instance)
69
+ - [Multiple Instances](#multiple-instances)
70
+ - [Standalone Instances](#standalone-instances)
71
+ * [Working with circuits](#working-with-circuits)
72
+ + [Running a Circuit](#running-a-circuit)
73
+ - [With Exceptions](#with-exceptions)
74
+ - [With Faulty::Result](#with-faultyresult)
75
+ + [Specifying the Captured Errors](#specifying-the-captured-errors)
76
+ + [Using the Cache](#using-the-cache)
77
+ + [Configuring the Circuit Threshold](#configuring-the-circuit-threshold)
78
+ - [Rate Threshold](#rate-threshold)
79
+ - [Sample Threshold](#sample-threshold)
80
+ - [Cool Down](#cool-down)
81
+ + [Circuit Options](#circuit-options)
82
+ + [Listing Circuits](#listing-circuits)
83
+ + [Locking Circuits](#locking-circuits)
84
+ * [Patches](#patches)
85
+ + [Patch::Redis](#patchredis)
86
+ + [Patch::Mysql2](#patchmysql2)
87
+ * [Event Handling](#event-handling)
88
+ + [CallbackListener](#callbacklistener)
89
+ + [Other Built-in Listeners](#other-built-in-listeners)
90
+ + [Custom Listeners](#custom-listeners)
91
+ * [How it Works](#how-it-works)
92
+ + [Caching](#caching)
93
+ + [Fault Tolerance](#fault-tolerance)
94
+ * [Implementing a Cache Backend](#implementing-a-cache-backend)
95
+ * [Implementing a Storage Backend](#implementing-a-storage-backend)
96
+ * [Alternatives](#alternatives)
97
+ + [Currently Active](#currently-active)
98
+ + [Previous Work](#previous-work)
99
+ + [Faulty's Unique Features](#faulty-s-unique-features)
100
+
17
101
  ## Installation
18
102
 
19
103
  Add it to your `Gemfile`:
@@ -28,8 +112,10 @@ Or install it manually:
28
112
  gem install faulty
29
113
  ```
30
114
 
31
- During your app startup, call `Faulty.init`. For Rails, you would do this in
32
- `config/initializers/faulty.rb`. See [Setup](#setup) for details.
115
+ During your app startup, call
116
+ [`Faulty.init`](https://www.rubydoc.info/gems/faulty/Faulty.init).
117
+ For Rails, you would do this in `config/initializers/faulty.rb`. See
118
+ [Setup](#setup) for details.
33
119
 
34
120
  ## API Docs
35
121
 
@@ -64,66 +150,31 @@ Faulty.init do |config|
64
150
  end
65
151
  ```
66
152
 
67
- For a full list of configuration options, see the
68
- [Global Configuration](#global-configuration) section.
69
-
70
- ## What is this for?
71
-
72
- Circuit breakers are a fault-tolerance tool for creating separation between your
73
- application and external dependencies. For example, your application may call an
74
- external API to send a text message:
75
-
76
- ```ruby
77
- TextApi.send(message)
78
- ```
79
-
80
- In normal operation, this API call is very fast. However what if the texting
81
- service started hanging? Your application would quickly use up a lot of
82
- resources waiting for requests to return from the service. You could consider
83
- adding a timeout to your request:
84
-
85
- ```ruby
86
- TextApi.send(message, timeout: 5)
87
- ```
88
-
89
- Now your application will terminate requests after 5 seconds, but that could
90
- still add up to a lot of resources if you call this thousands of times. Circuit
91
- breakers solve this problem.
153
+ Or use a faulty instance instead for an object-oriented approach
92
154
 
93
155
  ```ruby
94
- Faulty.circuit(:text_api).run do
95
- TextApi.send(message, timeout: 5)
156
+ faulty = Faulty.new do
157
+ config.storage = Faulty::Storage::Redis.new
96
158
  end
97
159
  ```
98
160
 
99
- Now, when the text API hangs, the first few will run and start timing out. This
100
- will trip the circuit. After the circuit trips
101
- (see [How it Works](#how-it-works)), calls to the text API will be paused for
102
- the configured cool down period. Your application resources are not overwhelmed.
103
-
104
- You are free to implement a fallback or error handling however you wish, for
105
- example, in this case, you might add the text message to a failure queue:
106
-
107
- ```ruby
108
- Faulty.circuit(:text_api).run do
109
- TextApi.send(message, timeout: 5)
110
- rescue Faulty::CircuitError => e
111
- FailureQueue.enqueue(message)
112
- end
113
- ```
161
+ For a full list of configuration options, see the
162
+ [Configuration](#configuration) section.
114
163
 
115
164
  ## Basic Usage
116
165
 
117
- To create a circuit, call `Faulty.circuit`. This can be done as you use the
118
- circuit, or you can set it up beforehand. Any options passed to the `circuit`
119
- method are synchronized across threads and saved as long as the process is alive.
166
+ To create a circuit, call
167
+ [`Faulty.circuit`](https://www.rubydoc.info/gems/faulty/Faulty.circuit).
168
+ This can be done as you use the circuit, or you can set it up beforehand. Any
169
+ options passed to the `circuit` method are synchronized across threads and saved
170
+ as long as the process is alive.
120
171
 
121
172
  ```ruby
122
- circuit1 = Faulty.circuit(:api, cache_refreshes_after: 1800)
173
+ circuit1 = Faulty.circuit(:api, rate_threshold: 0.6)
123
174
 
124
175
  # The options from above are also used when called here
125
176
  circuit2 = Faulty.circuit(:api)
126
- circuit2.options.cache_refreshes_after == 1800 # => true
177
+ circuit2.options.rate_threshold == 0.6 # => true
127
178
 
128
179
  # The same circuit is returned on each consecutive call
129
180
  circuit1.equal?(circuit2) # => true
@@ -140,11 +191,12 @@ end
140
191
  See [How it Works](#how-it-works) for more details about how Faulty handles
141
192
  circuit failures.
142
193
 
143
- If the `run` block above fails, a `Faulty::CircuitError` will be raised. It is
144
- up to your application to handle that error however necessary or crash. Often
145
- though, you don't want to crash your application when a circuit fails, but
146
- instead apply a fallback or default behavior. For this, Faulty provides the
147
- `try_run` method:
194
+ If the `run` block above fails, a
195
+ [`Faulty::CircuitError`](https://www.rubydoc.info/gems/faulty/Faulty/CircuitError)
196
+ will be raised. It is up to your application to handle that error however
197
+ necessary or crash. Often though, you don't want to crash your application when
198
+ a circuit fails, but instead apply a fallback or default behavior. For this,
199
+ Faulty provides the `try_run` method:
148
200
 
149
201
  ```ruby
150
202
  result = Faulty.circuit(:api).try_run do
@@ -158,11 +210,13 @@ else
158
210
  end
159
211
  ```
160
212
 
161
- The `try_run` method returns a result type instead of raising errors. See the
162
- API docs for `Result` for more information. Here we use it to check whether the
163
- result is `ok?` (not an error). If it is we set the users variable, otherwise we
164
- set a default of an empty array. This pattern is so common, that `Result` also
165
- implements a helper method `or_default` to do the same thing:
213
+ The [`try_run`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:try_run)
214
+ method returns a result type instead of raising errors. See the API docs for
215
+ [`Result`](https://www.rubydoc.info/gems/faulty/Faulty/Result) for more
216
+ information. Here we use it to check whether the result is `ok?` (not an error).
217
+ If it is we set the users variable, otherwise we set a default of an empty
218
+ array. This pattern is so common, that `Result` also implements a helper method
219
+ `or_default` to do the same thing:
166
220
 
167
221
  ```ruby
168
222
  users = Faulty.circuit(:api).try_run do
@@ -170,38 +224,58 @@ users = Faulty.circuit(:api).try_run do
170
224
  end.or_default([])
171
225
  ```
172
226
 
173
- ## How it Works
227
+ If you want to globally wrap your core dependencies, like your cache or
228
+ database, you may want to look at [Patches](#patches), which can automatically
229
+ wrap your connections in a Faulty circuit.
174
230
 
175
- Faulty implements a version of circuit breakers inspired by
176
- [Martin Fowler's post][martin fowler] on the subject. A few notable features of
177
- Faulty's implementation are:
231
+ See [Running a Circuit](#running-a-circuit) for more in-depth examples. Also,
232
+ make sure you have proper [Event Handlers](#event-handling) setup so that you
233
+ can monitor your circuits for failures.
178
234
 
179
- - Rate-based failure thresholds
180
- - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
181
- cache jitter and error fallback.
182
- - Event-based monitoring
235
+ ## What is this for?
183
236
 
184
- Following the principals of the circuit-breaker pattern, the block given to
185
- `run` or `try_run` will always be executed as long as it never raises an error.
186
- If the block _does_ raise an error, then the circuit keeps track of the number
187
- of runs and the failure rate.
237
+ Circuit breakers are a fault-tolerance tool for creating separation between your
238
+ application and external dependencies. For example, your application may call an
239
+ external API to send a text message:
188
240
 
189
- Once both thresholds are breached, the circuit is opened. Once open, the
190
- circuit starts the cool-down period. Any executions within that cool-down are
191
- skipped, and a `Faulty::OpenCircuitError` will be raised.
241
+ ```ruby
242
+ TextApi.send(message)
243
+ ```
192
244
 
193
- After the cool-down has elapsed, the circuit enters the half-open state. In this
194
- state, Faulty allows a single execution of the block as a test run. If the test
195
- run succeeds, the circuit is fully opened and the circuit state is reset. If the
196
- test run fails, the circuit is closed and the cool-down is reset.
245
+ In normal operation, this API call is very fast. However what if the texting
246
+ service started hanging? Your application would quickly use up a lot of
247
+ resources waiting for requests to return from the service. You could consider
248
+ adding a timeout to your request:
197
249
 
198
- Each time the circuit changes state or executes the block, events are raised
199
- that are sent to the Faulty event notifier. The notifier should be used to track
200
- circuit failure rates, open circuits, etc.
250
+ ```ruby
251
+ TextApi.send(message, timeout: 5)
252
+ ```
201
253
 
202
- In addition to the classic circuit breaker design, Faulty implements caching
203
- that is integrated with the circuit state. See [Caching](#caching) for more
204
- detail.
254
+ Now your application will terminate requests after 5 seconds, but that could
255
+ still add up to a lot of resources if you call this thousands of times. Circuit
256
+ breakers solve this problem.
257
+
258
+ ```ruby
259
+ Faulty.circuit('text_api').run do
260
+ TextApi.send(message, timeout: 5)
261
+ end
262
+ ```
263
+
264
+ Now, when the text API hangs, the first few will run and start timing out. This
265
+ will trip the circuit. After the circuit trips
266
+ (see [How it Works](#how-it-works)), calls to the text API will be paused for
267
+ the configured cool down period. Your application resources are not overwhelmed.
268
+
269
+ You are free to implement a fallback or error handling however you wish, for
270
+ example, in this case, you might add the text message to a failure queue:
271
+
272
+ ```ruby
273
+ Faulty.circuit('text_api').run do
274
+ TextApi.send(message, timeout: 5)
275
+ rescue Faulty::CircuitError => e
276
+ FailureQueue.enqueue(message)
277
+ end
278
+ ```
205
279
 
206
280
  ## Configuration
207
281
 
@@ -215,11 +289,22 @@ Faulty.init do |config|
215
289
  # The cache backend to use. By default, Faulty looks for a Rails cache. If
216
290
  # that's not available, it uses an ActiveSupport::Cache::Memory instance.
217
291
  # Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
292
+ # Whatever backend is given here is automatically wrapped in
293
+ # Faulty::Cache::AutoWire. This adds fault-tolerance features, see the
294
+ # AutoWire API docs for more details.
218
295
  config.cache = Faulty::Cache::Default.new
219
296
 
297
+ # A hash of default options to be used when creating new Circuits.
298
+ # See Circuit Options below for a full list of these
299
+ config.circuit_defaults = {}
300
+
220
301
  # The storage backend. By default, Faulty uses an in-memory store. For most
221
302
  # production applications, you'll want a more robust backend. Faulty also
222
303
  # provides Faulty::Storage::Redis for this.
304
+ # Whatever backend is given here is automatically wrapped in
305
+ # Faulty::Storage::AutoWire. This adds fault-tolerance features, see the
306
+ # AutoWire APi docs for more details. If an array of storage backends is
307
+ # given, each one will be tried in order until one succeeds.
223
308
  config.storage = Faulty::Storage::Memory.new
224
309
 
225
310
  # An array of event listeners. Each object in the array should implement
@@ -260,247 +345,545 @@ hash. For example, `Faulty.init` could be called like this:
260
345
  Faulty.init(cache: Faulty::Cache::Null.new)
261
346
  ```
262
347
 
263
- ## Circuit Options
348
+ ### Configuring the Storage Backend
264
349
 
265
- A circuit can be created with the following configuration options. Those options
266
- are only set once, synchronized across threads, and will persist in-memory until
267
- the process exits. If you're using [multiple configurations](#multiple-configurations),
268
- the options are retained within the context of each instance. All options given
269
- after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
350
+ A storage backend is required to use Faulty. By default, it uses in-memory
351
+ storage, but Redis is also available, along with a number of wrappers used to
352
+ improve resiliency and fault-tolerance.
270
353
 
271
- This is because the circuit objects themselves are internally memoized, and are
272
- read-only once created.
354
+ #### Memory
273
355
 
274
- The following example represents the defaults for a new circuit:
356
+ The
357
+ [`Faulty::Storage::Memory`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/Memory)
358
+ backend is the default storage backend. You may prefer this implementation if
359
+ you want to avoid the complexity and potential failure-mode of cross-network
360
+ circuit storage. The trade-off is that circuit state is only contained within a
361
+ single process and will not be saved across application restarts. Locks will
362
+ also be cleared on restart.
275
363
 
276
- ```ruby
277
- Faulty.circuit(:api) do |config|
278
- # The cache backend for this circuit. Inherits the global cache by default.
279
- config.cache = Faulty.options.cache
364
+ The default configuration:
280
365
 
281
- # The number of seconds before a cache entry is expired. After this time, the
282
- # cache entry may be fully deleted. If set to nil, the cache will not expire.
283
- config.cache_expires_in = 86400
366
+ ```ruby
367
+ Faulty.init do |config|
368
+ config.storage = Faulty::Storage::Memory.new do |storage|
369
+ # The maximum number of circuit runs that will be stored
370
+ storage.max_sample_size = 100
371
+ end
372
+ end
373
+ ```
284
374
 
285
- # The number of seconds before a cache entry should be refreshed. See the
286
- # Caching section for more detail. A value of nil disables cache refreshing.
287
- config.cache_refreshes_after = 900
375
+ #### Redis
288
376
 
289
- # The number of seconds to add or subtract from cache_refreshes_after
290
- # when determining whether a cache entry should be refreshed. Helps mitigate
291
- # the "thundering herd" effect
292
- config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
377
+ The [`Faulty::Storage::Redis`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/Redis)
378
+ backend provides distributed circuit storage using Redis. Although Faulty takes
379
+ steps to reduce risk (See [Fault Tolerance](#fault-tolerance)), using
380
+ cross-network storage does introduce some additional failure modes. To reduce
381
+ this risk, be sure to set conservative timeouts for your Redis connection.
382
+ Setting high timeouts will print warnings to stderr.
293
383
 
294
- # After a circuit is opened, the number of seconds to wait before moving the
295
- # circuit to half-open.
296
- config.cool_down = 300
384
+ The default configuration:
297
385
 
298
- # The errors that will be captured by Faulty and used to trigger circuit
299
- # state changes.
300
- config.errors = [StandardError]
386
+ ```ruby
387
+ Faulty.init do |config|
388
+ config.storage = Faulty::Storage::Redis.new do |storage|
389
+ # The Redis client. Accepts either a Redis instance, or a ConnectionPool
390
+ # of Redis instances. A low timeout is highly recommended to prevent
391
+ # cascading failures when evaluating circuits.
392
+ storage.client = ::Redis.new(timeout: 1)
301
393
 
302
- # Errors that should be ignored by Faulty and not captured.
303
- config.exclude = []
394
+ # The prefix to prepend to all redis keys used by Faulty circuits
395
+ storage.key_prefix = 'faulty'
304
396
 
305
- # The event notifier. Inherits the global notifier by default
306
- config.notifier = Faulty.options.notifier
397
+ # A string to separate the parts of the redis key
398
+ storage.key_separator = ':'
307
399
 
308
- # The minimum failure rate required to trip a circuit
309
- config.rate_threshold = 0.5
400
+ # The maximum number of circuit runs that will be stored
401
+ storage.max_sample_size = 100
310
402
 
311
- # The minimum number of runs required before a circuit can trip
312
- config.sample_threshold = 3
403
+ # The maximum number of seconds that a circuit run will be stored
404
+ storage.sample_ttl = 1800
313
405
 
314
- # The storage backend for this circuit. Inherits the global storage by default
315
- config.storage = Faulty.options.storage
316
- end
317
- ```
406
+ # The maximum number of seconds to store a circuit. Does not apply to
407
+ # locks, which are indefinite.
408
+ storage.circuit_ttl = 604_800 # 1 Week
318
409
 
319
- Following the same convention as `Faulty.init`, circuits can also be created
320
- with an options hash:
410
+ # The number of seconds between circuit expirations. Changing this setting
411
+ # is not recommended. See API docs for more implementation details.
412
+ storage.list_granularity = 3600
321
413
 
322
- ```ruby
323
- Faulty.circuit(:api, cache_expires_in: 1800)
414
+ # If true, disables warnings about recommended client settings like timeouts
415
+ storage.disable_warnings = false
416
+ end
417
+ end
324
418
  ```
325
419
 
326
- ## Caching
420
+ #### FallbackChain
327
421
 
328
- Faulty integrates caching into it's circuits in a way that is particularly
329
- suited to fault-tolerance. To make use of caching, you must specify the `cache`
330
- configuration option when initializing Faulty or creating a new Faulty instance.
331
- If you're using Rails, this is automatically set to the Rails cache.
422
+ The [`Faulty::Storage::FallbackChain`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/FallbackChain)
423
+ backend is a wrapper for multiple prioritized storage backends. If the first
424
+ backend in the chain fails, consecutive backends are tried until one succeeds.
425
+ The recommended use-case for this is to fall back on reliable storage if a
426
+ networked storage backend fails.
332
427
 
333
- Once your cache is configured, you can use the `cache` parameter when running
334
- a circuit to specify a cache key:
428
+ For example, you may configure Redis as your primary storage backend, with an
429
+ in-memory storage backend as a fallback:
335
430
 
336
431
  ```ruby
337
- feed = Faulty.circuit(:rss_feeds)
338
- .try_run(cache: "rss_feeds/#{feed}") do
339
- fetch_feed(feed)
340
- end.or_default([])
432
+ Faulty.init do |config|
433
+ config.storage = Faulty::Storage::FallbackChain.new([
434
+ Faulty::Storage::Redis.new,
435
+ Faulty::Storage::Memory.new
436
+ ])
437
+ end
341
438
  ```
342
439
 
343
- By default a circuit has the following options:
440
+ Faulty instances will automatically use a fallback chain if an array is given to
441
+ the `storage` option, so this example is equivalent to the above:
344
442
 
345
- - `cache_expires_in`: 86400 (1 day). This is sent to the cache backend and
346
- defines how long the cache entry should be stored. After this time elapses,
347
- queries will result in a cache miss.
348
- - `cache_refreshes_after`: 900 (15 minutes). This is used internally by Faulty
349
- to indicate when a cache should be refreshed. It does not affect how long the
350
- cache entry is stored.
351
- - `cache_refresh_jitter`: 180 (3 minutes = 20% of `cache_refreshes_after`). The
352
- maximum number of seconds to randomly add or subtract from
353
- `cache_refreshes_after` when determining whether to refresh a cache entry.
354
- This mitigates the "thundering herd" effect caused by many processes
355
- simultaneously refreshing the cache.
443
+ ```ruby
444
+ Faulty.init do |config|
445
+ config.storage = [
446
+ Faulty::Storage::Redis.new,
447
+ Faulty::Storage::Memory.new
448
+ ]
449
+ end
450
+ ```
356
451
 
357
- This code will attempt to fetch an RSS feed protected by a circuit. If the feed
358
- is within the cache refresh period, then the result will be returned from the
359
- cache and the block will not be executed regardless of the circuit state.
452
+ If the fallback chain fails-over to backup storage, circuit states will not
453
+ carry over, so failover could be temporarily disruptive to your application.
454
+ However, any calls to `#lock` or `#unlock` will always be persisted to all
455
+ backends so that locks are maintained during failover.
360
456
 
361
- If the cache is hit, but outside its refresh period, then Faulty will check the
362
- circuit state. If the circuit is closed or half-open, then it will run the
363
- block. If the block is successful, then it will update the circuit, write to the
364
- cache and return the new value.
457
+ #### Storage::FaultTolerantProxy
365
458
 
366
- However, if the cache is hit and the block fails, then that failure is noted
367
- in the circuit and Faulty returns the cached value.
459
+ This wrapper is applied to all non-fault-tolerant storage backends by default
460
+ (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/AutoWire)).
368
461
 
369
- If the circuit is open and the cache is hit, then Faulty will always return the
370
- cached value.
462
+ [`Faulty::Storage::FaultTolerantProxy`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/FaultTolerantProxy)
463
+ is a wrapper that suppresses storage errors and returns sensible defaults during
464
+ failures. If a storage backend is failing, all circuits will be treated as
465
+ closed regardless of locks or previous history.
371
466
 
372
- If the cache query results in a miss, then faulty operates as normal. In the
373
- code above, if the circuit is closed, the block will be executed. If the block
374
- succeeds, the cache is refreshed. If the block fails, the default of `[]` will
375
- be returned.
467
+ If you wish your application to use a secondary storage backend instead of
468
+ failing closed, use [`FallbackChain`](#storagefallbackchain).
376
469
 
377
- ## Fault Tolerance
470
+ #### Storage::CircuitProxy
378
471
 
379
- Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
380
- the storage or cache backends are captured and suppressed. Failure events for
381
- these errors are sent to the notifier.
472
+ This wrapper is applied to all non-fault-tolerant storage backends by default
473
+ (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/AutoWire)).
382
474
 
383
- If the storage backend fails, all circuits will default to open. If the cache
384
- backend fails, all cache queries will miss.
475
+ [`Faulty::Storage::CircuitProxy`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/CircuitProxy)
476
+ is a wrapper that uses an independent in-memory circuit to track failures to
477
+ storage backends. If a storage backend fails continuously, it will be
478
+ temporarily disabled and raise `Faulty::CircuitError`s.
385
479
 
386
- ## Event Handling
480
+ Typically this is used inside a [`FaultTolerantProxy`](#storagefaulttolerantproxy) or
481
+ [`FallbackChain`](#storagefallbackchain) so that these storage failures are handled
482
+ gracefully.
387
483
 
388
- Faulty uses an event-dispatching model to deliver notifications of internal
389
- events. The full list of events is available from `Faulty::Events::EVENTS`.
484
+ ### Configuring the Cache Backend
390
485
 
391
- - `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
392
- - `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
393
- - `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
394
- - `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
395
- - `circuit_closed` - A circuit closed. Payload: `circuit`
396
- - `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
397
- `status`, `error`
398
- - `circuit_opened` - A circuit execution caused the circuit to open. Payload
399
- `circuit`, `error`
400
- - `circuit_reopened` - A circuit execution cause the circuit to reopen from
401
- half-open. Payload: `circuit`, `error`.
402
- - `circuit_skipped` - A circuit execution was skipped because the circuit is
403
- closed. Payload: `circuit`
404
- - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
405
- `status`
406
- - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
407
- be nil), `action`, `error`
486
+ #### Null
408
487
 
409
- By default events are logged using `Faulty::Events::LogListener`, but that can
410
- be replaced, or additional listeners can be added.
488
+ The [`Faulty::Cache::Null`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/Null)
489
+ cache disables caching. It is the default if Rails and ActiveSupport are not
490
+ present.
411
491
 
412
- ### CallbackListener
492
+ #### Rails
413
493
 
414
- The callback listener is useful for ad-hoc handling of events. You can specify
415
- an event handler by calling a method on the callback handler by the same name.
494
+ [`Faulty::Cache::Rails`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/Rails)
495
+ is the default cache if Rails or ActiveSupport are present. If Rails is present,
496
+ it uses `Rails.cache` as the backend. If ActiveSupport is present, but Rails is
497
+ not, it creates a new `ActiveSupport::Cache::MemoryStore` by default. This
498
+ backend can be used with any `ActiveSupport::Cache`.
416
499
 
417
500
  ```ruby
418
501
  Faulty.init do |config|
419
- # Replace the default listener with a custom callback listener
420
- listener = Faulty::Events::CallbackListener.new do |events|
421
- events.circuit_opened do |payload|
422
- MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
423
- end
424
- end
425
- config.listeners = [listener]
502
+ config.cache = Faulty::Cache::Rails.new(
503
+ ActiveSupport::Cache::RedisCacheStore.new
504
+ )
426
505
  end
427
506
  ```
428
507
 
429
- ### Other Built-in Listeners
508
+ #### Cache::FaultTolerantProxy
430
509
 
431
- In addition to the log and callback listeners, Faulty intends to implement
432
- built-in service-specific handlers to make it easy to integrate with monitoring
433
- and reporting software.
510
+ This wrapper is applied to all non-fault-tolerant cache backends by default
511
+ (see the API docs for `Faulty::Cache::AutoWire`).
434
512
 
435
- - `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
436
- the Honeybadger error reporting service.
513
+ [`Faulty::Cache::FaultTolerantProxy`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/FaultTolerantProxy)
514
+ is a wrapper that suppresses cache errors and acts like a null cache during
515
+ failures. Reads always return `nil`, and writes are no-ops.
437
516
 
438
- ### Custom Listeners
517
+ #### Cache::CircuitProxy
439
518
 
440
- You can implement your own listener by following the documentation in
441
- `Faulty::Events::ListenerInterface`. For example:
519
+ This wrapper is applied to all non-fault-tolerant circuit backends by default
520
+ (see the API docs for `Faulty::Circuit::AutoWire`).
442
521
 
443
- ```ruby
444
- class MyFaultyListener
445
- def handle(event, payload)
446
- MyNotifier.alert(event, payload)
447
- end
522
+ [`Faulty::Cache::CircuitProxy`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/CircuitProxy)
523
+ is a wrapper that uses an independent in-memory circuit to track failures to
524
+ cache backends. If a cache backend fails continuously, it will be
525
+ temporarily disabled and raise `Faulty::CircuitError`s.
526
+
527
+ Typically this is used inside a
528
+ [`FaultTolerantProxy`](#cachefaulttolerantproxy) so that these cache failures
529
+ are handled gracefully.
530
+
531
+ ### Multiple Configurations
532
+
533
+ It is possible to have multiple configurations of Faulty running within the same
534
+ process. The most common setup is to simply use `Faulty.init` to
535
+ configure Faulty globally, however it is possible to have additional
536
+ configurations.
537
+
538
+ #### The default instance
539
+
540
+ When you call [`Faulty.init`](https://www.rubydoc.info/gems/faulty/Faulty.init),
541
+ you are actually creating the default instance of `Faulty`. You can access this
542
+ instance directly by calling
543
+ [`Faulty.default`](https://www.rubydoc.info/gems/faulty/Faulty.default).
544
+
545
+ ```ruby
546
+ # We create the default instance
547
+ Faulty.init
548
+
549
+ # Access the default instance
550
+ faulty = Faulty.default
551
+
552
+ # Alternatively, access the instance by name
553
+ faulty = Faulty[:default]
554
+ ```
555
+
556
+ You can rename the default instance if desired:
557
+
558
+ ```ruby
559
+ Faulty.init(:custom_default)
560
+
561
+ instance = Faulty.default
562
+ instance = Faulty[:custom_default]
563
+ ```
564
+
565
+ #### Multiple Instances
566
+
567
+ If you want multiple instance, but want global, thread-safe access to
568
+ them, you can use
569
+ [`Faulty.register`](https://www.rubydoc.info/gems/faulty/Faulty.register):
570
+
571
+ ```ruby
572
+ api_faulty = Faulty.new do |config|
573
+ # This accepts the same options as Faulty.init
448
574
  end
575
+
576
+ Faulty.register(:api, api_faulty)
577
+
578
+ # Now access the instance globally
579
+ Faulty[:api]
449
580
  ```
450
581
 
582
+ When you call [`Faulty.circuit`](https://www.rubydoc.info/gems/faulty/Faulty.circuit),
583
+ that's the same as calling `Faulty.default.circuit`, so you can apply the same
584
+ principal to any other registered Faulty instance:
585
+
451
586
  ```ruby
452
- Faulty.init do |config|
453
- config.listeners = [MyFaultyListener.new]
587
+ Faulty[:api].circuit('api_circuit').run { 'ok' }
588
+ ```
589
+
590
+ You can also create and register a Faulty instance in one step:
591
+
592
+ ```ruby
593
+ Faulty.register(:api) do |config|
594
+ # This accepts the same options as Faulty.init
454
595
  end
455
596
  ```
456
597
 
457
- ## Configuring the Storage Backend
598
+ #### Standalone Instances
599
+
600
+ If you choose, you can use Faulty instances without registering them globally by
601
+ simply calling [`Faulty.new`](https://www.rubydoc.info/gems/faulty/Faulty:initialize).
602
+ This is more object-oriented and is necessary if you use dependency injection.
603
+
604
+ ```ruby
605
+ faulty = Faulty.new
606
+ faulty.circuit('standalone_circuit')
607
+ ```
608
+
609
+ Calling `#circuit` on the instance still has the same memoization behavior that
610
+ `Faulty.circuit` has, so subsequent calls to the same circuit will return a
611
+ memoized circuit object.
458
612
 
459
- ### Memory
460
613
 
461
- The `Faulty::Cache::Memory` backend is the default storage backend. The default
462
- configuration:
614
+ ## Working with circuits
615
+
616
+ A circuit can be created by calling the `#circuit` method on `Faulty`, or on
617
+ your Faulty instance:
463
618
 
464
619
  ```ruby
465
- Faulty.init do |config|
466
- config.storage = Faulty::Storage::Memory.new do |storage|
467
- # The maximum number of circuit runs that will be stored
468
- storage.max_sample_size = 100
620
+ # With global Faulty configuration
621
+ circuit = Faulty.circuit('api')
622
+
623
+ # Or with a Faulty instance
624
+ circuit = faulty.circuit('api')
625
+ ```
626
+
627
+ ### Running a Circuit
628
+
629
+ You can handle circuit errors either with exceptions, or with a Faulty
630
+ [`Result`](https://www.rubydoc.info/gems/faulty/Faulty/Result). They both have
631
+ the same behavior, but you can choose whatever syntax is more convenient for
632
+ your use-case.
633
+
634
+ #### With Exceptions
635
+
636
+ If we want exceptions to be raised, we use the
637
+ [`#run`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:run) method. This
638
+ does not suppress exceptions, only monitors them. If `api.users` raises an
639
+ exception here, it will bubble up to the caller. The exception will be a
640
+ sub-class of [`Faulty::CircuitError`](https://www.rubydoc.info/gems/faulty/Faulty/CircuitError),
641
+ and the error `cause` will be the original error object.
642
+
643
+ ```ruby
644
+ begin
645
+ Faulty.circuit('api').run do
646
+ api.users
469
647
  end
648
+ rescue Faulty::CircuitError => e
649
+ e.cause # The original error
470
650
  end
471
651
  ```
472
652
 
473
- ### Redis
653
+ #### With Faulty::Result
474
654
 
475
- The `Faulty::Cache::Redis` backend provides distributed circuit storage using
476
- Redis. The default configuration:
655
+ Sometimes exception handling is awkward to deal with, and could cause a lot of
656
+ extra boilerplate code. In simple cases, it's can be more concise to allow
657
+ Faulty to capture exceptions. Use the
658
+ [`#try_run`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:try_run) method
659
+ for this.
477
660
 
478
661
  ```ruby
479
- Faulty.init do |config|
480
- config.storage = Faulty::Storage::Redis.new do |storage|
481
- # The Redis client. Accepts either a Redis instance, or a ConnectionPool
482
- # of Redis instances.
483
- storage.client = ::Redis.new
662
+ result = Faulty.circuit('api').try_run do
663
+ api.users
664
+ end
665
+ ```
484
666
 
485
- # The prefix to prepend to all redis keys used by Faulty circuits
486
- storage.key_prefix = 'faulty'
667
+ The `result` variable is an instance of
668
+ [`Faulty::Result`](https://www.rubydoc.info/gems/faulty/Faulty/Result). A result
669
+ can either be an error if the circuit failed, or an "ok" value if it succeeded.
487
670
 
488
- # A string to separate the parts of the redis key
489
- storage.key_separator = ':'
671
+ You can check whether it's an error with the `ok?` or `error?` method.
490
672
 
491
- # The maximum number of circuit runs that will be stored
492
- storage.max_sample_size = 100
673
+ ```ruby
674
+ if result.ok?
675
+ users = result.get
676
+ else
677
+ error = result.error
678
+ end
679
+ ```
493
680
 
494
- # The maximum number of seconds that a circuit run will be stored
495
- storage.sample_ttl = 1800
496
- end
681
+ Sometimes you want your application to crash when a circuit fails, but other
682
+ times, you might want to return a default or fallback value. The `Result` object
683
+ has a method [`#or_default`](https://www.rubydoc.info/gems/faulty/Faulty/Result:or_default)
684
+ to do that.
685
+
686
+ ```ruby
687
+ # Users will be nil if the result is an error
688
+ users = result.or_default
689
+
690
+ # Users will be an empty array if the result is an error
691
+ users = result.or_default([])
692
+
693
+ # Users will be the return value of the block
694
+ users = result.or_default do
695
+ # ...
696
+ end
697
+ ```
698
+
699
+ As we showed in the [Basic Usage](#basic-usage) section, you can put this
700
+ together in a nice one-liner.
701
+
702
+ ```ruby
703
+ Faulty.circuit('api').try_run { api.users }.or_default([])
704
+ ```
705
+
706
+ ### Specifying the Captured Errors
707
+
708
+ By default, Faulty circuits will capture all `StandardError` errors, but
709
+ sometimes you might not want every error to count as a circuit failure. For
710
+ example, an HTTP 404 Not Found response typically should not cause a circuit to
711
+ fail. You can customize the errors that Faulty captures
712
+
713
+ ```ruby
714
+ Faulty.circuit('api', errors: [Net::HTTPServerException]).run do
715
+ # If this raises any exception other than Net::HTTPServerException
716
+ # Faulty will not capture it at all, and it will not count as a circuit failure
717
+ api.find_user(3)
718
+ end
719
+ ```
720
+
721
+ Or, if you'd instead like to specify errors to be excluded:
722
+
723
+ ```ruby
724
+ Faulty.circuit('api', exclude: [Net::HTTPClientException]).run do
725
+ # If this raises a Net::HTTPClientException, Faulty will not capture it
726
+ api.find_user(3)
727
+ end
728
+ ```
729
+
730
+ Both options can even be specified together.
731
+
732
+ ```ruby
733
+ Faulty.circuit(
734
+ 'api',
735
+ errors: [ActiveRecord::ActiveRecordError]
736
+ exclude: [ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique]
737
+ ).run do
738
+ # This only captures ActiveRecord::ActiveRecordError errors, but not
739
+ # ActiveRecord::RecordNotFound or ActiveRecord::RecordNotUnique errors
740
+ user = User.find(3)
741
+ user.save!
742
+ end
743
+ ```
744
+
745
+ ### Using the Cache
746
+
747
+ Circuit runs can be given a cache key, and if they are, the result of the circuit
748
+ block will be cached. Calls to that circuit block will try to fetch from the
749
+ cache, and only execute the block if the cache misses.
750
+
751
+ ```ruby
752
+ Faulty.circuit('api').run(cache: 'all_users') do
753
+ api.users
754
+ end
755
+ ```
756
+
757
+ The cache will be refreshed (meaning the circuit will be allowed to execute)
758
+ after `cache_refreshes_after` (default 900 second). However, the value remains
759
+ stored in the cache for `cache_expires_in` (default 86400 seconds, 1 day). If
760
+ the circuit fails, the last cached value will be returned even if
761
+ `cache_refreshes_after` has passed.
762
+
763
+ See the [Caching](#caching) section for more details on Faulty's caching
764
+ strategy.
765
+
766
+ ### Configuring the Circuit Threshold
767
+
768
+ To configure how a circuit responds to error, use the `cool_down`,
769
+ `rate_threshold` and `sample_threshold` options.
770
+
771
+ #### Rate Threshold
772
+
773
+ The first option to look at is `rate_threshold`. This specifies the percentage
774
+ of circuit runs that must fail before a circuit is opened.
775
+
776
+ ```ruby
777
+ # This circuit must fail 70% of the time before the circuit will be tripped
778
+ Faulty.circuit('api', rate_threshold: 0.7).run { api.users }
779
+ ```
780
+
781
+ #### Sample Threshold
782
+
783
+ We typically don't want circuits to trip immediately if the first execution
784
+ fails. This is why we have the `sample_threshold` option. The circuit will never
785
+ be tripped until we record at least this number of executions.
786
+
787
+ ```ruby
788
+ # This circuit must run 10 times before it is allowed to trip. Those 10 runs
789
+ # can be successes or fails. If at least 70% of them are failures, the circuit
790
+ # will be opened.
791
+ Faulty.circuit('api', sample_threshold: 10, rate_threshold: 0.7).run { api.users }
792
+ ```
793
+
794
+ #### Cool Down
795
+
796
+ The `cool_down` option specifies how much time to wait after a circuit is
797
+ opened. During this period, the circuit will not be executed. After the cool
798
+ down elapses, the circuit enters the "half open" state, and execution can be
799
+ retried. See [How it Works](#how-it-works).
800
+
801
+ ```ruby
802
+ # If this circuit trips, it will skip executions for 120 seconds before retrying
803
+ Faulty.circuit('api', cool_down: 120).run { api.users }
804
+ ```
805
+
806
+ ### Circuit Options
807
+
808
+ A circuit can be created with the following configuration options. Those options
809
+ are only set once, synchronized across threads, and will persist in-memory until
810
+ the process exits. If you're using [multiple configurations](#multiple-configurations),
811
+ the options are retained within the context of each instance. All options given
812
+ after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
813
+
814
+ ```ruby
815
+ Faulty.circuit('api', rate_threshold: 0.7)
816
+
817
+ # These options are ignored since with already initialized the circuit
818
+ circuit = Faulty.circuit('api', rate_threshold: 0.3)
819
+ circuit.options.rate_threshold # => 0.7
820
+ ```
821
+
822
+ This is because the circuit objects themselves are internally memoized, and are
823
+ read-only once created.
824
+
825
+ The following example represents the defaults for a new circuit:
826
+
827
+ ```ruby
828
+ Faulty.circuit('api') do |config|
829
+ # The cache backend for this circuit. Inherits the global cache by default.
830
+ config.cache = Faulty.options.cache
831
+
832
+ # The number of seconds before a cache entry is expired. After this time, the
833
+ # cache entry may be fully deleted. If set to nil, the cache will not expire.
834
+ config.cache_expires_in = 86400
835
+
836
+ # The number of seconds before a cache entry should be refreshed. See the
837
+ # Caching section for more detail. A value of nil disables cache refreshing.
838
+ config.cache_refreshes_after = 900
839
+
840
+ # The number of seconds to add or subtract from cache_refreshes_after
841
+ # when determining whether a cache entry should be refreshed. Helps mitigate
842
+ # the "thundering herd" effect
843
+ config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
844
+
845
+ # After a circuit is opened, the number of seconds to wait before moving the
846
+ # circuit to half-open.
847
+ config.cool_down = 300
848
+
849
+ # The number of seconds of history that is considered when calculating
850
+ # the circuit failure rate. The length of the sliding window.
851
+ config.evaluation_window = 60
852
+
853
+ # The errors that will be captured by Faulty and used to trigger circuit
854
+ # state changes.
855
+ config.errors = [StandardError]
856
+
857
+ # Errors that should be ignored by Faulty and not captured.
858
+ config.exclude = []
859
+
860
+ # The event notifier. Inherits the Faulty instance notifier by default
861
+ config.notifier = Faulty.options.notifier
862
+
863
+ # The minimum failure rate required to trip a circuit
864
+ config.rate_threshold = 0.5
865
+
866
+ # The minimum number of runs required before a circuit can trip
867
+ config.sample_threshold = 3
868
+
869
+ # The storage backend for this circuit. Inherits the Faulty instance storage
870
+ # by default
871
+ config.storage = Faulty.options.storage
497
872
  end
498
873
  ```
499
874
 
500
- ## Listing Circuits
875
+ Following the same convention as `Faulty.init`, circuits can also be created
876
+ with an options hash:
877
+
878
+ ```ruby
879
+ Faulty.circuit(:api, cache_expires_in: 1800)
880
+ ```
881
+
882
+ ### Listing Circuits
501
883
 
502
884
  For monitoring or debugging, you may need to retrieve a list of all circuit
503
- names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
885
+ names. This is possible with [`Faulty.list_circuits`](https://www.rubydoc.info/gems/faulty/Faulty.list_circuits)
886
+ (or [`Faulty#list_circuits`](https://www.rubydoc.info/gems/faulty/Faulty:list_circuits)
504
887
  if you're using an instance).
505
888
 
506
889
  You can get a list of all circuit statuses by mapping those names to their
@@ -513,7 +896,7 @@ statuses = Faulty.list_circuits.map do |name|
513
896
  end
514
897
  ```
515
898
 
516
- ## Locking Circuits
899
+ ### Locking Circuits
517
900
 
518
901
  It is possible to lock a circuit open or closed. A circuit that is locked open
519
902
  will never execute its block, and always raise an `Faulty::OpenCircuitError`.
@@ -521,8 +904,12 @@ This is useful in cases where you need to manually disable a dependency
521
904
  entirely. If a cached value is available, that will be returned from the circuit
522
905
  until it expires, even outside its refresh period.
523
906
 
907
+ * [`lock_open!`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:lock_open!)
908
+ * [`lock_closed!`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:lock_closed!)
909
+ * [`unlock!`](https://www.rubydoc.info/gems/faulty/Faulty/Circuit:unlock!)
910
+
524
911
  ```ruby
525
- Faulty.circuit(:broken_api).lock_open!
912
+ Faulty.circuit('broken_api').lock_open!
526
913
  ```
527
914
 
528
915
  A circuit that is locked closed will never trip. This is useful in cases where a
@@ -530,94 +917,320 @@ circuit is continuously tripping incorrectly. If a cached value is available, it
530
917
  will have the same behavior as an unlocked circuit.
531
918
 
532
919
  ```ruby
533
- Faulty.circuit(:false_positive).lock_closed!
920
+ Faulty.circuit('false_positive').lock_closed!
534
921
  ```
535
922
 
536
923
  To remove a lock of either type:
537
924
 
538
925
  ```ruby
539
- Faulty.circuit(:fixed).unlock!
926
+ Faulty.circuit('fixed').unlock!
540
927
  ```
541
928
 
542
929
  Locking or unlocking a circuit has no concurrency guarantees, so it's not
543
930
  recommended to lock or unlock circuits from production code. Instead, locks are
544
931
  intended as an emergency tool for troubleshooting and debugging.
545
932
 
546
- ## Multiple Configurations
933
+ ## Patches
547
934
 
548
- It is possible to have multiple configurations of Faulty running within the same
549
- process. The most common setup is to simply use `Faulty.init` to
550
- configure Faulty globally, however it is possible to have additional
551
- configurations.
935
+ For certain core dependencies like a cache or a database connection, it is
936
+ inconvenient to wrap every call in its own circuit. Faulty provides some patches
937
+ to wrap these calls in a circuit automatically. To use a patch, it first needs
938
+ to be loaded. Since patches modify third-party code, they are not automatically
939
+ required with the Faulty gem, so they need to be required individually.
552
940
 
553
- ### The default instance
941
+ ```ruby
942
+ require 'faulty'
943
+ require 'faulty/patch/redis'
944
+ ```
554
945
 
555
- When you call `Faulty.init`, you are actually creating the default instance of
556
- `Faulty`. You can access this instance directly by calling `Faulty.default`.
946
+ Or require them in your `Gemfile`
557
947
 
558
948
  ```ruby
559
- # We create the default instance
560
- Faulty.init
949
+ gem 'faulty', require: %w[faulty faulty/patch/redis]
950
+ ```
561
951
 
562
- # Access the default instance
563
- faulty = Faulty.default
952
+ For core dependencies you'll most likely want to use the in-memory circuit
953
+ storage adapter and not the Redis storage adapter. That way if Redis fails, your
954
+ circuit storage doesn't also fail, causing cascading failures.
564
955
 
565
- # Alternatively, access the instance by name
566
- faulty = Faulty[:default]
956
+ For example, you can use a separate Faulty instance to manage your Mysql2
957
+ circuit:
958
+
959
+ ```ruby
960
+ # Setup your default config. This can use the Redis backend if you prefer
961
+ Faulty.init do |config|
962
+ # ...
963
+ end
964
+
965
+ Faulty.register(:mysql) do |config|
966
+ # Here we decide to set some circuit defaults more useful for
967
+ # frequent database calls
968
+ config.circuit_defaults = {
969
+ cool_down: 20.0,
970
+ evaluation_window: 40,
971
+ sample_threshold: 25
972
+ }
973
+ end
974
+
975
+ # Now we can use our "mysql" faulty instance when constructing a Mysql2 client
976
+ Mysql2::Client.new(host: '127.0.0.1', faulty: { instance: 'mysql2' })
567
977
  ```
568
978
 
569
- You can rename the default instance if desired:
979
+ ### Patch::Redis
980
+
981
+ [`Faulty::Patch::Redis`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Redis)
982
+ protects a Redis client with an internal circuit. Pass a `:faulty` key along
983
+ with your connection options to enable the circuit breaker.
984
+
985
+ The Redis patch supports the Redis gem versions 3 and 4.
570
986
 
571
987
  ```ruby
572
- Faulty.init(:custom_default)
988
+ require 'faulty/patch/redis'
989
+
990
+ redis = Redis.new(url: 'redis://localhost:6379', faulty: {
991
+ # The name for the redis circuit
992
+ name: 'redis'
993
+
994
+ # The faulty instance to use
995
+ # This can also be a registered faulty instance or a constant name. See API
996
+ # docs for more details
997
+ instance: Faulty.default
998
+
999
+ # By default, circuit errors will be subclasses of Redis::BaseError
1000
+ # To disable this behavior, set patch_errors to false and Faulty
1001
+ # will raise its default errors
1002
+ patch_errors: true
1003
+ })
1004
+ redis.connect # raises Faulty::CircuitError if connection fails
1005
+
1006
+ # If the faulty key is not given, no circuit is used
1007
+ redis = Redis.new(url: 'redis://localhost:6379')
1008
+ redis.connect # not protected by a circuit
1009
+ ```
573
1010
 
574
- instance = Faulty.default
575
- instance = Faulty[:custom_default]
1011
+ ### Patch::Mysql2
1012
+
1013
+ [`Faulty::Patch::Mysql2`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Mysql2)
1014
+ protects a `Mysql2::Client` with an internal circuit. Pass a `:faulty` key along
1015
+ with your connection options to enable the circuit breaker.
1016
+
1017
+ Faulty supports the mysql2 gem versions 0.5 and greater.
1018
+
1019
+ Note: Although Faulty supports Ruby 2.3 in general, the Mysql2 patch is not
1020
+ fully supported on Ruby 2.3. It may work for you, but use it at your own risk.
1021
+
1022
+ ```ruby
1023
+ require 'faulty/patch/mysql2'
1024
+
1025
+ mysql = Mysql2::Client.new(host: '127.0.0.1', faulty: {
1026
+ # The name for the Mysql2 circuit
1027
+ name: 'mysql2'
1028
+
1029
+ # The faulty instance to use
1030
+ # This can also be a registered faulty instance or a constant name. See API
1031
+ # docs for more details
1032
+ instance: Faulty.default
1033
+
1034
+ # By default, circuit errors will be subclasses of
1035
+ # Mysql2::Error::ConnectionError
1036
+ # To disable this behavior, set patch_errors to false and Faulty
1037
+ # will raise its default errors
1038
+ patch_errors: true
1039
+ })
1040
+
1041
+ mysql.query('SELECT * FROM users') # raises Faulty::CircuitError if connection fails
1042
+
1043
+ # If the faulty key is not given, no circuit is used
1044
+ mysql = Mysql2::Client.new(host: '127.0.0.1')
1045
+ mysql.query('SELECT * FROM users') # not protected by a circuit
576
1046
  ```
577
1047
 
578
- ### Multiple Instances
1048
+ ## Event Handling
579
1049
 
580
- If you want multiple instance, but want global, thread-safe access to
581
- them, you can use `Faulty.register`:
1050
+ Faulty uses an event-dispatching model to deliver notifications of internal
1051
+ events. The full list of events is available from
1052
+ [`Faulty::Events::EVENTS`](https://www.rubydoc.info/gems/faulty/Faulty/Events).
1053
+
1054
+ - `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
1055
+ - `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
1056
+ - `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
1057
+ - `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
1058
+ - `circuit_closed` - A circuit closed. Payload: `circuit`
1059
+ - `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
1060
+ `status`, `error`
1061
+ - `circuit_opened` - A circuit execution caused the circuit to open. Payload
1062
+ `circuit`, `error`
1063
+ - `circuit_reopened` - A circuit execution cause the circuit to reopen from
1064
+ half-open. Payload: `circuit`, `error`.
1065
+ - `circuit_skipped` - A circuit execution was skipped because the circuit is
1066
+ open. Payload: `circuit`
1067
+ - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
1068
+ `status`
1069
+ - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
1070
+ be nil), `action`, `error`
1071
+
1072
+ By default events are logged using `Faulty::Events::LogListener`, but that can
1073
+ be replaced, or additional listeners can be added.
1074
+
1075
+ ### CallbackListener
1076
+
1077
+ The [`CallbackListener`](https://www.rubydoc.info/gems/faulty/Faulty/Events/CallbackListener)
1078
+ is useful for ad-hoc handling of events. You can specify an event handler by
1079
+ calling a method on the callback handler by the same name.
582
1080
 
583
1081
  ```ruby
584
- api_faulty = Faulty.new do |config|
585
- # This accepts the same options as Faulty.init
1082
+ Faulty.init do |config|
1083
+ # Replace the default listener with a custom callback listener
1084
+ listener = Faulty::Events::CallbackListener.new do |events|
1085
+ events.circuit_opened do |payload|
1086
+ MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
1087
+ end
1088
+ end
1089
+ config.listeners = [listener]
586
1090
  end
1091
+ ```
587
1092
 
588
- Faulty.register(:api, api_faulty)
1093
+ ### Other Built-in Listeners
589
1094
 
590
- # Now access the instance globally
591
- Faulty[:api]
592
- ```
1095
+ In addition to the log and callback listeners, Faulty intends to implement
1096
+ built-in service-specific handlers to make it easy to integrate with monitoring
1097
+ and reporting software.
1098
+
1099
+ * [`Faulty::Events::LogListener`](https://www.rubydoc.info/gems/faulty/Faulty/Events/LogListener):
1100
+ Logs all circuit events to a specified `Logger` or `$stderr` by default. This
1101
+ is enabled by default if no listeners are specified.
1102
+ * [`Faulty::Events::HoneybadgerListener`](https://www.rubydoc.info/gems/faulty/Faulty/Events/HoneybadgerListener):
1103
+ Reports circuit and backend errors to the Honeybadger error reporting service.
593
1104
 
594
- When you call `Faulty.circuit`, that's the same as calling
595
- `Faulty.default.circuit`, so you can apply the same principal to any other
596
- registered Faulty instance:
1105
+ If your favorite monitoring software is not supported here, please open a PR
1106
+ that implements a listener for it.
1107
+
1108
+ ### Custom Listeners
1109
+
1110
+ You can implement your own listener by following the documentation in
1111
+ [`Faulty::Events::ListenerInterface`](https://www.rubydoc.info/gems/faulty/Faulty/Events/ListenerInterface).
1112
+ For example:
597
1113
 
598
1114
  ```ruby
599
- Faulty[:api].circuit(:api_circuit).run { 'ok' }
1115
+ class MyFaultyListener
1116
+ def handle(event, payload)
1117
+ MyNotifier.alert(event, payload)
1118
+ end
1119
+ end
600
1120
  ```
601
1121
 
602
- ### Standalone Instances
1122
+ ```ruby
1123
+ Faulty.init do |config|
1124
+ config.listeners = [MyFaultyListener.new]
1125
+ end
1126
+ ```
603
1127
 
604
- If you choose, you can use Faulty instances without registering them globally.
605
- This is more object-oriented and is necessary if you use dependency injection.
1128
+ ## How it Works
1129
+
1130
+ Faulty implements a version of circuit breakers inspired by "Release It!: Design
1131
+ and Deploy Production-Ready Software" by [Michael T. Nygard][michael nygard] and
1132
+ [Martin Fowler's post][martin fowler] on the subject. A few notable features of
1133
+ Faulty's implementation are:
1134
+
1135
+ - Rate-based failure thresholds
1136
+ - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
1137
+ cache jitter and error fallback.
1138
+ - Event-based monitoring
1139
+ - Flexible fault-tolerant storage with optional fallbacks
1140
+
1141
+ Following the principals of the circuit-breaker pattern, the block given to
1142
+ `run` or `try_run` will always be executed as long as it never raises an error.
1143
+ If the block _does_ raise an error, then the circuit keeps track of the number
1144
+ of runs and the failure rate.
1145
+
1146
+ Once both thresholds are breached, the circuit is opened. Once open, the
1147
+ circuit starts the cool-down period. Any executions within that cool-down are
1148
+ skipped, and a `Faulty::OpenCircuitError` will be raised.
1149
+
1150
+ After the cool-down has elapsed, the circuit enters the half-open state. In this
1151
+ state, Faulty allows a single execution of the block as a test run. If the test
1152
+ run succeeds, the circuit is fully closed and the circuit state is reset. If the
1153
+ test run fails, the circuit is opened and the cool-down is reset.
1154
+
1155
+ Each time the circuit changes state or executes the block, events are raised
1156
+ that are sent to the Faulty event notifier. The notifier should be used to track
1157
+ circuit failure rates, open circuits, etc.
1158
+
1159
+ In addition to the classic circuit breaker design, Faulty implements caching
1160
+ that is integrated with the circuit state. See [Caching](#caching) for more
1161
+ detail.
1162
+
1163
+ ### Caching
1164
+
1165
+ Faulty integrates caching into it's circuits in a way that is particularly
1166
+ suited to fault-tolerance. To make use of caching, you must specify the `cache`
1167
+ configuration option when initializing Faulty or creating a new Faulty instance.
1168
+ If you're using Rails, this is automatically set to the Rails cache.
1169
+
1170
+ Once your cache is configured, you can use the `cache` parameter when running
1171
+ a circuit to specify a cache key:
606
1172
 
607
1173
  ```ruby
608
- faulty = Faulty.new
609
- faulty.circuit(:standalone_circuit)
1174
+ feed = Faulty.circuit('rss_feeds')
1175
+ .try_run(cache: "rss_feeds/#{feed}") do
1176
+ fetch_feed(feed)
1177
+ end.or_default([])
610
1178
  ```
611
1179
 
612
- Calling `#circuit` on the instance still has the same memoization behavior that
613
- `Faulty.circuit` has, so subsequent calls to the same circuit will return a
614
- memoized circuit object.
1180
+ By default a circuit has the following options:
1181
+
1182
+ - `cache_expires_in`: 86400 (1 day). This is sent to the cache backend and
1183
+ defines how long the cache entry should be stored. After this time elapses,
1184
+ queries will result in a cache miss.
1185
+ - `cache_refreshes_after`: 900 (15 minutes). This is used internally by Faulty
1186
+ to indicate when a cache should be refreshed. It does not affect how long the
1187
+ cache entry is stored.
1188
+ - `cache_refresh_jitter`: 180 (3 minutes = 20% of `cache_refreshes_after`). The
1189
+ maximum number of seconds to randomly add or subtract from
1190
+ `cache_refreshes_after` when determining whether to refresh a cache entry.
1191
+ This mitigates the "thundering herd" effect caused by many processes
1192
+ simultaneously refreshing the cache.
1193
+
1194
+ This code will attempt to fetch an RSS feed protected by a circuit. If the feed
1195
+ is within the cache refresh period, then the result will be returned from the
1196
+ cache and the block will not be executed regardless of the circuit state.
1197
+
1198
+ If the cache is hit, but outside its refresh period, then Faulty will check the
1199
+ circuit state. If the circuit is closed or half-open, then it will run the
1200
+ block. If the block is successful, then it will update the circuit, write to the
1201
+ cache and return the new value.
1202
+
1203
+ However, if the cache is hit and the block fails, then that failure is noted
1204
+ in the circuit and Faulty returns the cached value.
1205
+
1206
+ If the circuit is open and the cache is hit, then Faulty will always return the
1207
+ cached value.
1208
+
1209
+ If the cache query results in a miss, then faulty operates as normal. In the
1210
+ code above, if the circuit is closed, the block will be executed. If the block
1211
+ succeeds, the cache is refreshed. If the block fails, the default of `[]` will
1212
+ be returned.
1213
+
1214
+ ### Fault Tolerance
1215
+
1216
+ Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
1217
+ the storage or cache backends are captured and suppressed. Failure events for
1218
+ these errors are sent to the notifier.
1219
+
1220
+ In case of a flaky storage or cache backend, Faulty also uses independent
1221
+ in-memory circuits to track failures so that we don't keep calling a backend
1222
+ that is failing. See the API docs for [`Cache::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/AutoWire),
1223
+ and [`Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/AutoWire)
1224
+ for more details.
1225
+
1226
+ If the storage backend fails, circuits will default to closed. If the cache
1227
+ backend fails, all cache queries will miss.
615
1228
 
616
1229
  ## Implementing a Cache Backend
617
1230
 
618
1231
  You can implement your own cache backend by following the documentation in
619
- `Faulty::Cache::Interface`. It is a fairly simple API, with only get/set
620
- methods. For example:
1232
+ [`Faulty::Cache::Interface`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/Interface).
1233
+ It is a fairly simple API, with only get/set methods. For example:
621
1234
 
622
1235
  ```ruby
623
1236
  class MyFaultyCache
@@ -646,10 +1259,11 @@ users.
646
1259
  ## Implementing a Storage Backend
647
1260
 
648
1261
  You can implement your own storage backend by following the documentation in
649
- `Faulty::Storage::Interface`. Since the storage has some tricky requirements
650
- regarding concurrency, the `Faulty::Storage::Memory` can be used as a reference
651
- implementation. Feel free to open a pull request if your storage backend
652
- would be useful for other users.
1262
+ [`Faulty::Storage::Interface`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/Interface).
1263
+ Since the storage has some tricky requirements regarding concurrency, the
1264
+ [`Faulty::Storage::Memory`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/Memory)
1265
+ can be used as a reference implementation. Feel free to open a pull request if
1266
+ your storage backend would be useful for other users.
653
1267
 
654
1268
  ## Alternatives
655
1269
 
@@ -660,8 +1274,8 @@ but there are and have been many other options:
660
1274
 
661
1275
  - [semian](https://github.com/Shopify/semian): A resiliency toolkit that
662
1276
  includes circuit breakers. It uses adapters to auto-wire circuits, and it has
663
- only host-local storage by design.
664
- - [circuitbox](https://github.com/yammer/circuitbox): Similar in function to
1277
+ only in-memory storage by design.
1278
+ - [circuitbox](https://github.com/yammer/circuitbox): Similar in design to
665
1279
  Faulty, but with a different API. It uses Moneta to abstract circuit storage
666
1280
  to allow any key-value store.
667
1281
 
@@ -680,10 +1294,12 @@ but there are and have been many other options:
680
1294
 
681
1295
  - Simple API but configurable for advanced users
682
1296
  - Pluggable storage backends (circuitbox also has this)
1297
+ - Protected storage access with fallback to safe storage
683
1298
  - Global, or object-oriented configuration with multiple instances
684
1299
  - Integrated caching support tailored for fault-tolerance
685
1300
  - Manually lock circuits open or closed
686
1301
 
687
1302
  [api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
1303
+ [michael nygard]: https://www.michaelnygard.com/
688
1304
  [martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
689
1305
  [hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works