faulty 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0712dd797dfe2922e4e52a792f48c5ac72c4b9766d218dfa597e7ad630f9fd0
4
- data.tar.gz: fb4a787507006131f34a301fc164466fd8befb72051550ca0a63fc2d69ec949c
3
+ metadata.gz: 7e68341d29ccefb2a4fa4c265f3f2d5ec37371d37cb41d5a186b3f25d2130b46
4
+ data.tar.gz: 56e659d66871738e8818c4874102de04ef97873cb0d1c444d58259c875b21378
5
5
  SHA512:
6
- metadata.gz: 5381dd13f058045906330c67ff80aeefac55ae81077eb4552d5461d743344fa097e205b8ac3a39b385c52976e64315aec4e903c1a0c298a05452fab7788d1df2
7
- data.tar.gz: af31cbf86cac54226189f8f63b1d8b0bcf82f9023085e98d07e4e9db65e402883c2219dbf255e21427c40b0c0a66ec71bd4dc9dfdd94fb9bc7b574ddf8426674
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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
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
+
1
17
  ## Release v0.3.0
2
18
 
3
19
  * Add tools for backend fault-tolerance #10
data/Gemfile CHANGED
@@ -16,7 +16,12 @@ gem 'byebug', platforms: not_jruby
16
16
  gem 'irb', '~> 1.0'
17
17
  gem 'redcarpet', '~> 3.5', platforms: not_jruby
18
18
  gem 'rspec_junit_formatter', '~> 0.4'
19
- # For now, code climate doesn't support simplecov 0.18
20
- # https://github.com/codeclimate/test-reporter/issues/413
21
- gem 'simplecov', '>= 0.17.1', '< 0.18'
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'
22
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,66 +147,31 @@ Faulty.init do |config|
64
147
  end
65
148
  ```
66
149
 
67
- For a full list of configuration options, see the
68
- [Global Configuration](#global-configuration) section.
69
-
70
- ## What is this for?
71
-
72
- Circuit breakers are a fault-tolerance tool for creating separation between your
73
- application and external dependencies. For example, your application may call an
74
- external API to send a text message:
75
-
76
- ```ruby
77
- TextApi.send(message)
78
- ```
79
-
80
- In normal operation, this API call is very fast. However what if the texting
81
- service started hanging? Your application would quickly use up a lot of
82
- resources waiting for requests to return from the service. You could consider
83
- adding a timeout to your request:
84
-
85
- ```ruby
86
- TextApi.send(message, timeout: 5)
87
- ```
88
-
89
- Now your application will terminate requests after 5 seconds, but that could
90
- still add up to a lot of resources if you call this thousands of times. Circuit
91
- breakers solve this problem.
150
+ Or use a faulty instance instead for an object-oriented approach
92
151
 
93
152
  ```ruby
94
- Faulty.circuit(:text_api).run do
95
- TextApi.send(message, timeout: 5)
153
+ faulty = Faulty.new do
154
+ config.storage = Faulty::Storage::Redis.new
96
155
  end
97
156
  ```
98
157
 
99
- Now, when the text API hangs, the first few will run and start timing out. This
100
- will trip the circuit. After the circuit trips
101
- (see [How it Works](#how-it-works)), calls to the text API will be paused for
102
- the configured cool down period. Your application resources are not overwhelmed.
103
-
104
- You are free to implement a fallback or error handling however you wish, for
105
- example, in this case, you might add the text message to a failure queue:
106
-
107
- ```ruby
108
- Faulty.circuit(:text_api).run do
109
- TextApi.send(message, timeout: 5)
110
- rescue Faulty::CircuitError => e
111
- FailureQueue.enqueue(message)
112
- end
113
- ```
158
+ For a full list of configuration options, see the
159
+ [Configuration](#configuration) section.
114
160
 
115
161
  ## Basic Usage
116
162
 
117
- To create a circuit, call `Faulty.circuit`. This can be done as you use the
118
- circuit, or you can set it up beforehand. Any options passed to the `circuit`
119
- 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.
120
168
 
121
169
  ```ruby
122
- circuit1 = Faulty.circuit(:api, cache_refreshes_after: 1800)
170
+ circuit1 = Faulty.circuit(:api, rate_threshold: 0.6)
123
171
 
124
172
  # The options from above are also used when called here
125
173
  circuit2 = Faulty.circuit(:api)
126
- circuit2.options.cache_refreshes_after == 1800 # => true
174
+ circuit2.options.rate_threshold == 0.6 # => true
127
175
 
128
176
  # The same circuit is returned on each consecutive call
129
177
  circuit1.equal?(circuit2) # => true
@@ -140,11 +188,12 @@ end
140
188
  See [How it Works](#how-it-works) for more details about how Faulty handles
141
189
  circuit failures.
142
190
 
143
- If the `run` block above fails, a `Faulty::CircuitError` will be raised. It is
144
- up to your application to handle that error however necessary or crash. Often
145
- though, you don't want to crash your application when a circuit fails, but
146
- instead apply a fallback or default behavior. For this, Faulty provides the
147
- `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:
148
197
 
149
198
  ```ruby
150
199
  result = Faulty.circuit(:api).try_run do
@@ -158,11 +207,13 @@ else
158
207
  end
159
208
  ```
160
209
 
161
- The `try_run` method returns a result type instead of raising errors. See the
162
- API docs for `Result` for more information. Here we use it to check whether the
163
- result is `ok?` (not an error). If it is we set the users variable, otherwise we
164
- set a default of an empty array. This pattern is so common, that `Result` also
165
- 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:
166
217
 
167
218
  ```ruby
168
219
  users = Faulty.circuit(:api).try_run do
@@ -170,40 +221,54 @@ users = Faulty.circuit(:api).try_run do
170
221
  end.or_default([])
171
222
  ```
172
223
 
173
- ## 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.
174
227
 
175
- Faulty implements a version of circuit breakers inspired by "Release It!: Design
176
- and Deploy Production-Ready Software" by [Michael T. Nygard][michael nygard] and
177
- [Martin Fowler's post][martin fowler] on the subject. A few notable features of
178
- Faulty's implementation are:
228
+ ## What is this for?
179
229
 
180
- - Rate-based failure thresholds
181
- - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
182
- cache jitter and error fallback.
183
- - Event-based monitoring
184
- - Flexible fault-tolerant storage with optional fallbacks
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:
185
233
 
186
- Following the principals of the circuit-breaker pattern, the block given to
187
- `run` or `try_run` will always be executed as long as it never raises an error.
188
- If the block _does_ raise an error, then the circuit keeps track of the number
189
- of runs and the failure rate.
234
+ ```ruby
235
+ TextApi.send(message)
236
+ ```
190
237
 
191
- Once both thresholds are breached, the circuit is opened. Once open, the
192
- circuit starts the cool-down period. Any executions within that cool-down are
193
- 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:
194
242
 
195
- After the cool-down has elapsed, the circuit enters the half-open state. In this
196
- state, Faulty allows a single execution of the block as a test run. If the test
197
- run succeeds, the circuit is fully opened and the circuit state is reset. If the
198
- test run fails, the circuit is closed and the cool-down is reset.
243
+ ```ruby
244
+ TextApi.send(message, timeout: 5)
245
+ ```
199
246
 
200
- Each time the circuit changes state or executes the block, events are raised
201
- that are sent to the Faulty event notifier. The notifier should be used to track
202
- 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.
203
250
 
204
- In addition to the classic circuit breaker design, Faulty implements caching
205
- that is integrated with the circuit state. See [Caching](#caching) for more
206
- detail.
251
+ ```ruby
252
+ Faulty.circuit('text_api').run do
253
+ TextApi.send(message, timeout: 5)
254
+ end
255
+ ```
256
+
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.
261
+
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
+ ```
207
272
 
208
273
  ## Configuration
209
274
 
@@ -222,6 +287,10 @@ Faulty.init do |config|
222
287
  # AutoWire API docs for more details.
223
288
  config.cache = Faulty::Cache::Default.new
224
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
+
225
294
  # The storage backend. By default, Faulty uses an in-memory store. For most
226
295
  # production applications, you'll want a more robust backend. Faulty also
227
296
  # provides Faulty::Storage::Redis for this.
@@ -269,384 +338,537 @@ hash. For example, `Faulty.init` could be called like this:
269
338
  Faulty.init(cache: Faulty::Cache::Null.new)
270
339
  ```
271
340
 
272
- ## Circuit Options
341
+ ### Configuring the Storage Backend
273
342
 
274
- A circuit can be created with the following configuration options. Those options
275
- are only set once, synchronized across threads, and will persist in-memory until
276
- the process exits. If you're using [multiple configurations](#multiple-configurations),
277
- the options are retained within the context of each instance. All options given
278
- after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
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.
279
346
 
280
- This is because the circuit objects themselves are internally memoized, and are
281
- read-only once created.
347
+ #### Memory
282
348
 
283
- The following example represents the defaults for a new circuit:
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:
284
358
 
285
359
  ```ruby
286
- Faulty.circuit(:api) do |config|
287
- # The cache backend for this circuit. Inherits the global cache by default.
288
- config.cache = Faulty.options.cache
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
+ ```
289
367
 
290
- # The number of seconds before a cache entry is expired. After this time, the
291
- # cache entry may be fully deleted. If set to nil, the cache will not expire.
292
- config.cache_expires_in = 86400
368
+ #### Redis
293
369
 
294
- # The number of seconds before a cache entry should be refreshed. See the
295
- # Caching section for more detail. A value of nil disables cache refreshing.
296
- config.cache_refreshes_after = 900
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.
297
376
 
298
- # The number of seconds to add or subtract from cache_refreshes_after
299
- # when determining whether a cache entry should be refreshed. Helps mitigate
300
- # the "thundering herd" effect
301
- config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
377
+ The default configuration:
302
378
 
303
- # After a circuit is opened, the number of seconds to wait before moving the
304
- # circuit to half-open.
305
- config.cool_down = 300
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)
306
386
 
307
- # The errors that will be captured by Faulty and used to trigger circuit
308
- # state changes.
309
- config.errors = [StandardError]
387
+ # The prefix to prepend to all redis keys used by Faulty circuits
388
+ storage.key_prefix = 'faulty'
310
389
 
311
- # Errors that should be ignored by Faulty and not captured.
312
- config.exclude = []
390
+ # A string to separate the parts of the redis key
391
+ storage.key_separator = ':'
313
392
 
314
- # The event notifier. Inherits the global notifier by default
315
- config.notifier = Faulty.options.notifier
393
+ # The maximum number of circuit runs that will be stored
394
+ storage.max_sample_size = 100
316
395
 
317
- # The minimum failure rate required to trip a circuit
318
- config.rate_threshold = 0.5
396
+ # The maximum number of seconds that a circuit run will be stored
397
+ storage.sample_ttl = 1800
319
398
 
320
- # The minimum number of runs required before a circuit can trip
321
- config.sample_threshold = 3
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
322
402
 
323
- # The storage backend for this circuit. Inherits the global storage by default
324
- config.storage = Faulty.options.storage
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
325
410
  end
326
411
  ```
327
412
 
328
- Following the same convention as `Faulty.init`, circuits can also be created
329
- with an options hash:
413
+ #### FallbackChain
330
414
 
331
- ```ruby
332
- Faulty.circuit(:api, cache_expires_in: 1800)
333
- ```
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.
334
420
 
335
- ## Caching
421
+ For example, you may configure Redis as your primary storage backend, with an
422
+ in-memory storage backend as a fallback:
336
423
 
337
- Faulty integrates caching into it's circuits in a way that is particularly
338
- suited to fault-tolerance. To make use of caching, you must specify the `cache`
339
- configuration option when initializing Faulty or creating a new Faulty instance.
340
- If you're using Rails, this is automatically set to the Rails cache.
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
+ ```
341
432
 
342
- Once your cache is configured, you can use the `cache` parameter when running
343
- a circuit to specify a cache key:
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:
344
435
 
345
436
  ```ruby
346
- feed = Faulty.circuit(:rss_feeds)
347
- .try_run(cache: "rss_feeds/#{feed}") do
348
- fetch_feed(feed)
349
- end.or_default([])
437
+ Faulty.init do |config|
438
+ config.storage = [
439
+ Faulty::Storage::Redis.new,
440
+ Faulty::Storage::Memory.new
441
+ ]
442
+ end
350
443
  ```
351
444
 
352
- By default a circuit has the following options:
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.
353
449
 
354
- - `cache_expires_in`: 86400 (1 day). This is sent to the cache backend and
355
- defines how long the cache entry should be stored. After this time elapses,
356
- queries will result in a cache miss.
357
- - `cache_refreshes_after`: 900 (15 minutes). This is used internally by Faulty
358
- to indicate when a cache should be refreshed. It does not affect how long the
359
- cache entry is stored.
360
- - `cache_refresh_jitter`: 180 (3 minutes = 20% of `cache_refreshes_after`). The
361
- maximum number of seconds to randomly add or subtract from
362
- `cache_refreshes_after` when determining whether to refresh a cache entry.
363
- This mitigates the "thundering herd" effect caused by many processes
364
- simultaneously refreshing the cache.
450
+ #### Storage::FaultTolerantProxy
365
451
 
366
- This code will attempt to fetch an RSS feed protected by a circuit. If the feed
367
- is within the cache refresh period, then the result will be returned from the
368
- cache and the block will not be executed regardless of the circuit state.
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)).
369
454
 
370
- If the cache is hit, but outside its refresh period, then Faulty will check the
371
- circuit state. If the circuit is closed or half-open, then it will run the
372
- block. If the block is successful, then it will update the circuit, write to the
373
- cache and return the new value.
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.
374
459
 
375
- However, if the cache is hit and the block fails, then that failure is noted
376
- in the circuit and Faulty returns the cached value.
460
+ If you wish your application to use a secondary storage backend instead of
461
+ failing closed, use [`FallbackChain`](#storagefallbackchain).
377
462
 
378
- If the circuit is open and the cache is hit, then Faulty will always return the
379
- cached value.
463
+ #### Storage::CircuitProxy
380
464
 
381
- If the cache query results in a miss, then faulty operates as normal. In the
382
- code above, if the circuit is closed, the block will be executed. If the block
383
- succeeds, the cache is refreshed. If the block fails, the default of `[]` will
384
- be returned.
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)).
385
467
 
386
- ## Fault Tolerance
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.
387
472
 
388
- Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
389
- the storage or cache backends are captured and suppressed. Failure events for
390
- these errors are sent to the notifier.
473
+ Typically this is used inside a [`FaultTolerantProxy`](#storagefaulttolerantproxy) or
474
+ [`FallbackChain`](#storagefallbackchain) so that these storage failures are handled
475
+ gracefully.
391
476
 
392
- In case of a flaky storage or cache backend, Faulty also uses independent
393
- in-memory circuits to track failures so that we don't keep calling a backend
394
- that is failing. See the API docs for `Cache::AutoWire`, and `Storage::AutoWire`
395
- for more details.
477
+ ### Configuring the Cache Backend
396
478
 
397
- If the storage backend fails, circuits will default to closed. If the cache
398
- backend fails, all cache queries will miss.
479
+ #### Null
399
480
 
400
- ## Event Handling
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.
401
484
 
402
- Faulty uses an event-dispatching model to deliver notifications of internal
403
- events. The full list of events is available from `Faulty::Events::EVENTS`.
485
+ #### Rails
404
486
 
405
- - `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
406
- - `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
407
- - `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
408
- - `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
409
- - `circuit_closed` - A circuit closed. Payload: `circuit`
410
- - `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
411
- `status`, `error`
412
- - `circuit_opened` - A circuit execution caused the circuit to open. Payload
413
- `circuit`, `error`
414
- - `circuit_reopened` - A circuit execution cause the circuit to reopen from
415
- half-open. Payload: `circuit`, `error`.
416
- - `circuit_skipped` - A circuit execution was skipped because the circuit is
417
- closed. Payload: `circuit`
418
- - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
419
- `status`
420
- - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
421
- be nil), `action`, `error`
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`.
422
492
 
423
- By default events are logged using `Faulty::Events::LogListener`, but that can
424
- be replaced, or additional listeners can be added.
493
+ ```ruby
494
+ Faulty.init do |config|
495
+ config.cache = Faulty::Cache::Rails.new(
496
+ ActiveSupport::Cache::RedisCacheStore.new
497
+ )
498
+ end
499
+ ```
425
500
 
426
- ### CallbackListener
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`).
427
505
 
428
- The callback listener is useful for ad-hoc handling of events. You can specify
429
- an event handler by calling a method on the callback handler by the same name.
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).
430
537
 
431
538
  ```ruby
432
- Faulty.init do |config|
433
- # Replace the default listener with a custom callback listener
434
- listener = Faulty::Events::CallbackListener.new do |events|
435
- events.circuit_opened do |payload|
436
- MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
437
- end
438
- end
439
- config.listeners = [listener]
440
- end
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]
441
547
  ```
442
548
 
443
- ### Other Built-in Listeners
549
+ You can rename the default instance if desired:
444
550
 
445
- In addition to the log and callback listeners, Faulty intends to implement
446
- built-in service-specific handlers to make it easy to integrate with monitoring
447
- and reporting software.
551
+ ```ruby
552
+ Faulty.init(:custom_default)
448
553
 
449
- - `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
450
- the Honeybadger error reporting service.
554
+ instance = Faulty.default
555
+ instance = Faulty[:custom_default]
556
+ ```
451
557
 
452
- ### Custom Listeners
558
+ #### Multiple Instances
453
559
 
454
- You can implement your own listener by following the documentation in
455
- `Faulty::Events::ListenerInterface`. For example:
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):
456
563
 
457
564
  ```ruby
458
- class MyFaultyListener
459
- def handle(event, payload)
460
- MyNotifier.alert(event, payload)
461
- end
565
+ api_faulty = Faulty.new do |config|
566
+ # This accepts the same options as Faulty.init
462
567
  end
568
+
569
+ Faulty.register(:api, api_faulty)
570
+
571
+ # Now access the instance globally
572
+ Faulty[:api]
463
573
  ```
464
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
+
465
579
  ```ruby
466
- Faulty.init do |config|
467
- config.listeners = [MyFaultyListener.new]
468
- end
580
+ Faulty[:api].circuit('api_circuit').run { 'ok' }
469
581
  ```
470
582
 
471
- ## Configuring the Storage Backend
583
+ #### Standalone Instances
472
584
 
473
- A storage backend is required to use Faulty. By default, it uses in-memory
474
- storage, but Redis is also available, along with a number of wrappers used to
475
- improve resiliency and fault-tolerance.
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.
476
588
 
477
- ### Memory
589
+ ```ruby
590
+ faulty = Faulty.new
591
+ faulty.circuit('standalone_circuit')
592
+ ```
478
593
 
479
- The `Faulty::Storage::Memory` backend is the default storage backend. You may
480
- prefer this implementation if you want to avoid the complexity and potential
481
- failure-mode of cross-network circuit storage. The trade-off is that circuit
482
- state is only contained within a single process and will not be saved across
483
- application restarts. Locks will also be cleared on restart.
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.
484
597
 
485
- The default configuration:
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:
486
603
 
487
604
  ```ruby
488
- Faulty.init do |config|
489
- config.storage = Faulty::Storage::Memory.new do |storage|
490
- # The maximum number of circuit runs that will be stored
491
- storage.max_sample_size = 100
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
492
632
  end
633
+ rescue Faulty::CircuitError => e
634
+ e.cause # The original error
493
635
  end
494
636
  ```
495
637
 
496
- ### Redis
638
+ #### With Faulty::Result
497
639
 
498
- The `Faulty::Storage::Redis` backend provides distributed circuit storage using
499
- Redis. Although Faulty takes steps to reduce risk
500
- (See [Fault Tolerance](#fault-tolerance)), using cross-network storage does
501
- introduce some additional failure modes. To reduce this risk, be sure to set
502
- conservative timeouts for your Redis connection. Setting high timeouts will
503
- print warnings to stderr.
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.
504
645
 
505
- The default configuration:
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.
506
657
 
507
658
  ```ruby
508
- Faulty.init do |config|
509
- config.storage = Faulty::Storage::Redis.new do |storage|
510
- # The Redis client. Accepts either a Redis instance, or a ConnectionPool
511
- # of Redis instances. A low timeout is highly recommended to prevent
512
- # cascading failures when evaluating circuits.
513
- storage.client = ::Redis.new(timeout: 1)
659
+ if result.ok?
660
+ users = result.get
661
+ else
662
+ error = result.error
663
+ end
664
+ ```
514
665
 
515
- # The prefix to prepend to all redis keys used by Faulty circuits
516
- storage.key_prefix = 'faulty'
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.
517
670
 
518
- # A string to separate the parts of the redis key
519
- storage.key_separator = ':'
671
+ ```ruby
672
+ # Users will be nil if the result is an error
673
+ users = result.or_default
520
674
 
521
- # The maximum number of circuit runs that will be stored
522
- storage.max_sample_size = 100
675
+ # Users will be an empty array if the result is an error
676
+ users = result.or_default([])
523
677
 
524
- # The maximum number of seconds that a circuit run will be stored
525
- storage.sample_ttl = 1800
678
+ # Users will be the return value of the block
679
+ users = result.or_default do
680
+ # ...
681
+ end
682
+ ```
526
683
 
527
- # The maximum number of seconds to store a circuit. Does not apply to
528
- # locks, which are indefinite.
529
- storage.circuit_ttl = 604_800 # 1 Week
684
+ As we showed in the [Basic Usage](#basic-usage) section, you can put this
685
+ together in a nice one-liner.
530
686
 
531
- # The number of seconds between circuit expirations. Changing this setting
532
- # is not recommended. See API docs for more implementation details.
533
- storage.list_granularity = 3600
687
+ ```ruby
688
+ Faulty.circuit('api').try_run { api.users }.or_default([])
689
+ ```
534
690
 
535
- # If true, disables warnings about recommended client settings like timeouts
536
- storage.disable_warnings = false
537
- end
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)
538
703
  end
539
704
  ```
540
705
 
541
- ### FallbackChain
706
+ Or, if you'd instead like to specify errors to be excluded:
542
707
 
543
- The `Faulty::Storage::FallbackChain` backend is a wrapper for multiple
544
- prioritized storage backends. If the first backend in the chain fails,
545
- consecutive backends are tried until one succeeds. The recommended use-case for
546
- this is to fall back on reliable storage if a networked storage backend fails.
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
+ ```
547
714
 
548
- For example, you may configure Redis as your primary storage backend, with an
549
- in-memory storage backend as a fallback:
715
+ Both options can even be specified together.
550
716
 
551
717
  ```ruby
552
- Faulty.init do |config|
553
- config.storage = Faulty::Storage::FallbackChain.new([
554
- Faulty::Storage::Redis.new,
555
- Faulty::Storage::Memory.new
556
- ])
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!
557
727
  end
558
728
  ```
559
729
 
560
- Faulty instances will automatically use a fallback chain if an array is given to
561
- the `storage` option, so this example is equivalent to the above:
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.
562
735
 
563
736
  ```ruby
564
- Faulty.init do |config|
565
- config.storage = [
566
- Faulty::Storage::Redis.new,
567
- Faulty::Storage::Memory.new
568
- ]
737
+ Faulty.circuit('api').run(cache: 'all_users') do
738
+ api.users
569
739
  end
570
740
  ```
571
741
 
572
- If the fallback chain fails-over to backup storage, circuit states will not
573
- carry over, so failover could be temporarily disruptive to your application.
574
- However, any calls to `#lock` or `#unlock` will always be persisted to all
575
- backends so that locks are maintained during failover.
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.
576
747
 
577
- ### Storage::FaultTolerantProxy
748
+ See the [Caching](#caching) section for more details on Faulty's caching
749
+ strategy.
578
750
 
579
- This wrapper is applied to all non-fault-tolerant storage backends by default
580
- (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/AutoWire)).
751
+ ### Configuring the Circuit Threshold
581
752
 
582
- `Faulty::Storage::FaultTolerantProxy` is a wrapper that suppresses storage
583
- errors and returns sensible defaults during failures. If a storage backend is
584
- failing, all circuits will be treated as closed regardless of locks or previous
585
- history.
753
+ To configure how a circuit responds to error, use the `cool_down`,
754
+ `rate_threshold` and `sample_threshold` options.
586
755
 
587
- If you wish your application to use a secondary storage backend instead of
588
- failing closed, use `FallbackChain`.
756
+ #### Rate Threshold
589
757
 
590
- ### Storage::CircuitProxy
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.
591
760
 
592
- This wrapper is applied to all non-fault-tolerant storage backends by default
593
- (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/AutoWire)).
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
+ ```
594
765
 
595
- `Faulty::Storage::CircuitProxy` is a wrapper that uses an independent in-memory
596
- circuit to track failures to storage backends. If a storage backend fails
597
- continuously, it will be temporarily disabled and raise `Faulty::CircuitError`s.
766
+ #### Sample Threshold
598
767
 
599
- Typically this is used inside a `FaultTolerantProxy` or `FallbackChain` so that
600
- these storage failures are handled gracefully.
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.
601
771
 
602
- ## Configuring the Cache Backend
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
603
780
 
604
- ### Null
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).
605
785
 
606
- The `Faulty::Cache::Null` cache disables caching. It is the default if Rails and
607
- ActiveSupport are not present.
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
+ ```
608
790
 
609
- ### Rails
791
+ ### Circuit Options
610
792
 
611
- `Faulty::Cache::Rails` is the default cache if Rails or ActiveSupport are
612
- present. If Rails is present, it uses `Rails.cache` as the backend. If
613
- ActiveSupport is present, but Rails is not, it creates a new
614
- `ActiveSupport::Cache::MemoryStore` by default. This backend can be used with
615
- any `ActiveSupport::Cache`.
793
+ A circuit can be created with the following configuration options. Those options
794
+ are only set once, synchronized across threads, and will persist in-memory until
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.
616
798
 
617
799
  ```ruby
618
- Faulty.init do |config|
619
- config.cache = Faulty::Cache::Rails.new(
620
- ActiveSupport::Cache::RedisCacheStore.new
621
- )
622
- end
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
623
805
  ```
624
806
 
625
- ### Cache::FaultTolerantProxy
807
+ This is because the circuit objects themselves are internally memoized, and are
808
+ read-only once created.
626
809
 
627
- This wrapper is applied to all non-fault-tolerant cache backends by default
628
- (see the API docs for `Faulty::Cache::AutoWire`).
810
+ The following example represents the defaults for a new circuit:
629
811
 
630
- `Faulty::Cache::FaultTolerantProxy` is a wrapper that suppresses cache errors
631
- and acts like a null cache during failures. Reads always return `nil`, and
632
- writes are no-ops.
812
+ ```ruby
813
+ Faulty.circuit('api') do |config|
814
+ # The cache backend for this circuit. Inherits the global cache by default.
815
+ config.cache = Faulty.options.cache
633
816
 
634
- ### Cache::CircuitProxy
817
+ # The number of seconds before a cache entry is expired. After this time, the
818
+ # cache entry may be fully deleted. If set to nil, the cache will not expire.
819
+ config.cache_expires_in = 86400
635
820
 
636
- This wrapper is applied to all non-fault-tolerant circuit backends by default
637
- (see the API docs for `Faulty::Circuit::AutoWire`).
821
+ # The number of seconds before a cache entry should be refreshed. See the
822
+ # Caching section for more detail. A value of nil disables cache refreshing.
823
+ config.cache_refreshes_after = 900
638
824
 
639
- `Faulty::Circuit::CircuitProxy` is a wrapper that uses an independent in-memory
640
- circuit to track failures to circuit backends. If a circuit backend fails
641
- continuously, it will be temporarily disabled and raise `Faulty::CircuitError`s.
825
+ # The number of seconds to add or subtract from cache_refreshes_after
826
+ # when determining whether a cache entry should be refreshed. Helps mitigate
827
+ # the "thundering herd" effect
828
+ config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
642
829
 
643
- Typically this is used inside a `FaultTolerantProxy` so that these cache
644
- failures are handled gracefully.
830
+ # After a circuit is opened, the number of seconds to wait before moving the
831
+ # circuit to half-open.
832
+ config.cool_down = 300
645
833
 
646
- ## Listing Circuits
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
+
838
+ # The errors that will be captured by Faulty and used to trigger circuit
839
+ # state changes.
840
+ config.errors = [StandardError]
841
+
842
+ # Errors that should be ignored by Faulty and not captured.
843
+ config.exclude = []
844
+
845
+ # The event notifier. Inherits the Faulty instance notifier by default
846
+ config.notifier = Faulty.options.notifier
847
+
848
+ # The minimum failure rate required to trip a circuit
849
+ config.rate_threshold = 0.5
850
+
851
+ # The minimum number of runs required before a circuit can trip
852
+ config.sample_threshold = 3
853
+
854
+ # The storage backend for this circuit. Inherits the Faulty instance storage
855
+ # by default
856
+ config.storage = Faulty.options.storage
857
+ end
858
+ ```
859
+
860
+ Following the same convention as `Faulty.init`, circuits can also be created
861
+ with an options hash:
862
+
863
+ ```ruby
864
+ Faulty.circuit(:api, cache_expires_in: 1800)
865
+ ```
866
+
867
+ ### Listing Circuits
647
868
 
648
869
  For monitoring or debugging, you may need to retrieve a list of all circuit
649
- names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
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)
650
872
  if you're using an instance).
651
873
 
652
874
  You can get a list of all circuit statuses by mapping those names to their
@@ -659,7 +881,7 @@ statuses = Faulty.list_circuits.map do |name|
659
881
  end
660
882
  ```
661
883
 
662
- ## Locking Circuits
884
+ ### Locking Circuits
663
885
 
664
886
  It is possible to lock a circuit open or closed. A circuit that is locked open
665
887
  will never execute its block, and always raise an `Faulty::OpenCircuitError`.
@@ -667,8 +889,12 @@ This is useful in cases where you need to manually disable a dependency
667
889
  entirely. If a cached value is available, that will be returned from the circuit
668
890
  until it expires, even outside its refresh period.
669
891
 
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!)
895
+
670
896
  ```ruby
671
- Faulty.circuit(:broken_api).lock_open!
897
+ Faulty.circuit('broken_api').lock_open!
672
898
  ```
673
899
 
674
900
  A circuit that is locked closed will never trip. This is useful in cases where a
@@ -676,94 +902,205 @@ circuit is continuously tripping incorrectly. If a cached value is available, it
676
902
  will have the same behavior as an unlocked circuit.
677
903
 
678
904
  ```ruby
679
- Faulty.circuit(:false_positive).lock_closed!
905
+ Faulty.circuit('false_positive').lock_closed!
680
906
  ```
681
907
 
682
908
  To remove a lock of either type:
683
909
 
684
910
  ```ruby
685
- Faulty.circuit(:fixed).unlock!
911
+ Faulty.circuit('fixed').unlock!
686
912
  ```
687
913
 
688
914
  Locking or unlocking a circuit has no concurrency guarantees, so it's not
689
915
  recommended to lock or unlock circuits from production code. Instead, locks are
690
916
  intended as an emergency tool for troubleshooting and debugging.
691
917
 
692
- ## Multiple Configurations
918
+ ## Event Handling
693
919
 
694
- It is possible to have multiple configurations of Faulty running within the same
695
- process. The most common setup is to simply use `Faulty.init` to
696
- configure Faulty globally, however it is possible to have additional
697
- configurations.
920
+ Faulty uses an event-dispatching model to deliver notifications of internal
921
+ events. The full list of events is available from
922
+ [`Faulty::Events::EVENTS`](https://www.rubydoc.info/gems/faulty/Faulty/Events).
698
923
 
699
- ### The default instance
924
+ - `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
925
+ - `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
926
+ - `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
927
+ - `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
928
+ - `circuit_closed` - A circuit closed. Payload: `circuit`
929
+ - `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
930
+ `status`, `error`
931
+ - `circuit_opened` - A circuit execution caused the circuit to open. Payload
932
+ `circuit`, `error`
933
+ - `circuit_reopened` - A circuit execution cause the circuit to reopen from
934
+ half-open. Payload: `circuit`, `error`.
935
+ - `circuit_skipped` - A circuit execution was skipped because the circuit is
936
+ closed. Payload: `circuit`
937
+ - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
938
+ `status`
939
+ - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
940
+ be nil), `action`, `error`
700
941
 
701
- When you call `Faulty.init`, you are actually creating the default instance of
702
- `Faulty`. You can access this instance directly by calling `Faulty.default`.
942
+ By default events are logged using `Faulty::Events::LogListener`, but that can
943
+ be replaced, or additional listeners can be added.
703
944
 
704
- ```ruby
705
- # We create the default instance
706
- Faulty.init
945
+ ### CallbackListener
707
946
 
708
- # Access the default instance
709
- faulty = Faulty.default
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.
710
950
 
711
- # Alternatively, access the instance by name
712
- faulty = Faulty[:default]
951
+ ```ruby
952
+ Faulty.init do |config|
953
+ # Replace the default listener with a custom callback listener
954
+ listener = Faulty::Events::CallbackListener.new do |events|
955
+ events.circuit_opened do |payload|
956
+ MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
957
+ end
958
+ end
959
+ config.listeners = [listener]
960
+ end
713
961
  ```
714
962
 
715
- You can rename the default instance if desired:
963
+ ### Other Built-in Listeners
716
964
 
717
- ```ruby
718
- Faulty.init(:custom_default)
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.
719
968
 
720
- instance = Faulty.default
721
- instance = Faulty[:custom_default]
722
- ```
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.
723
974
 
724
- ### Multiple Instances
975
+ If your favorite monitoring software is not supported here, please open a PR
976
+ that implements a listener for it.
725
977
 
726
- If you want multiple instance, but want global, thread-safe access to
727
- them, you can use `Faulty.register`:
978
+ ### Custom Listeners
979
+
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:
728
983
 
729
984
  ```ruby
730
- api_faulty = Faulty.new do |config|
731
- # This accepts the same options as Faulty.init
985
+ class MyFaultyListener
986
+ def handle(event, payload)
987
+ MyNotifier.alert(event, payload)
988
+ end
732
989
  end
733
-
734
- Faulty.register(:api, api_faulty)
735
-
736
- # Now access the instance globally
737
- Faulty[:api]
738
990
  ```
739
991
 
740
- When you call `Faulty.circuit`, that's the same as calling
741
- `Faulty.default.circuit`, so you can apply the same principal to any other
742
- registered Faulty instance:
743
-
744
992
  ```ruby
745
- Faulty[:api].circuit(:api_circuit).run { 'ok' }
993
+ Faulty.init do |config|
994
+ config.listeners = [MyFaultyListener.new]
995
+ end
746
996
  ```
747
997
 
748
- ### Standalone Instances
998
+ ## How it Works
749
999
 
750
- If you choose, you can use Faulty instances without registering them globally.
751
- This is more object-oriented and is necessary if you use dependency injection.
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:
1004
+
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
1010
+
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.
1015
+
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.
1019
+
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.
1024
+
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.
1028
+
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.
1032
+
1033
+ ### Caching
1034
+
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.
1039
+
1040
+ Once your cache is configured, you can use the `cache` parameter when running
1041
+ a circuit to specify a cache key:
752
1042
 
753
1043
  ```ruby
754
- faulty = Faulty.new
755
- faulty.circuit(:standalone_circuit)
1044
+ feed = Faulty.circuit('rss_feeds')
1045
+ .try_run(cache: "rss_feeds/#{feed}") do
1046
+ fetch_feed(feed)
1047
+ end.or_default([])
756
1048
  ```
757
1049
 
758
- Calling `#circuit` on the instance still has the same memoization behavior that
759
- `Faulty.circuit` has, so subsequent calls to the same circuit will return a
760
- memoized circuit object.
1050
+ By default a circuit has the following options:
1051
+
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.
1063
+
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.
1067
+
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.
1072
+
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.
1075
+
1076
+ If the circuit is open and the cache is hit, then Faulty will always return the
1077
+ cached value.
1078
+
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.
1083
+
1084
+ ### Fault Tolerance
1085
+
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.
1089
+
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.
1095
+
1096
+ If the storage backend fails, circuits will default to closed. If the cache
1097
+ backend fails, all cache queries will miss.
761
1098
 
762
1099
  ## Implementing a Cache Backend
763
1100
 
764
1101
  You can implement your own cache backend by following the documentation in
765
- `Faulty::Cache::Interface`. It is a fairly simple API, with only get/set
766
- 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:
767
1104
 
768
1105
  ```ruby
769
1106
  class MyFaultyCache
@@ -792,10 +1129,11 @@ users.
792
1129
  ## Implementing a Storage Backend
793
1130
 
794
1131
  You can implement your own storage backend by following the documentation in
795
- `Faulty::Storage::Interface`. Since the storage has some tricky requirements
796
- regarding concurrency, the `Faulty::Storage::Memory` can be used as a reference
797
- implementation. Feel free to open a pull request if your storage backend
798
- 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.
799
1137
 
800
1138
  ## Alternatives
801
1139
 
@@ -806,7 +1144,7 @@ but there are and have been many other options:
806
1144
 
807
1145
  - [semian](https://github.com/Shopify/semian): A resiliency toolkit that
808
1146
  includes circuit breakers. It uses adapters to auto-wire circuits, and it has
809
- only host-local storage by design.
1147
+ only in-memory storage by design.
810
1148
  - [circuitbox](https://github.com/yammer/circuitbox): Similar in design to
811
1149
  Faulty, but with a different API. It uses Moneta to abstract circuit storage
812
1150
  to allow any key-value store.