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