faulty 0.1.2 → 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/.rubocop.yml +9 -0
- data/CHANGELOG.md +50 -2
- data/Gemfile +22 -0
- data/README.md +836 -220
- data/bin/check-version +5 -1
- data/bin/console +1 -1
- data/faulty.gemspec +4 -11
- data/lib/faulty.rb +157 -43
- data/lib/faulty/cache.rb +3 -1
- data/lib/faulty/cache/auto_wire.rb +58 -0
- data/lib/faulty/cache/circuit_proxy.rb +61 -0
- data/lib/faulty/cache/default.rb +10 -21
- data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
- data/lib/faulty/cache/interface.rb +1 -1
- data/lib/faulty/cache/mock.rb +1 -1
- data/lib/faulty/cache/null.rb +1 -1
- data/lib/faulty/cache/rails.rb +9 -10
- data/lib/faulty/circuit.rb +10 -5
- data/lib/faulty/error.rb +18 -4
- data/lib/faulty/events.rb +3 -2
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/honeybadger_listener.rb +53 -0
- data/lib/faulty/events/listener_interface.rb +1 -1
- data/lib/faulty/events/log_listener.rb +5 -6
- data/lib/faulty/events/notifier.rb +11 -2
- data/lib/faulty/immutable_options.rb +1 -1
- data/lib/faulty/result.rb +2 -2
- data/lib/faulty/status.rb +3 -2
- data/lib/faulty/storage.rb +4 -1
- data/lib/faulty/storage/auto_wire.rb +107 -0
- data/lib/faulty/storage/circuit_proxy.rb +64 -0
- data/lib/faulty/storage/fallback_chain.rb +207 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +51 -56
- data/lib/faulty/storage/interface.rb +1 -1
- data/lib/faulty/storage/memory.rb +8 -4
- data/lib/faulty/storage/redis.rb +75 -13
- data/lib/faulty/version.rb +2 -2
- metadata +18 -122
- data/.travis.yml +0 -44
- data/lib/faulty/scope.rb +0 -117
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/.rubocop.yml
CHANGED
@@ -29,6 +29,9 @@ Layout/LineLength:
|
|
29
29
|
Layout/MultilineMethodCallIndentation:
|
30
30
|
EnforcedStyle: indented
|
31
31
|
|
32
|
+
Layout/RescueEnsureAlignment:
|
33
|
+
Enabled: false
|
34
|
+
|
32
35
|
Lint/RaiseException:
|
33
36
|
Enabled: true
|
34
37
|
|
@@ -44,6 +47,9 @@ RSpec/FilePath:
|
|
44
47
|
RSpec/NamedSubject:
|
45
48
|
Enabled: false
|
46
49
|
|
50
|
+
RSpec/MessageSpies:
|
51
|
+
Enabled: false
|
52
|
+
|
47
53
|
RSpec/MultipleExpectations:
|
48
54
|
Enabled: false
|
49
55
|
|
@@ -59,6 +65,9 @@ Metrics/BlockLength:
|
|
59
65
|
Metrics/MethodLength:
|
60
66
|
Max: 30
|
61
67
|
|
68
|
+
Naming/MethodParameterName:
|
69
|
+
MinNameLength: 1
|
70
|
+
|
62
71
|
Style/Documentation:
|
63
72
|
Enabled: false
|
64
73
|
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,58 @@
|
|
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
|
+
|
17
|
+
## Release v0.3.0
|
18
|
+
|
19
|
+
* Add tools for backend fault-tolerance #10
|
20
|
+
* CircuitProxy for wrapping storage in an internal circuit
|
21
|
+
* FallbackChain storage backend for falling back to stable storage
|
22
|
+
* Timeout warnings for Redis backend
|
23
|
+
* AutoWire wrappers for automatically configuring storage and cache
|
24
|
+
* Better documentation for fault-tolerance
|
25
|
+
|
26
|
+
## Release v0.2.0
|
27
|
+
|
28
|
+
* Remove Scopes and replace them with Faulty instances #9
|
29
|
+
|
30
|
+
### Breaking Changes
|
31
|
+
|
32
|
+
* `Faulty::Scope` has been removed. Use `Faulty.new` instead.
|
33
|
+
* `Faulty` is now a class, not a module
|
34
|
+
|
35
|
+
## Release v0.1.5
|
36
|
+
|
37
|
+
* Fix redis storage to expire state key when using CAS #8
|
38
|
+
|
39
|
+
## Release v0.1.4
|
40
|
+
|
41
|
+
* Improve spec coverage for supporting classes #6
|
42
|
+
* Fix redis bug where concurrent CAS requests could crash #7
|
43
|
+
|
44
|
+
## Release v0.1.3
|
45
|
+
|
46
|
+
* Fix bug where memory storage would delete the newest entries #5
|
47
|
+
* Add HoneybadgerListener for error reporting #4
|
48
|
+
|
1
49
|
## Release v0.1.2
|
2
50
|
|
3
|
-
* Fix Storage::FaultTolerantProxy open and reopen methods
|
51
|
+
* Fix Storage::FaultTolerantProxy open and reopen methods #2
|
4
52
|
|
5
53
|
## Release v0.1.1
|
6
54
|
|
7
|
-
* Fix a crash when Storage::FaultTolerantProxy created a status stub
|
55
|
+
* Fix a crash when Storage::FaultTolerantProxy created a status stub #1
|
8
56
|
|
9
57
|
## Release v0.1.0
|
10
58
|
|
data/Gemfile
CHANGED
@@ -3,3 +3,25 @@
|
|
3
3
|
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
gemspec
|
6
|
+
|
7
|
+
# We add non-essential gems like debugging tools and CI dependencies
|
8
|
+
# here. This also allows us to use conditional dependencies that depend on the
|
9
|
+
# platform
|
10
|
+
|
11
|
+
not_jruby = %i[ruby mingw x64_mingw].freeze
|
12
|
+
|
13
|
+
gem 'activesupport', '>= 4.2'
|
14
|
+
gem 'bundler', '>= 1.17', '< 3'
|
15
|
+
gem 'byebug', platforms: not_jruby
|
16
|
+
gem 'irb', '~> 1.0'
|
17
|
+
gem 'redcarpet', '~> 3.5', platforms: not_jruby
|
18
|
+
gem 'rspec_junit_formatter', '~> 0.4'
|
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'
|
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,21 +147,31 @@ Faulty.init do |config|
|
|
64
147
|
end
|
65
148
|
```
|
66
149
|
|
150
|
+
Or use a faulty instance instead for an object-oriented approach
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
faulty = Faulty.new do
|
154
|
+
config.storage = Faulty::Storage::Redis.new
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
67
158
|
For a full list of configuration options, see the
|
68
|
-
[
|
159
|
+
[Configuration](#configuration) section.
|
69
160
|
|
70
161
|
## Basic Usage
|
71
162
|
|
72
|
-
To create a circuit, call
|
73
|
-
circuit
|
74
|
-
|
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.
|
75
168
|
|
76
169
|
```ruby
|
77
|
-
circuit1 = Faulty.circuit(:api,
|
170
|
+
circuit1 = Faulty.circuit(:api, rate_threshold: 0.6)
|
78
171
|
|
79
172
|
# The options from above are also used when called here
|
80
173
|
circuit2 = Faulty.circuit(:api)
|
81
|
-
circuit2.options.
|
174
|
+
circuit2.options.rate_threshold == 0.6 # => true
|
82
175
|
|
83
176
|
# The same circuit is returned on each consecutive call
|
84
177
|
circuit1.equal?(circuit2) # => true
|
@@ -95,11 +188,12 @@ end
|
|
95
188
|
See [How it Works](#how-it-works) for more details about how Faulty handles
|
96
189
|
circuit failures.
|
97
190
|
|
98
|
-
If the `run` block above fails, a
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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:
|
103
197
|
|
104
198
|
```ruby
|
105
199
|
result = Faulty.circuit(:api).try_run do
|
@@ -113,11 +207,13 @@ else
|
|
113
207
|
end
|
114
208
|
```
|
115
209
|
|
116
|
-
The `try_run`
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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:
|
121
217
|
|
122
218
|
```ruby
|
123
219
|
users = Faulty.circuit(:api).try_run do
|
@@ -125,55 +221,83 @@ users = Faulty.circuit(:api).try_run do
|
|
125
221
|
end.or_default([])
|
126
222
|
```
|
127
223
|
|
128
|
-
|
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.
|
129
227
|
|
130
|
-
|
131
|
-
[Martin Fowler's post][martin fowler] on the subject. A few notable features of
|
132
|
-
Faulty's implementation are:
|
228
|
+
## What is this for?
|
133
229
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
- Event-based monitoring
|
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:
|
138
233
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
number of runs and the failure rate.
|
234
|
+
```ruby
|
235
|
+
TextApi.send(message)
|
236
|
+
```
|
143
237
|
|
144
|
-
|
145
|
-
|
146
|
-
|
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:
|
147
242
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
test run fails, the circuit is closed and the cool-down is reset.
|
243
|
+
```ruby
|
244
|
+
TextApi.send(message, timeout: 5)
|
245
|
+
```
|
152
246
|
|
153
|
-
|
154
|
-
|
155
|
-
|
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.
|
156
250
|
|
157
|
-
|
158
|
-
|
159
|
-
|
251
|
+
```ruby
|
252
|
+
Faulty.circuit('text_api').run do
|
253
|
+
TextApi.send(message, timeout: 5)
|
254
|
+
end
|
255
|
+
```
|
160
256
|
|
161
|
-
|
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.
|
162
261
|
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
+
```
|
272
|
+
|
273
|
+
## Configuration
|
274
|
+
|
275
|
+
Faulty can be configured with the following configuration options. This example
|
276
|
+
illustrates the default values. In the first example, we configure Faulty
|
277
|
+
globally. The second example shows the same configuration using an instance of
|
278
|
+
Faulty instead of global configuration.
|
166
279
|
|
167
280
|
```ruby
|
168
281
|
Faulty.init do |config|
|
169
282
|
# The cache backend to use. By default, Faulty looks for a Rails cache. If
|
170
283
|
# that's not available, it uses an ActiveSupport::Cache::Memory instance.
|
171
284
|
# Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
|
285
|
+
# Whatever backend is given here is automatically wrapped in
|
286
|
+
# Faulty::Cache::AutoWire. This adds fault-tolerance features, see the
|
287
|
+
# AutoWire API docs for more details.
|
172
288
|
config.cache = Faulty::Cache::Default.new
|
173
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
|
+
|
174
294
|
# The storage backend. By default, Faulty uses an in-memory store. For most
|
175
295
|
# production applications, you'll want a more robust backend. Faulty also
|
176
296
|
# provides Faulty::Storage::Redis for this.
|
297
|
+
# Whatever backend is given here is automatically wrapped in
|
298
|
+
# Faulty::Storage::AutoWire. This adds fault-tolerance features, see the
|
299
|
+
# AutoWire APi docs for more details. If an array of storage backends is
|
300
|
+
# given, each one will be tried in order until one succeeds.
|
177
301
|
config.storage = Faulty::Storage::Memory.new
|
178
302
|
|
179
303
|
# An array of event listeners. Each object in the array should implement
|
@@ -188,6 +312,25 @@ Faulty.init do |config|
|
|
188
312
|
end
|
189
313
|
```
|
190
314
|
|
315
|
+
Here is the same configuration using an instance of `Faulty`. This is a more
|
316
|
+
object-oriented approach.
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
faulty = Faulty.new do |config|
|
320
|
+
config.cache = Faulty::Cache::Default.new
|
321
|
+
config.storage = Faulty::Storage::Memory.new
|
322
|
+
config.listeners = [Faulty::Events::LogListener.new]
|
323
|
+
config.notifier = Faulty::Events::Notifier.new(config.listeners)
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
Most of the examples in this README use the global Faulty class methods, but
|
328
|
+
they work the same way when using an instance. Just substitute your instance
|
329
|
+
instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever
|
330
|
+
configuration mechanism works best for your application. Also see
|
331
|
+
[Multiple Configurations](#multiple-configurations) if your application needs
|
332
|
+
to set different options in different scenarios.
|
333
|
+
|
191
334
|
For all Faulty APIs that have configuration, you can also pass in an options
|
192
335
|
hash. For example, `Faulty.init` could be called like this:
|
193
336
|
|
@@ -195,13 +338,471 @@ hash. For example, `Faulty.init` could be called like this:
|
|
195
338
|
Faulty.init(cache: Faulty::Cache::Null.new)
|
196
339
|
```
|
197
340
|
|
198
|
-
|
341
|
+
### Configuring the Storage Backend
|
342
|
+
|
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.
|
346
|
+
|
347
|
+
#### Memory
|
348
|
+
|
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:
|
358
|
+
|
359
|
+
```ruby
|
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
|
+
```
|
367
|
+
|
368
|
+
#### Redis
|
369
|
+
|
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.
|
376
|
+
|
377
|
+
The default configuration:
|
378
|
+
|
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)
|
386
|
+
|
387
|
+
# The prefix to prepend to all redis keys used by Faulty circuits
|
388
|
+
storage.key_prefix = 'faulty'
|
389
|
+
|
390
|
+
# A string to separate the parts of the redis key
|
391
|
+
storage.key_separator = ':'
|
392
|
+
|
393
|
+
# The maximum number of circuit runs that will be stored
|
394
|
+
storage.max_sample_size = 100
|
395
|
+
|
396
|
+
# The maximum number of seconds that a circuit run will be stored
|
397
|
+
storage.sample_ttl = 1800
|
398
|
+
|
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
|
402
|
+
|
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
|
410
|
+
end
|
411
|
+
```
|
412
|
+
|
413
|
+
#### FallbackChain
|
414
|
+
|
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.
|
420
|
+
|
421
|
+
For example, you may configure Redis as your primary storage backend, with an
|
422
|
+
in-memory storage backend as a fallback:
|
423
|
+
|
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
|
+
```
|
432
|
+
|
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:
|
435
|
+
|
436
|
+
```ruby
|
437
|
+
Faulty.init do |config|
|
438
|
+
config.storage = [
|
439
|
+
Faulty::Storage::Redis.new,
|
440
|
+
Faulty::Storage::Memory.new
|
441
|
+
]
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
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.
|
449
|
+
|
450
|
+
#### Storage::FaultTolerantProxy
|
451
|
+
|
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)).
|
454
|
+
|
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.
|
459
|
+
|
460
|
+
If you wish your application to use a secondary storage backend instead of
|
461
|
+
failing closed, use [`FallbackChain`](#storagefallbackchain).
|
462
|
+
|
463
|
+
#### Storage::CircuitProxy
|
464
|
+
|
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)).
|
467
|
+
|
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.
|
472
|
+
|
473
|
+
Typically this is used inside a [`FaultTolerantProxy`](#storagefaulttolerantproxy) or
|
474
|
+
[`FallbackChain`](#storagefallbackchain) so that these storage failures are handled
|
475
|
+
gracefully.
|
476
|
+
|
477
|
+
### Configuring the Cache Backend
|
478
|
+
|
479
|
+
#### Null
|
480
|
+
|
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.
|
484
|
+
|
485
|
+
#### Rails
|
486
|
+
|
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`.
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
Faulty.init do |config|
|
495
|
+
config.cache = Faulty::Cache::Rails.new(
|
496
|
+
ActiveSupport::Cache::RedisCacheStore.new
|
497
|
+
)
|
498
|
+
end
|
499
|
+
```
|
500
|
+
|
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`).
|
505
|
+
|
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).
|
537
|
+
|
538
|
+
```ruby
|
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]
|
547
|
+
```
|
548
|
+
|
549
|
+
You can rename the default instance if desired:
|
550
|
+
|
551
|
+
```ruby
|
552
|
+
Faulty.init(:custom_default)
|
553
|
+
|
554
|
+
instance = Faulty.default
|
555
|
+
instance = Faulty[:custom_default]
|
556
|
+
```
|
557
|
+
|
558
|
+
#### Multiple Instances
|
559
|
+
|
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):
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
api_faulty = Faulty.new do |config|
|
566
|
+
# This accepts the same options as Faulty.init
|
567
|
+
end
|
568
|
+
|
569
|
+
Faulty.register(:api, api_faulty)
|
570
|
+
|
571
|
+
# Now access the instance globally
|
572
|
+
Faulty[:api]
|
573
|
+
```
|
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
|
+
|
579
|
+
```ruby
|
580
|
+
Faulty[:api].circuit('api_circuit').run { 'ok' }
|
581
|
+
```
|
582
|
+
|
583
|
+
#### Standalone Instances
|
584
|
+
|
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.
|
588
|
+
|
589
|
+
```ruby
|
590
|
+
faulty = Faulty.new
|
591
|
+
faulty.circuit('standalone_circuit')
|
592
|
+
```
|
593
|
+
|
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.
|
597
|
+
|
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:
|
603
|
+
|
604
|
+
```ruby
|
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
|
632
|
+
end
|
633
|
+
rescue Faulty::CircuitError => e
|
634
|
+
e.cause # The original error
|
635
|
+
end
|
636
|
+
```
|
637
|
+
|
638
|
+
#### With Faulty::Result
|
639
|
+
|
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.
|
645
|
+
|
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.
|
657
|
+
|
658
|
+
```ruby
|
659
|
+
if result.ok?
|
660
|
+
users = result.get
|
661
|
+
else
|
662
|
+
error = result.error
|
663
|
+
end
|
664
|
+
```
|
665
|
+
|
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.
|
670
|
+
|
671
|
+
```ruby
|
672
|
+
# Users will be nil if the result is an error
|
673
|
+
users = result.or_default
|
674
|
+
|
675
|
+
# Users will be an empty array if the result is an error
|
676
|
+
users = result.or_default([])
|
677
|
+
|
678
|
+
# Users will be the return value of the block
|
679
|
+
users = result.or_default do
|
680
|
+
# ...
|
681
|
+
end
|
682
|
+
```
|
683
|
+
|
684
|
+
As we showed in the [Basic Usage](#basic-usage) section, you can put this
|
685
|
+
together in a nice one-liner.
|
686
|
+
|
687
|
+
```ruby
|
688
|
+
Faulty.circuit('api').try_run { api.users }.or_default([])
|
689
|
+
```
|
690
|
+
|
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)
|
703
|
+
end
|
704
|
+
```
|
705
|
+
|
706
|
+
Or, if you'd instead like to specify errors to be excluded:
|
707
|
+
|
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
|
+
```
|
714
|
+
|
715
|
+
Both options can even be specified together.
|
716
|
+
|
717
|
+
```ruby
|
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!
|
727
|
+
end
|
728
|
+
```
|
729
|
+
|
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.
|
735
|
+
|
736
|
+
```ruby
|
737
|
+
Faulty.circuit('api').run(cache: 'all_users') do
|
738
|
+
api.users
|
739
|
+
end
|
740
|
+
```
|
741
|
+
|
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.
|
747
|
+
|
748
|
+
See the [Caching](#caching) section for more details on Faulty's caching
|
749
|
+
strategy.
|
750
|
+
|
751
|
+
### Configuring the Circuit Threshold
|
752
|
+
|
753
|
+
To configure how a circuit responds to error, use the `cool_down`,
|
754
|
+
`rate_threshold` and `sample_threshold` options.
|
755
|
+
|
756
|
+
#### Rate Threshold
|
757
|
+
|
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.
|
760
|
+
|
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
|
+
```
|
765
|
+
|
766
|
+
#### Sample Threshold
|
767
|
+
|
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.
|
771
|
+
|
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
|
780
|
+
|
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).
|
785
|
+
|
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
|
+
```
|
790
|
+
|
791
|
+
### Circuit Options
|
199
792
|
|
200
793
|
A circuit can be created with the following configuration options. Those options
|
201
794
|
are only set once, synchronized across threads, and will persist in-memory until
|
202
|
-
the process exits. If you're using [
|
203
|
-
within the context of each
|
204
|
-
`Faulty.circuit` (or `
|
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.
|
798
|
+
|
799
|
+
```ruby
|
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
|
805
|
+
```
|
205
806
|
|
206
807
|
This is because the circuit objects themselves are internally memoized, and are
|
207
808
|
read-only once created.
|
@@ -209,7 +810,7 @@ read-only once created.
|
|
209
810
|
The following example represents the defaults for a new circuit:
|
210
811
|
|
211
812
|
```ruby
|
212
|
-
Faulty.circuit(
|
813
|
+
Faulty.circuit('api') do |config|
|
213
814
|
# The cache backend for this circuit. Inherits the global cache by default.
|
214
815
|
config.cache = Faulty.options.cache
|
215
816
|
|
@@ -230,6 +831,10 @@ Faulty.circuit(:api) do |config|
|
|
230
831
|
# circuit to half-open.
|
231
832
|
config.cool_down = 300
|
232
833
|
|
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
|
+
|
233
838
|
# The errors that will be captured by Faulty and used to trigger circuit
|
234
839
|
# state changes.
|
235
840
|
config.errors = [StandardError]
|
@@ -237,7 +842,7 @@ Faulty.circuit(:api) do |config|
|
|
237
842
|
# Errors that should be ignored by Faulty and not captured.
|
238
843
|
config.exclude = []
|
239
844
|
|
240
|
-
# The event notifier. Inherits the
|
845
|
+
# The event notifier. Inherits the Faulty instance notifier by default
|
241
846
|
config.notifier = Faulty.options.notifier
|
242
847
|
|
243
848
|
# The minimum failure rate required to trip a circuit
|
@@ -246,7 +851,8 @@ Faulty.circuit(:api) do |config|
|
|
246
851
|
# The minimum number of runs required before a circuit can trip
|
247
852
|
config.sample_threshold = 3
|
248
853
|
|
249
|
-
# The storage backend for this circuit. Inherits the
|
854
|
+
# The storage backend for this circuit. Inherits the Faulty instance storage
|
855
|
+
# by default
|
250
856
|
config.storage = Faulty.options.storage
|
251
857
|
end
|
252
858
|
```
|
@@ -258,70 +864,62 @@ with an options hash:
|
|
258
864
|
Faulty.circuit(:api, cache_expires_in: 1800)
|
259
865
|
```
|
260
866
|
|
261
|
-
|
867
|
+
### Listing Circuits
|
262
868
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
869
|
+
For monitoring or debugging, you may need to retrieve a list of all circuit
|
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)
|
872
|
+
if you're using an instance).
|
267
873
|
|
268
|
-
|
269
|
-
|
874
|
+
You can get a list of all circuit statuses by mapping those names to their
|
875
|
+
status objects. Be careful though, since this could cause performance issues for
|
876
|
+
very large numbers of circuits.
|
270
877
|
|
271
878
|
```ruby
|
272
|
-
|
273
|
-
.
|
274
|
-
|
275
|
-
end.or_default([])
|
879
|
+
statuses = Faulty.list_circuits.map do |name|
|
880
|
+
Faulty.circuit(name).status
|
881
|
+
end
|
276
882
|
```
|
277
883
|
|
278
|
-
|
884
|
+
### Locking Circuits
|
279
885
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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.
|
886
|
+
It is possible to lock a circuit open or closed. A circuit that is locked open
|
887
|
+
will never execute its block, and always raise an `Faulty::OpenCircuitError`.
|
888
|
+
This is useful in cases where you need to manually disable a dependency
|
889
|
+
entirely. If a cached value is available, that will be returned from the circuit
|
890
|
+
until it expires, even outside its refresh period.
|
295
891
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
cache and return the new value.
|
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!)
|
300
895
|
|
301
|
-
|
302
|
-
|
896
|
+
```ruby
|
897
|
+
Faulty.circuit('broken_api').lock_open!
|
898
|
+
```
|
303
899
|
|
304
|
-
|
305
|
-
cached value
|
900
|
+
A circuit that is locked closed will never trip. This is useful in cases where a
|
901
|
+
circuit is continuously tripping incorrectly. If a cached value is available, it
|
902
|
+
will have the same behavior as an unlocked circuit.
|
306
903
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
be returned.
|
904
|
+
```ruby
|
905
|
+
Faulty.circuit('false_positive').lock_closed!
|
906
|
+
```
|
311
907
|
|
312
|
-
|
908
|
+
To remove a lock of either type:
|
313
909
|
|
314
|
-
|
315
|
-
|
316
|
-
|
910
|
+
```ruby
|
911
|
+
Faulty.circuit('fixed').unlock!
|
912
|
+
```
|
317
913
|
|
318
|
-
|
319
|
-
|
914
|
+
Locking or unlocking a circuit has no concurrency guarantees, so it's not
|
915
|
+
recommended to lock or unlock circuits from production code. Instead, locks are
|
916
|
+
intended as an emergency tool for troubleshooting and debugging.
|
320
917
|
|
321
918
|
## Event Handling
|
322
919
|
|
323
920
|
Faulty uses an event-dispatching model to deliver notifications of internal
|
324
|
-
events. The full list of events is available from
|
921
|
+
events. The full list of events is available from
|
922
|
+
[`Faulty::Events::EVENTS`](https://www.rubydoc.info/gems/faulty/Faulty/Events).
|
325
923
|
|
326
924
|
- `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
|
327
925
|
- `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
|
@@ -338,12 +936,18 @@ events. The full list of events is available from `Faulty::Events::EVENTS`.
|
|
338
936
|
closed. Payload: `circuit`
|
339
937
|
- `circuit_success` - A circuit execution was successful. Payload: `circuit`,
|
340
938
|
`status`
|
341
|
-
- `storage_failure` - A storage backend raised an error. Payload `circuit
|
342
|
-
`action`, `error`
|
939
|
+
- `storage_failure` - A storage backend raised an error. Payload `circuit` (can
|
940
|
+
be nil), `action`, `error`
|
343
941
|
|
344
942
|
By default events are logged using `Faulty::Events::LogListener`, but that can
|
345
943
|
be replaced, or additional listeners can be added.
|
346
944
|
|
945
|
+
### CallbackListener
|
946
|
+
|
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.
|
950
|
+
|
347
951
|
```ruby
|
348
952
|
Faulty.init do |config|
|
349
953
|
# Replace the default listener with a custom callback listener
|
@@ -356,157 +960,147 @@ Faulty.init do |config|
|
|
356
960
|
end
|
357
961
|
```
|
358
962
|
|
359
|
-
|
360
|
-
`Faulty::Events::ListenerInterface`. For example:
|
963
|
+
### Other Built-in Listeners
|
361
964
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
MyNotifier.alert(event, payload)
|
366
|
-
end
|
367
|
-
end
|
368
|
-
```
|
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.
|
369
968
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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.
|
375
974
|
|
376
|
-
|
975
|
+
If your favorite monitoring software is not supported here, please open a PR
|
976
|
+
that implements a listener for it.
|
377
977
|
|
378
|
-
###
|
978
|
+
### Custom Listeners
|
379
979
|
|
380
|
-
|
381
|
-
|
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:
|
382
983
|
|
383
984
|
```ruby
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
storage.max_sample_size = 100
|
985
|
+
class MyFaultyListener
|
986
|
+
def handle(event, payload)
|
987
|
+
MyNotifier.alert(event, payload)
|
388
988
|
end
|
389
989
|
end
|
390
990
|
```
|
391
991
|
|
392
|
-
### Redis
|
393
|
-
|
394
|
-
The `Faulty::Cache::Redis` backend provides distributed circuit storage using
|
395
|
-
Redis. The default configuration:
|
396
|
-
|
397
992
|
```ruby
|
398
993
|
Faulty.init do |config|
|
399
|
-
config.
|
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
|
994
|
+
config.listeners = [MyFaultyListener.new]
|
416
995
|
end
|
417
996
|
```
|
418
997
|
|
419
|
-
|
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)).
|
998
|
+
## How it Works
|
424
999
|
|
425
|
-
|
426
|
-
|
427
|
-
|
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:
|
428
1004
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
434
1010
|
|
435
|
-
|
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.
|
436
1015
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
configurations using scopes.
|
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.
|
441
1019
|
|
442
|
-
|
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.
|
443
1024
|
|
444
|
-
|
445
|
-
|
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.
|
446
1028
|
|
447
|
-
|
448
|
-
|
449
|
-
|
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.
|
450
1032
|
|
451
|
-
|
452
|
-
scope = Faulty.default
|
1033
|
+
### Caching
|
453
1034
|
|
454
|
-
|
455
|
-
|
456
|
-
|
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.
|
457
1039
|
|
458
|
-
|
1040
|
+
Once your cache is configured, you can use the `cache` parameter when running
|
1041
|
+
a circuit to specify a cache key:
|
459
1042
|
|
460
1043
|
```ruby
|
461
|
-
Faulty.
|
462
|
-
|
463
|
-
|
464
|
-
|
1044
|
+
feed = Faulty.circuit('rss_feeds')
|
1045
|
+
.try_run(cache: "rss_feeds/#{feed}") do
|
1046
|
+
fetch_feed(feed)
|
1047
|
+
end.or_default([])
|
465
1048
|
```
|
466
1049
|
|
467
|
-
|
1050
|
+
By default a circuit has the following options:
|
468
1051
|
|
469
|
-
|
470
|
-
|
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.
|
471
1063
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
end
|
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.
|
476
1067
|
|
477
|
-
|
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.
|
478
1072
|
|
479
|
-
|
480
|
-
Faulty
|
481
|
-
```
|
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.
|
482
1075
|
|
483
|
-
|
484
|
-
|
485
|
-
scope:
|
1076
|
+
If the circuit is open and the cache is hit, then Faulty will always return the
|
1077
|
+
cached value.
|
486
1078
|
|
487
|
-
|
488
|
-
|
489
|
-
|
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.
|
490
1083
|
|
491
|
-
###
|
1084
|
+
### Fault Tolerance
|
492
1085
|
|
493
|
-
|
494
|
-
|
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.
|
495
1089
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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.
|
500
1095
|
|
501
|
-
|
502
|
-
|
503
|
-
memoized circuit object.
|
1096
|
+
If the storage backend fails, circuits will default to closed. If the cache
|
1097
|
+
backend fails, all cache queries will miss.
|
504
1098
|
|
505
1099
|
## Implementing a Cache Backend
|
506
1100
|
|
507
1101
|
You can implement your own cache backend by following the documentation in
|
508
|
-
`Faulty::Cache::Interface
|
509
|
-
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:
|
510
1104
|
|
511
1105
|
```ruby
|
512
1106
|
class MyFaultyCache
|
@@ -535,25 +1129,47 @@ users.
|
|
535
1129
|
## Implementing a Storage Backend
|
536
1130
|
|
537
1131
|
You can implement your own storage backend by following the documentation in
|
538
|
-
`Faulty::Storage::Interface
|
539
|
-
|
540
|
-
|
541
|
-
|
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.
|
542
1137
|
|
543
1138
|
## Alternatives
|
544
1139
|
|
545
1140
|
Faulty has its own opinions about how to implement a circuit breaker in Ruby,
|
546
1141
|
but there are and have been many other options:
|
547
1142
|
|
548
|
-
|
549
|
-
|
550
|
-
- [
|
1143
|
+
### Currently Active
|
1144
|
+
|
1145
|
+
- [semian](https://github.com/Shopify/semian): A resiliency toolkit that
|
1146
|
+
includes circuit breakers. It uses adapters to auto-wire circuits, and it has
|
1147
|
+
only in-memory storage by design.
|
1148
|
+
- [circuitbox](https://github.com/yammer/circuitbox): Similar in design to
|
1149
|
+
Faulty, but with a different API. It uses Moneta to abstract circuit storage
|
1150
|
+
to allow any key-value store.
|
1151
|
+
|
1152
|
+
### Previous Work
|
1153
|
+
|
1154
|
+
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
|
1155
|
+
recent activity)
|
1156
|
+
- [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
|
551
1157
|
- [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
|
552
1158
|
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
553
1159
|
(unmaintained)
|
554
1160
|
- [breaker](https://github.com/ahawkins/breaker) (unmaintained)
|
555
1161
|
- [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
|
556
1162
|
|
1163
|
+
### Faulty's Unique Features
|
1164
|
+
|
1165
|
+
- Simple API but configurable for advanced users
|
1166
|
+
- Pluggable storage backends (circuitbox also has this)
|
1167
|
+
- Protected storage access with fallback to safe storage
|
1168
|
+
- Global, or object-oriented configuration with multiple instances
|
1169
|
+
- Integrated caching support tailored for fault-tolerance
|
1170
|
+
- Manually lock circuits open or closed
|
1171
|
+
|
557
1172
|
[api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
|
1173
|
+
[michael nygard]: https://www.michaelnygard.com/
|
558
1174
|
[martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
|
559
1175
|
[hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works
|