faulty 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +44 -0
- data/.yardopts +3 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +559 -0
- data/bin/check-version +10 -0
- data/bin/console +12 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/faulty.gemspec +43 -0
- data/lib/faulty.rb +118 -0
- data/lib/faulty/cache.rb +13 -0
- data/lib/faulty/cache/default.rb +48 -0
- data/lib/faulty/cache/fault_tolerant_proxy.rb +74 -0
- data/lib/faulty/cache/interface.rb +44 -0
- data/lib/faulty/cache/mock.rb +39 -0
- data/lib/faulty/cache/null.rb +23 -0
- data/lib/faulty/cache/rails.rb +37 -0
- data/lib/faulty/circuit.rb +436 -0
- data/lib/faulty/error.rb +66 -0
- data/lib/faulty/events.rb +25 -0
- data/lib/faulty/events/callback_listener.rb +42 -0
- data/lib/faulty/events/listener_interface.rb +18 -0
- data/lib/faulty/events/log_listener.rb +88 -0
- data/lib/faulty/events/notifier.rb +25 -0
- data/lib/faulty/immutable_options.rb +40 -0
- data/lib/faulty/result.rb +150 -0
- data/lib/faulty/scope.rb +117 -0
- data/lib/faulty/status.rb +165 -0
- data/lib/faulty/storage.rb +11 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +178 -0
- data/lib/faulty/storage/interface.rb +161 -0
- data/lib/faulty/storage/memory.rb +195 -0
- data/lib/faulty/storage/redis.rb +335 -0
- data/lib/faulty/version.rb +8 -0
- metadata +306 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 67b20b18497c36c64f2a3dc7d16de761af0c22c0a38d93f3101534a5ac85e26d
|
4
|
+
data.tar.gz: 3d880b9a143939ab8ef4a22393c4bb8fa2b724d0bb02986bf52ac06ac3e5cf0b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 379c17805a9c647d71fac74e5190b6c077ef55c6d42b797f68dff008e602b005053a86c0c41572c761f32df25436b918e68f61049adf613e3a61a1a779d0364d
|
7
|
+
data.tar.gz: a7ff7c19ec6759282ec0f17ffe72f2a56aa5a41d83221ca53040f676951f8346fa2fa83fbe20c71a4ef843be9d2dfbbe0d23380d94342173e8bf41632d74ad12
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
---
|
2
|
+
require:
|
3
|
+
- rubocop-rspec
|
4
|
+
|
5
|
+
AllCops:
|
6
|
+
TargetRubyVersion: 2.3
|
7
|
+
|
8
|
+
Layout/ArgumentAlignment:
|
9
|
+
EnforcedStyle: with_fixed_indentation
|
10
|
+
|
11
|
+
Layout/ParameterAlignment:
|
12
|
+
EnforcedStyle: with_fixed_indentation
|
13
|
+
|
14
|
+
Layout/EndAlignment:
|
15
|
+
EnforcedStyleAlignWith: start_of_line
|
16
|
+
|
17
|
+
Layout/FirstArgumentIndentation:
|
18
|
+
EnforcedStyle: consistent
|
19
|
+
|
20
|
+
Layout/FirstArrayElementIndentation:
|
21
|
+
EnforcedStyle: consistent
|
22
|
+
|
23
|
+
Layout/FirstHashElementIndentation:
|
24
|
+
EnforcedStyle: consistent
|
25
|
+
|
26
|
+
Layout/LineLength:
|
27
|
+
Max: 120
|
28
|
+
|
29
|
+
Layout/MultilineMethodCallIndentation:
|
30
|
+
EnforcedStyle: indented
|
31
|
+
|
32
|
+
Lint/RaiseException:
|
33
|
+
Enabled: true
|
34
|
+
|
35
|
+
Lint/StructNewOverride:
|
36
|
+
Enabled: true
|
37
|
+
|
38
|
+
RSpec/ExampleLength:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
RSpec/FilePath:
|
42
|
+
Enabled: false
|
43
|
+
|
44
|
+
RSpec/NamedSubject:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
RSpec/MultipleExpectations:
|
48
|
+
Enabled: false
|
49
|
+
|
50
|
+
RSpec/SubjectStub:
|
51
|
+
Enabled: false
|
52
|
+
|
53
|
+
Metrics/AbcSize:
|
54
|
+
Max: 35
|
55
|
+
|
56
|
+
Metrics/BlockLength:
|
57
|
+
Enabled: false
|
58
|
+
|
59
|
+
Metrics/MethodLength:
|
60
|
+
Max: 30
|
61
|
+
|
62
|
+
Style/Documentation:
|
63
|
+
Enabled: false
|
64
|
+
|
65
|
+
Style/EmptyMethod:
|
66
|
+
EnforcedStyle: expanded
|
67
|
+
|
68
|
+
Style/FrozenStringLiteralComment:
|
69
|
+
Enabled: true
|
70
|
+
EnforcedStyle: always
|
71
|
+
|
72
|
+
Style/GuardClause:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Style/HashEachMethods:
|
76
|
+
Enabled: true
|
77
|
+
|
78
|
+
Style/HashTransformKeys:
|
79
|
+
Enabled: false
|
80
|
+
|
81
|
+
Style/HashTransformValues:
|
82
|
+
Enabled: false
|
83
|
+
|
84
|
+
Style/IfUnlessModifier:
|
85
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.3
|
5
|
+
- 2.4
|
6
|
+
- 2.5
|
7
|
+
- 2.6
|
8
|
+
- 2.7
|
9
|
+
|
10
|
+
env:
|
11
|
+
global:
|
12
|
+
- COVERAGE=1
|
13
|
+
# Encrypted code climate CC_TEST_REPORTER_ID=
|
14
|
+
- secure: 'a/qmESTnDq37KvBY4u1VcBEjd7Y3b16L28AiV0y3d7SbrGeuWPmTR7WMvD8b0CV1Ou5wpic9HAxC9u2ZYhy0/BN9I4AOnkyj4IXF9K+ETbTq2XOAix38hNF4a/Hl89mgKD3IPtjxVrQ83h7pxbqtQ2jPnLyiR7mTj0rfOmzTSaItcn/auONuvanHuitGjYTw3xUA1okbhMy8cVh3nbozP0m3eujSbSOpLD3N9+s+A8lE+6VbNvoygqoqdcQqlYH4q5x+SGaD7L1illIywxVWpExygB9jIum3mhaXe7ThJ/FkUfCHqINRejR63QDVwvYGRPnrlg0c8rNiLDjfVyY83ESSqXN1hf/vzyVtGruj4t/35VsFL/fyE/iNGDDmMu/GH5Gbsa9+lykeRSOL97VAlzAuVeb4AwvZ9HFV5QcbGnNkrVG8sTMIOcUzJVGX2C38fnElGxGwrbpV8+ReMZtcQZSg2isRRKDUFRBdFFUrHKK8uqII+a5oZDi5OK7ytZHg8XTbrpJXStYhQjRMfbHsd2YFGeiyLWqzrASd/bpgdyLtZuQ1/r7z1IVGJjb01nNhD4mchU3Lj4mxaQe2zvBEhbUZufz2zrIDPiKxsQvYcqw4nvHJE7zWM0cMgHvQrzcZH44sHVtdVavjSPoOzN6665zaA9sCcYffESCPPQTnopE='
|
15
|
+
|
16
|
+
services:
|
17
|
+
- redis-server
|
18
|
+
|
19
|
+
before_script:
|
20
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
21
|
+
- chmod +x ./cc-test-reporter
|
22
|
+
- ./cc-test-reporter before-build
|
23
|
+
|
24
|
+
script:
|
25
|
+
- bin/rubocop
|
26
|
+
- bin/rspec --format doc
|
27
|
+
- bin/check-version
|
28
|
+
|
29
|
+
after_script:
|
30
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
31
|
+
|
32
|
+
jobs:
|
33
|
+
include:
|
34
|
+
- stage: release
|
35
|
+
rvm: 2.7
|
36
|
+
script: skip
|
37
|
+
deploy:
|
38
|
+
provider: rubygems
|
39
|
+
api_key:
|
40
|
+
secure: 'eS6u+XyT6hHk/0gLXWzoe3RTJzEVFQHcJ+MNGSp7iq+cavJHisndcYBmUxi6/Ttb/aT/YoczhIWYo6WPbcjDqWszDfUsrQyaiOiZy4BBcMwN1WHeHkeRHfO2NX7us0AKO66TAiNfpMqUR2UT/c1LCPtLb+bkG6IFWxRuF5Fo32e0th6Z+hJ4Se2E/Qg1lrZk5zBlhQSOtU88vWQAkT9FdzpwBskVENBuDmH5YTV2Y28QYHsDtSNIxoDiUK9LOoPxaYUQp5ZZ58HZHtPdNydPHvtOXaWcBlakH5HNkh3FUHsxqbA1b3U412PC4TK+0jnxfISH4EOAMZEkL1nmroJORlb5nlwG1eiHpPVbOke1z2cZarGmWWAEf9bGE99GVbuxUxRrm9i1rmPdJY2G6Z0Kz8zoLTDYI/9l2P81/99a7h84rWACeeI3bcdvqViLxUuVMiwQjQsOZhzfq1M6jjETAWAI3AMRLxaGSgp0LV3WtaSWk1T5qvOvOF2HIfQ1EMd74kblJrVGWUiqd94/UgQoB/+lg9PdP/h4aHY5Cq1ec83wzaO6leKNlO+EQfyAVD1nd8kxo7YHEUpcB8wWCNFA5iaLqwIMt7aeWvG/BVUIH0apppzJGDy9UZ0gGsYi6ID3gbRRgmHIdJospr6TN7hksu1ZqkwEaprpXq8zH0KFYW0='
|
41
|
+
gem: faulty
|
42
|
+
on:
|
43
|
+
tags: true
|
44
|
+
repo: ParentSquare/faulty
|
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright © 2020 ParentSquare
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the “Software”), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,559 @@
|
|
1
|
+
# Faulty
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/faulty)
|
4
|
+
[](https://travis-ci.org/ParentSquare/faulty)
|
5
|
+
[](https://codeclimate.com/github/ParentSquare/faulty)
|
6
|
+
[](https://codeclimate.com/github/ParentSquare/faulty)
|
7
|
+
[](http://inch-ci.org/github/ParentSquare/faulty)
|
8
|
+
|
9
|
+
Fault-tolerance tools for ruby based on [circuit-breakers][martin fowler].
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
users = Faulty.circuit(:api).try_run do
|
13
|
+
api.users
|
14
|
+
end.or_default([])
|
15
|
+
```
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add it to your `Gemfile`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'faulty'
|
23
|
+
```
|
24
|
+
|
25
|
+
Or install it manually:
|
26
|
+
|
27
|
+
```sh
|
28
|
+
gem install faulty
|
29
|
+
```
|
30
|
+
|
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.
|
33
|
+
|
34
|
+
## API Docs
|
35
|
+
|
36
|
+
API docs can be read [on rubydoc.info][api docs], inline in the source code, or
|
37
|
+
you can generate them yourself with Ruby `yard`:
|
38
|
+
|
39
|
+
```sh
|
40
|
+
bin/yardoc
|
41
|
+
```
|
42
|
+
|
43
|
+
Then open `doc/index.html` in your browser.
|
44
|
+
|
45
|
+
## Setup
|
46
|
+
|
47
|
+
Use the default configuration options:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
Faulty.init
|
51
|
+
```
|
52
|
+
|
53
|
+
Or specify your own configuration:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Faulty.init do |config|
|
57
|
+
config.storage = Faulty::Storage::Redis.new
|
58
|
+
|
59
|
+
config.listeners << Faulty::Events::CallbackListener.new do |events|
|
60
|
+
events.circuit_open do |payload|
|
61
|
+
puts 'Circuit was opened'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
For a full list of configuration options, see the
|
68
|
+
[Global Configuration](#global-configuration) section.
|
69
|
+
|
70
|
+
## Basic Usage
|
71
|
+
|
72
|
+
To create a circuit, call `Faulty.circuit`. This can be done as you use the
|
73
|
+
circuit, or you can set it up beforehand. Any options passed to the `circuit`
|
74
|
+
method are synchronized across threads and saved as long as the process is alive.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
circuit1 = Faulty.circuit(:api, cache_refreshes_after: 1800)
|
78
|
+
|
79
|
+
# The options from above are also used when called here
|
80
|
+
circuit2 = Faulty.circuit(:api)
|
81
|
+
circuit2.options.cache_refreshes_after == 1800 # => true
|
82
|
+
|
83
|
+
# The same circuit is returned on each consecutive call
|
84
|
+
circuit1.equal?(circuit2) # => true
|
85
|
+
```
|
86
|
+
|
87
|
+
To run a circuit, call the `run` method:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
Faulty.circuit(:api).run do
|
91
|
+
api.users
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
See [How it Works](#how-it-works) for more details about how Faulty handles
|
96
|
+
circuit failures.
|
97
|
+
|
98
|
+
If the `run` block above fails, a `Faulty::CircuitError` will be raised. It is
|
99
|
+
up to your application to handle that error however necessary or crash. Often
|
100
|
+
though, you don't want to crash your application when a circuit fails, but
|
101
|
+
instead apply a fallback or default behavior. For this, Faulty provides the
|
102
|
+
`try_run` method:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
result = Faulty.circuit(:api).try_run do
|
106
|
+
api.users
|
107
|
+
end
|
108
|
+
|
109
|
+
users = if result.ok?
|
110
|
+
result.get
|
111
|
+
else
|
112
|
+
[]
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
The `try_run` method returns a result type instead of raising errors. See the
|
117
|
+
API docs for `Result` for more information. Here we use it to check whether the
|
118
|
+
result is `ok?` (not an error). If it is we set the users variable, otherwise we
|
119
|
+
set a default of an empty array. This pattern is so common, that `Result` also
|
120
|
+
implements a helper method `or_default` to do the same thing:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
users = Faulty.circuit(:api).try_run do
|
124
|
+
api.users
|
125
|
+
end.or_default([])
|
126
|
+
```
|
127
|
+
|
128
|
+
## How it Works
|
129
|
+
|
130
|
+
Faulty implements a version of circuit breakers inspired by
|
131
|
+
[Martin Fowler's post][martin fowler] on the subject. A few notable features of
|
132
|
+
Faulty's implementation are:
|
133
|
+
|
134
|
+
- Rate-based failure thresholds
|
135
|
+
- Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
|
136
|
+
cache jitter and error fallback.
|
137
|
+
- Event-based monitoring
|
138
|
+
|
139
|
+
Following the principals of the circuit-breaker pattern, the block given to
|
140
|
+
`run` or `try_run` will always be executed as long as long as it never raises an
|
141
|
+
error. If the block _does_ raise an error, then the circuit keeps track of the
|
142
|
+
number of runs and the failure rate.
|
143
|
+
|
144
|
+
Once both thresholds are breached, the circuit is opened. Once open, the
|
145
|
+
circuit starts the cool-down period. Any executions within that cool-down are
|
146
|
+
skipped, and a `Faulty::OpenCircuitError` will be raised.
|
147
|
+
|
148
|
+
After the cool-down has elapsed, the circuit enters the half-open state. In this
|
149
|
+
state, Faulty allows a single execution of the block as a test run. If the test
|
150
|
+
run succeeds, the circuit is fully opened and the circuit state is reset. If the
|
151
|
+
test run fails, the circuit is closed and the cool-down is reset.
|
152
|
+
|
153
|
+
Each time the circuit changes state or executes the block, events are raised
|
154
|
+
that are sent to the Faulty event notifier. The notifier should be used to track
|
155
|
+
circuit failure rates, open circuits, etc.
|
156
|
+
|
157
|
+
In addition to the classic circuit breaker design, Faulty implements caching
|
158
|
+
that is integrated with the circuit state. See [Caching](#caching) for more
|
159
|
+
detail.
|
160
|
+
|
161
|
+
## Global Configuration
|
162
|
+
|
163
|
+
`Faulty.init` can set the following global configuration options. This example
|
164
|
+
illustrates the default values. It is also possible to define multiple
|
165
|
+
non-global configuration scopes (see [Scopes](#scopes)).
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
Faulty.init do |config|
|
169
|
+
# The cache backend to use. By default, Faulty looks for a Rails cache. If
|
170
|
+
# that's not available, it uses an ActiveSupport::Cache::Memory instance.
|
171
|
+
# Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
|
172
|
+
config.cache = Faulty::Cache::Default.new
|
173
|
+
|
174
|
+
# The storage backend. By default, Faulty uses an in-memory store. For most
|
175
|
+
# production applications, you'll want a more robust backend. Faulty also
|
176
|
+
# provides Faulty::Storage::Redis for this.
|
177
|
+
config.storage = Faulty::Storage::Memory.new
|
178
|
+
|
179
|
+
# An array of event listeners. Each object in the array should implement
|
180
|
+
# Faulty::Events::ListenerInterface. For ad-hoc custom listeners, Faulty
|
181
|
+
# provides Faulty::Events::CallbackListener.
|
182
|
+
config.listeners = [Faulty::Events::LogListener.new]
|
183
|
+
|
184
|
+
# The event notifier. For most use-cases, you don't need to change this,
|
185
|
+
# However, Faulty allows substituting your own notifier if necessary.
|
186
|
+
# If overridden, config.listeners will be ignored.
|
187
|
+
config.notifier = Faulty::Events::Notifier.new(config.listeners)
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
For all Faulty APIs that have configuration, you can also pass in an options
|
192
|
+
hash. For example, `Faulty.init` could be called like this:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
Faulty.init(cache: Faulty::Cache::Null.new)
|
196
|
+
```
|
197
|
+
|
198
|
+
## Circuit Options
|
199
|
+
|
200
|
+
A circuit can be created with the following configuration options. Those options
|
201
|
+
are only set once, synchronized across threads, and will persist in-memory until
|
202
|
+
the process exits. If you're using [scopes](#scopes), the options are retained
|
203
|
+
within the context of each scope. All options given after the first call to
|
204
|
+
`Faulty.circuit` (or `Scope.circuit` are ignored.
|
205
|
+
|
206
|
+
This is because the circuit objects themselves are internally memoized, and are
|
207
|
+
read-only once created.
|
208
|
+
|
209
|
+
The following example represents the defaults for a new circuit:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
Faulty.circuit(:api) do |config|
|
213
|
+
# The cache backend for this circuit. Inherits the global cache by default.
|
214
|
+
config.cache = Faulty.options.cache
|
215
|
+
|
216
|
+
# The number of seconds before a cache entry is expired. After this time, the
|
217
|
+
# cache entry may be fully deleted. If set to nil, the cache will not expire.
|
218
|
+
config.cache_expires_in = 86400
|
219
|
+
|
220
|
+
# The number of seconds before a cache entry should be refreshed. See the
|
221
|
+
# Caching section for more detail. A value of nil disables cache refreshing.
|
222
|
+
config.cache_refreshes_after = 900
|
223
|
+
|
224
|
+
# The number of seconds to add or subtract from cache_refreshes_after
|
225
|
+
# when determining whether a cache entry should be refreshed. Helps mitigate
|
226
|
+
# the "thundering herd" effect
|
227
|
+
config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
|
228
|
+
|
229
|
+
# After a circuit is opened, the number of seconds to wait before moving the
|
230
|
+
# circuit to half-open.
|
231
|
+
config.cool_down = 300
|
232
|
+
|
233
|
+
# The errors that will be captured by Faulty and used to trigger circuit
|
234
|
+
# state changes.
|
235
|
+
config.errors = [StandardError]
|
236
|
+
|
237
|
+
# Errors that should be ignored by Faulty and not captured.
|
238
|
+
config.exclude = []
|
239
|
+
|
240
|
+
# The event notifier. Inherits the global notifier by default
|
241
|
+
config.notifier = Faulty.options.notifier
|
242
|
+
|
243
|
+
# The minimum failure rate required to trip a circuit
|
244
|
+
config.rate_threshold = 0.5
|
245
|
+
|
246
|
+
# The minimum number of runs required before a circuit can trip
|
247
|
+
config.sample_threshold = 3
|
248
|
+
|
249
|
+
# The storage backend for this circuit. Inherits the global storage by default
|
250
|
+
config.storage = Faulty.options.storage
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
Following the same convention as `Faulty.init`, circuits can also be created
|
255
|
+
with an options hash:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
Faulty.circuit(:api, cache_expires_in: 1800)
|
259
|
+
```
|
260
|
+
|
261
|
+
## Caching
|
262
|
+
|
263
|
+
Faulty integrates caching into it's circuits in a way that is particularly
|
264
|
+
suited to fault-tolerance. To make use of caching, you must specify the `cache`
|
265
|
+
configuration option when initializing Faulty or creating a scope. If you're
|
266
|
+
using Rails, this is automatically set to the Rails cache.
|
267
|
+
|
268
|
+
Once your cache is configured, you can use the `cache` parameter when running
|
269
|
+
a circuit to specify a cache key:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
feed = Faulty.circuit(:rss_feeds)
|
273
|
+
.try_run(cache: "rss_feeds/#{feed}") do
|
274
|
+
fetch_feed(feed)
|
275
|
+
end.or_default([])
|
276
|
+
```
|
277
|
+
|
278
|
+
By default a circuit has the following options:
|
279
|
+
|
280
|
+
- `cache_expires_in`: 86400 (1 day). This is sent to the cache backend and
|
281
|
+
defines how long the cache entry should be stored. After this time elapses,
|
282
|
+
queries will result in a cache miss.
|
283
|
+
- `cache_refreshes_after`: 900 (15 minutes). This is used internally by Faulty
|
284
|
+
to indicate when a cache should be refreshed. It does not affect how long the
|
285
|
+
cache entry is stored.
|
286
|
+
- `cache_refresh_jitter`: 180 (3 minutes = 20% of `cache_refreshes_after`). The
|
287
|
+
maximum number of seconds to randomly add or subtract from
|
288
|
+
`cache_refreshes_after` when determining whether to refresh a cache entry.
|
289
|
+
This mitigates the "thundering herd" effect caused by many processes
|
290
|
+
simultaneously refreshing the cache.
|
291
|
+
|
292
|
+
This code will attempt to fetch an RSS feed protected by a circuit. If the feed
|
293
|
+
is within the cache refresh period, then the result will be returned from the
|
294
|
+
cache and the block will not be executed regardless of the circuit state.
|
295
|
+
|
296
|
+
If the cache is hit, but outside its refresh period, then Faulty will check the
|
297
|
+
circuit state. If the circuit is closed or half-open, then it will run the
|
298
|
+
block. If the block is successful, then it will update the circuit, write to the
|
299
|
+
cache and return the new value.
|
300
|
+
|
301
|
+
However, if the cache is hit and the block fails, then that failure is noted
|
302
|
+
in the circuit and Faulty returns the cached value.
|
303
|
+
|
304
|
+
If the circuit is open and the cache is hit, then Faulty will always return the
|
305
|
+
cached value.
|
306
|
+
|
307
|
+
If the cache query results in a miss, then faulty operates as normal. In the
|
308
|
+
code above, if the circuit is closed, the block will be executed. If the block
|
309
|
+
succeeds, the cache is refreshed. If the block fails, the default of `[]` will
|
310
|
+
be returned.
|
311
|
+
|
312
|
+
## Fault Tolerance
|
313
|
+
|
314
|
+
Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
|
315
|
+
the storage or cache backends are captured and suppressed. Failure events for
|
316
|
+
these errors are sent to the notifier.
|
317
|
+
|
318
|
+
If the storage backend fails, all circuits will default to open. If the cache
|
319
|
+
backend fails, all cache queries will miss.
|
320
|
+
|
321
|
+
## Event Handling
|
322
|
+
|
323
|
+
Faulty uses an event-dispatching model to deliver notifications of internal
|
324
|
+
events. The full list of events is available from `Faulty::Events::EVENTS`.
|
325
|
+
|
326
|
+
- `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
|
327
|
+
- `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
|
328
|
+
- `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
|
329
|
+
- `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
|
330
|
+
- `circuit_closed` - A circuit closed. Payload: `circuit`
|
331
|
+
- `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
|
332
|
+
`status`, `error`
|
333
|
+
- `circuit_opened` - A circuit execution caused the circuit to open. Payload
|
334
|
+
`circuit`, `error`
|
335
|
+
- `circuit_reopened` - A circuit execution cause the circuit to reopen from
|
336
|
+
half-open. Payload: `circuit`, `error`.
|
337
|
+
- `circuit_skipped` - A circuit execution was skipped because the circuit is
|
338
|
+
closed. Payload: `circuit`
|
339
|
+
- `circuit_success` - A circuit execution was successful. Payload: `circuit`,
|
340
|
+
`status`
|
341
|
+
- `storage_failure` - A storage backend raised an error. Payload `circuit`,
|
342
|
+
`action`, `error`
|
343
|
+
|
344
|
+
By default events are logged using `Faulty::Events::LogListener`, but that can
|
345
|
+
be replaced, or additional listeners can be added.
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
Faulty.init do |config|
|
349
|
+
# Replace the default listener with a custom callback listener
|
350
|
+
listener = Faulty::Events::CallbackListener.new do |events|
|
351
|
+
events.circuit_opened do |payload|
|
352
|
+
MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
|
353
|
+
end
|
354
|
+
end
|
355
|
+
config.listeners = [listener]
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
You can implement your own listener by following the documentation in
|
360
|
+
`Faulty::Events::ListenerInterface`. For example:
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
class MyFaultyListener
|
364
|
+
def handle(event, payload)
|
365
|
+
MyNotifier.alert(event, payload)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
Faulty.init do |config|
|
372
|
+
config.listeners = [MyFaultyListener.new]
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
## Configuring the Storage Backend
|
377
|
+
|
378
|
+
### Memory
|
379
|
+
|
380
|
+
The `Faulty::Cache::Memory` backend is the default storage backend. The default
|
381
|
+
configuration:
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
Faulty.init do |config|
|
385
|
+
config.storage = Faulty::Storage::Memory.new do |storage|
|
386
|
+
# The maximum number of circuit runs that will be stored
|
387
|
+
storage.max_sample_size = 100
|
388
|
+
end
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
### Redis
|
393
|
+
|
394
|
+
The `Faulty::Cache::Redis` backend provides distributed circuit storage using
|
395
|
+
Redis. The default configuration:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
Faulty.init do |config|
|
399
|
+
config.storage = Faulty::Storage::Redis.new do |storage|
|
400
|
+
# The Redis client. Accepts either a Redis instance, or a ConnectionPool
|
401
|
+
# of Redis instances.
|
402
|
+
storage.client = ::Redis.new
|
403
|
+
|
404
|
+
# The prefix to prepend to all redis keys used by Faulty circuits
|
405
|
+
storage.key_prefix = 'faulty'
|
406
|
+
|
407
|
+
# A string to separate the parts of the redis key
|
408
|
+
storage.key_separator: ':'
|
409
|
+
|
410
|
+
# The maximum number of circuit runs that will be stored
|
411
|
+
storage.max_sample_size = 100
|
412
|
+
|
413
|
+
# The maximum number of seconds that a circuit run will be stored
|
414
|
+
storage.sample_ttl = 1800
|
415
|
+
end
|
416
|
+
end
|
417
|
+
```
|
418
|
+
|
419
|
+
### Listing Circuits
|
420
|
+
|
421
|
+
For monitoring or debugging, you may need to retrieve a list of all circuit
|
422
|
+
names. This is possible with `Faulty.list_circuits` (or the equivalent method on
|
423
|
+
your [scope](#scopes)).
|
424
|
+
|
425
|
+
You can get a list of all circuit statuses by mapping those names to their
|
426
|
+
status objects. Be careful though, since this could cause performance issues for
|
427
|
+
very large numbers of circuits.
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
statuses = Faulty.list_circuits.map do |name|
|
431
|
+
Faulty.circuit(name).status
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
## Scopes
|
436
|
+
|
437
|
+
It is possible to have multiple configurations of Faulty running within the same
|
438
|
+
process. The most common configuration is to simply use `Faulty.init` to
|
439
|
+
configure Faulty globally, however it is possible to have additional
|
440
|
+
configurations using scopes.
|
441
|
+
|
442
|
+
### The default scope
|
443
|
+
|
444
|
+
When you call `Faulty.init`, you are actually creating the default scope. You
|
445
|
+
can access this scope directly by calling `Faulty.default`.
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
# We create the default scope
|
449
|
+
Faulty.init
|
450
|
+
|
451
|
+
# Access the default scope
|
452
|
+
scope = Faulty.default
|
453
|
+
|
454
|
+
# Alternatively, access the scope by name
|
455
|
+
scope = Faulty[:default]
|
456
|
+
```
|
457
|
+
|
458
|
+
You can rename the default scope if desired:
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
Faulty.init(:custom_default)
|
462
|
+
|
463
|
+
scope = Faulty.default
|
464
|
+
scope = Faulty[:custom_default]
|
465
|
+
```
|
466
|
+
|
467
|
+
### Multiple Scopes
|
468
|
+
|
469
|
+
If you want multiple scopes, but want global, thread-safe access to
|
470
|
+
them, you can use `Faulty.register`:
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
api_scope = Faulty::Scope.new do |config|
|
474
|
+
# This accepts the same options as Faulty.init
|
475
|
+
end
|
476
|
+
|
477
|
+
Faulty.register(:api, api_scope)
|
478
|
+
|
479
|
+
# Now access the scope globally
|
480
|
+
Faulty[:api]
|
481
|
+
```
|
482
|
+
|
483
|
+
When you call `Faulty.circuit`, that's the same as calling
|
484
|
+
`Faulty.default.circuit`, so you can apply the same API to any other Faulty
|
485
|
+
scope:
|
486
|
+
|
487
|
+
```ruby
|
488
|
+
Faulty[:api].circuit(:api_circuit).run { 'ok' }
|
489
|
+
```
|
490
|
+
|
491
|
+
### Standalone Scopes
|
492
|
+
|
493
|
+
If you choose, you can use Faulty scopes without registering them globally. This
|
494
|
+
could be useful if you prefer dependency injection over global state.
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
faulty = Faulty::Scope.new
|
498
|
+
faulty.circuit(:standalone_circuit)
|
499
|
+
```
|
500
|
+
|
501
|
+
Calling `circuit` on the scope still has the same memoization behavior that
|
502
|
+
`Faulty.circuit` has, so subsequent calls to the same circuit will return a
|
503
|
+
memoized circuit object.
|
504
|
+
|
505
|
+
## Implementing a Cache Backend
|
506
|
+
|
507
|
+
You can implement your own cache backend by following the documentation in
|
508
|
+
`Faulty::Cache::Interface`. It is a fairly simple API, with only get/set
|
509
|
+
methods. For example:
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
class MyFaultyCache
|
513
|
+
def initialize(my_cache)
|
514
|
+
@cache = my_cache
|
515
|
+
end
|
516
|
+
|
517
|
+
def read(key)
|
518
|
+
@cache.read(key)
|
519
|
+
end
|
520
|
+
|
521
|
+
def write(key, value, expires_in: nil)
|
522
|
+
@cache.write(key, value, expires_in)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Set this to false unless your cache never raises errors
|
526
|
+
def fault_tolerant?
|
527
|
+
false
|
528
|
+
end
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
Feel free to open a pull request if your cache backend would be useful for other
|
533
|
+
users.
|
534
|
+
|
535
|
+
## Implementing a Storage Backend
|
536
|
+
|
537
|
+
You can implement your own storage backend by following the documentation in
|
538
|
+
`Faulty::Storage::Interface`. Since the storage has some tricky requirements
|
539
|
+
regarding concurrency, the `Faulty::Storage::Memory` can be used as a reference
|
540
|
+
implementation. Feel free to open a pull request if your storage backend
|
541
|
+
would be useful for other users.
|
542
|
+
|
543
|
+
## Alternatives
|
544
|
+
|
545
|
+
Faulty has its own opinions about how to implement a circuit breaker in Ruby,
|
546
|
+
but there are and have been many other options:
|
547
|
+
|
548
|
+
- [circuitbox](https://github.com/yammer/circuitbox)
|
549
|
+
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby)
|
550
|
+
- [stoplight](https://github.com/orgsync/stoplight) (currently unmaintained)
|
551
|
+
- [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
|
552
|
+
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
553
|
+
(unmaintained)
|
554
|
+
- [breaker](https://github.com/ahawkins/breaker) (unmaintained)
|
555
|
+
- [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
|
556
|
+
|
557
|
+
[api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
|
558
|
+
[martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
|
559
|
+
[hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works
|