faulty 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/faulty.svg)](https://badge.fury.io/rb/faulty)
|
4
|
+
[![Build Status](https://travis-ci.org/ParentSquare/faulty.svg?branch=master)](https://travis-ci.org/ParentSquare/faulty)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/ParentSquare/faulty/badges/gpa.svg)](https://codeclimate.com/github/ParentSquare/faulty)
|
6
|
+
[![Test Coverage](https://codeclimate.com/github/ParentSquare/faulty/badges/coverage.svg)](https://codeclimate.com/github/ParentSquare/faulty)
|
7
|
+
[![Inline docs](http://inch-ci.org/github/ParentSquare/faulty.svg?branch=master)](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
|