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