faulty 0.1.4 → 0.5.1

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