faulty 0.1.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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