faulty 0.1.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +50 -2
  5. data/Gemfile +22 -0
  6. data/README.md +836 -220
  7. data/bin/check-version +5 -1
  8. data/bin/console +1 -1
  9. data/faulty.gemspec +4 -11
  10. data/lib/faulty.rb +157 -43
  11. data/lib/faulty/cache.rb +3 -1
  12. data/lib/faulty/cache/auto_wire.rb +58 -0
  13. data/lib/faulty/cache/circuit_proxy.rb +61 -0
  14. data/lib/faulty/cache/default.rb +10 -21
  15. data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
  16. data/lib/faulty/cache/interface.rb +1 -1
  17. data/lib/faulty/cache/mock.rb +1 -1
  18. data/lib/faulty/cache/null.rb +1 -1
  19. data/lib/faulty/cache/rails.rb +9 -10
  20. data/lib/faulty/circuit.rb +10 -5
  21. data/lib/faulty/error.rb +18 -4
  22. data/lib/faulty/events.rb +3 -2
  23. data/lib/faulty/events/callback_listener.rb +1 -1
  24. data/lib/faulty/events/honeybadger_listener.rb +53 -0
  25. data/lib/faulty/events/listener_interface.rb +1 -1
  26. data/lib/faulty/events/log_listener.rb +5 -6
  27. data/lib/faulty/events/notifier.rb +11 -2
  28. data/lib/faulty/immutable_options.rb +1 -1
  29. data/lib/faulty/result.rb +2 -2
  30. data/lib/faulty/status.rb +3 -2
  31. data/lib/faulty/storage.rb +4 -1
  32. data/lib/faulty/storage/auto_wire.rb +107 -0
  33. data/lib/faulty/storage/circuit_proxy.rb +64 -0
  34. data/lib/faulty/storage/fallback_chain.rb +207 -0
  35. data/lib/faulty/storage/fault_tolerant_proxy.rb +51 -56
  36. data/lib/faulty/storage/interface.rb +1 -1
  37. data/lib/faulty/storage/memory.rb +8 -4
  38. data/lib/faulty/storage/redis.rb +75 -13
  39. data/lib/faulty/version.rb +2 -2
  40. metadata +18 -122
  41. data/.travis.yml +0 -44
  42. data/lib/faulty/scope.rb +0 -117
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1da265b4bb2e4ae865726930ec3855a03d31c3b47c92b9ef08d7519cdb9d9d9
4
- data.tar.gz: a2fdbb6d1ccdfb7be6c68467ce45a802644ef4f9a61769e8b7387d626307facd
3
+ metadata.gz: 7e68341d29ccefb2a4fa4c265f3f2d5ec37371d37cb41d5a186b3f25d2130b46
4
+ data.tar.gz: 56e659d66871738e8818c4874102de04ef97873cb0d1c444d58259c875b21378
5
5
  SHA512:
6
- metadata.gz: 2ad680693b76db3f3d420d7ded71269c8ad669c23bffa0e4ab598d8d0772955c70479d1497ea51b7aa9691b8438e06bc83ff13d4f3e3f6842a3327971dcbb4d3
7
- data.tar.gz: 2890441590276ecd72e16ee9866955b027a3e1df26907f9cc71a94f3f1161c01ea30665860bcf526fe871e84a10a7c9c728c050bbf961d8b9d670b4a6fb4bbfa
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
  [![Gem Version](https://badge.fury.io/rb/faulty.svg)](https://badge.fury.io/rb/faulty)
4
- [![Build Status](https://travis-ci.org/ParentSquare/faulty.svg?branch=master)](https://travis-ci.org/ParentSquare/faulty)
5
- [![Code Climate](https://codeclimate.com/github/ParentSquare/faulty/badges/gpa.svg)](https://codeclimate.com/github/ParentSquare/faulty)
6
- [![Test Coverage](https://codeclimate.com/github/ParentSquare/faulty/badges/coverage.svg)](https://codeclimate.com/github/ParentSquare/faulty)
4
+ [![CI](https://github.com/ParentSquare/faulty/workflows/CI/badge.svg)](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
5
+ [![Code Quality](https://app.codacy.com/project/badge/Grade/16bb1df1569a4ddba893a866673dac2a)](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=ParentSquare/faulty&amp;utm_campaign=Badge_Grade)
6
+ [![Code Coverage](https://app.codacy.com/project/badge/Coverage/16bb1df1569a4ddba893a866673dac2a)](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
  [![Inline docs](http://inch-ci.org/github/ParentSquare/faulty.svg?branch=master)](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
- users = Faulty.circuit(:api).try_run do
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 `Faulty.init`. For Rails, you would do this in
32
- `config/initializers/faulty.rb`. See [Setup](#setup) for details.
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
- [Global Configuration](#global-configuration) section.
159
+ [Configuration](#configuration) section.
69
160
 
70
161
  ## Basic Usage
71
162
 
72
- To create a circuit, call `Faulty.circuit`. This can be done as you use the
73
- circuit, or you can set it up beforehand. Any options passed to the `circuit`
74
- method are synchronized across threads and saved as long as the process is alive.
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, cache_refreshes_after: 1800)
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.cache_refreshes_after == 1800 # => true
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 `Faulty::CircuitError` will be raised. It is
99
- up to your application to handle that error however necessary or crash. Often
100
- though, you don't want to crash your application when a circuit fails, but
101
- instead apply a fallback or default behavior. For this, Faulty provides the
102
- `try_run` method:
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` method returns a result type instead of raising errors. See the
117
- API docs for `Result` for more information. Here we use it to check whether the
118
- result is `ok?` (not an error). If it is we set the users variable, otherwise we
119
- set a default of an empty array. This pattern is so common, that `Result` also
120
- implements a helper method `or_default` to do the same thing:
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
- ## How it Works
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
- Faulty implements a version of circuit breakers inspired by
131
- [Martin Fowler's post][martin fowler] on the subject. A few notable features of
132
- Faulty's implementation are:
228
+ ## What is this for?
133
229
 
134
- - Rate-based failure thresholds
135
- - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
136
- cache jitter and error fallback.
137
- - Event-based monitoring
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
- Following the principals of the circuit-breaker pattern, the block given to
140
- `run` or `try_run` will always be executed as long as long as it never raises an
141
- error. If the block _does_ raise an error, then the circuit keeps track of the
142
- number of runs and the failure rate.
234
+ ```ruby
235
+ TextApi.send(message)
236
+ ```
143
237
 
144
- Once both thresholds are breached, the circuit is opened. Once open, the
145
- circuit starts the cool-down period. Any executions within that cool-down are
146
- skipped, and a `Faulty::OpenCircuitError` will be raised.
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
- After the cool-down has elapsed, the circuit enters the half-open state. In this
149
- state, Faulty allows a single execution of the block as a test run. If the test
150
- run succeeds, the circuit is fully opened and the circuit state is reset. If the
151
- test run fails, the circuit is closed and the cool-down is reset.
243
+ ```ruby
244
+ TextApi.send(message, timeout: 5)
245
+ ```
152
246
 
153
- Each time the circuit changes state or executes the block, events are raised
154
- that are sent to the Faulty event notifier. The notifier should be used to track
155
- circuit failure rates, open circuits, etc.
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
- In addition to the classic circuit breaker design, Faulty implements caching
158
- that is integrated with the circuit state. See [Caching](#caching) for more
159
- detail.
251
+ ```ruby
252
+ Faulty.circuit('text_api').run do
253
+ TextApi.send(message, timeout: 5)
254
+ end
255
+ ```
160
256
 
161
- ## Global Configuration
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
- `Faulty.init` can set the following global configuration options. This example
164
- illustrates the default values. It is also possible to define multiple
165
- non-global configuration scopes (see [Scopes](#scopes)).
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
- ## Circuit Options
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 [scopes](#scopes), the options are retained
203
- within the context of each scope. All options given after the first call to
204
- `Faulty.circuit` (or `Scope.circuit` are ignored.
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(:api) do |config|
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 global notifier by default
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 global storage by default
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
- ## Caching
867
+ ### Listing Circuits
262
868
 
263
- Faulty integrates caching into it's circuits in a way that is particularly
264
- suited to fault-tolerance. To make use of caching, you must specify the `cache`
265
- configuration option when initializing Faulty or creating a scope. If you're
266
- using Rails, this is automatically set to the Rails cache.
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
- Once your cache is configured, you can use the `cache` parameter when running
269
- a circuit to specify a cache key:
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
- feed = Faulty.circuit(:rss_feeds)
273
- .try_run(cache: "rss_feeds/#{feed}") do
274
- fetch_feed(feed)
275
- end.or_default([])
879
+ statuses = Faulty.list_circuits.map do |name|
880
+ Faulty.circuit(name).status
881
+ end
276
882
  ```
277
883
 
278
- By default a circuit has the following options:
884
+ ### Locking Circuits
279
885
 
280
- - `cache_expires_in`: 86400 (1 day). This is sent to the cache backend and
281
- defines how long the cache entry should be stored. After this time elapses,
282
- queries will result in a cache miss.
283
- - `cache_refreshes_after`: 900 (15 minutes). This is used internally by Faulty
284
- to indicate when a cache should be refreshed. It does not affect how long the
285
- cache entry is stored.
286
- - `cache_refresh_jitter`: 180 (3 minutes = 20% of `cache_refreshes_after`). The
287
- maximum number of seconds to randomly add or subtract from
288
- `cache_refreshes_after` when determining whether to refresh a cache entry.
289
- This mitigates the "thundering herd" effect caused by many processes
290
- simultaneously refreshing the cache.
291
-
292
- This code will attempt to fetch an RSS feed protected by a circuit. If the feed
293
- is within the cache refresh period, then the result will be returned from the
294
- cache and the block will not be executed regardless of the circuit state.
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
- If the cache is hit, but outside its refresh period, then Faulty will check the
297
- circuit state. If the circuit is closed or half-open, then it will run the
298
- block. If the block is successful, then it will update the circuit, write to the
299
- cache and return the new value.
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
- However, if the cache is hit and the block fails, then that failure is noted
302
- in the circuit and Faulty returns the cached value.
896
+ ```ruby
897
+ Faulty.circuit('broken_api').lock_open!
898
+ ```
303
899
 
304
- If the circuit is open and the cache is hit, then Faulty will always return the
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
- If the cache query results in a miss, then faulty operates as normal. In the
308
- code above, if the circuit is closed, the block will be executed. If the block
309
- succeeds, the cache is refreshed. If the block fails, the default of `[]` will
310
- be returned.
904
+ ```ruby
905
+ Faulty.circuit('false_positive').lock_closed!
906
+ ```
311
907
 
312
- ## Fault Tolerance
908
+ To remove a lock of either type:
313
909
 
314
- Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
315
- the storage or cache backends are captured and suppressed. Failure events for
316
- these errors are sent to the notifier.
910
+ ```ruby
911
+ Faulty.circuit('fixed').unlock!
912
+ ```
317
913
 
318
- If the storage backend fails, all circuits will default to open. If the cache
319
- backend fails, all cache queries will miss.
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 `Faulty::Events::EVENTS`.
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
- You can implement your own listener by following the documentation in
360
- `Faulty::Events::ListenerInterface`. For example:
963
+ ### Other Built-in Listeners
361
964
 
362
- ```ruby
363
- class MyFaultyListener
364
- def handle(event, payload)
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
- ```ruby
371
- Faulty.init do |config|
372
- config.listeners = [MyFaultyListener.new]
373
- end
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
- ## Configuring the Storage Backend
975
+ If your favorite monitoring software is not supported here, please open a PR
976
+ that implements a listener for it.
377
977
 
378
- ### Memory
978
+ ### Custom Listeners
379
979
 
380
- The `Faulty::Cache::Memory` backend is the default storage backend. The default
381
- configuration:
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
- Faulty.init do |config|
385
- config.storage = Faulty::Storage::Memory.new do |storage|
386
- # The maximum number of circuit runs that will be stored
387
- storage.max_sample_size = 100
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.storage = Faulty::Storage::Redis.new do |storage|
400
- # The Redis client. Accepts either a Redis instance, or a ConnectionPool
401
- # of Redis instances.
402
- storage.client = ::Redis.new
403
-
404
- # The prefix to prepend to all redis keys used by Faulty circuits
405
- storage.key_prefix = 'faulty'
406
-
407
- # A string to separate the parts of the redis key
408
- storage.key_separator: ':'
409
-
410
- # The maximum number of circuit runs that will be stored
411
- storage.max_sample_size = 100
412
-
413
- # The maximum number of seconds that a circuit run will be stored
414
- storage.sample_ttl = 1800
415
- end
994
+ config.listeners = [MyFaultyListener.new]
416
995
  end
417
996
  ```
418
997
 
419
- ### Listing Circuits
420
-
421
- For monitoring or debugging, you may need to retrieve a list of all circuit
422
- names. This is possible with `Faulty.list_circuits` (or the equivalent method on
423
- your [scope](#scopes)).
998
+ ## How it Works
424
999
 
425
- You can get a list of all circuit statuses by mapping those names to their
426
- status objects. Be careful though, since this could cause performance issues for
427
- very large numbers of circuits.
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
- ```ruby
430
- statuses = Faulty.list_circuits.map do |name|
431
- Faulty.circuit(name).status
432
- end
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
- ## Scopes
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
- It is possible to have multiple configurations of Faulty running within the same
438
- process. The most common configuration is to simply use `Faulty.init` to
439
- configure Faulty globally, however it is possible to have additional
440
- configurations using scopes.
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
- ### The default scope
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
- When you call `Faulty.init`, you are actually creating the default scope. You
445
- can access this scope directly by calling `Faulty.default`.
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
- ```ruby
448
- # We create the default scope
449
- Faulty.init
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
- # Access the default scope
452
- scope = Faulty.default
1033
+ ### Caching
453
1034
 
454
- # Alternatively, access the scope by name
455
- scope = Faulty[:default]
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
- You can rename the default scope if desired:
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.init(:custom_default)
462
-
463
- scope = Faulty.default
464
- scope = Faulty[:custom_default]
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
- ### Multiple Scopes
1050
+ By default a circuit has the following options:
468
1051
 
469
- If you want multiple scopes, but want global, thread-safe access to
470
- them, you can use `Faulty.register`:
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
- ```ruby
473
- api_scope = Faulty::Scope.new do |config|
474
- # This accepts the same options as Faulty.init
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
- Faulty.register(:api, api_scope)
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
- # Now access the scope globally
480
- Faulty[:api]
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
- When you call `Faulty.circuit`, that's the same as calling
484
- `Faulty.default.circuit`, so you can apply the same API to any other Faulty
485
- scope:
1076
+ If the circuit is open and the cache is hit, then Faulty will always return the
1077
+ cached value.
486
1078
 
487
- ```ruby
488
- Faulty[:api].circuit(:api_circuit).run { 'ok' }
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
- ### Standalone Scopes
1084
+ ### Fault Tolerance
492
1085
 
493
- If you choose, you can use Faulty scopes without registering them globally. This
494
- could be useful if you prefer dependency injection over global state.
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
- ```ruby
497
- faulty = Faulty::Scope.new
498
- faulty.circuit(:standalone_circuit)
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
- Calling `circuit` on the scope still has the same memoization behavior that
502
- `Faulty.circuit` has, so subsequent calls to the same circuit will return a
503
- memoized circuit object.
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`. It is a fairly simple API, with only get/set
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`. Since the storage has some tricky requirements
539
- regarding concurrency, the `Faulty::Storage::Memory` can be used as a reference
540
- implementation. Feel free to open a pull request if your storage backend
541
- would be useful for other users.
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
- - [circuitbox](https://github.com/yammer/circuitbox)
549
- - [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby)
550
- - [stoplight](https://github.com/orgsync/stoplight) (currently unmaintained)
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