faulty 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67b20b18497c36c64f2a3dc7d16de761af0c22c0a38d93f3101534a5ac85e26d
4
+ data.tar.gz: 3d880b9a143939ab8ef4a22393c4bb8fa2b724d0bb02986bf52ac06ac3e5cf0b
5
+ SHA512:
6
+ metadata.gz: 379c17805a9c647d71fac74e5190b6c077ef55c6d42b797f68dff008e602b005053a86c0c41572c761f32df25436b918e68f61049adf613e3a61a1a779d0364d
7
+ data.tar.gz: a7ff7c19ec6759282ec0f17ffe72f2a56aa5a41d83221ca53040f676951f8346fa2fa83fbe20c71a4ef843be9d2dfbbe0d23380d94342173e8bf41632d74ad12
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /vendor/
4
+ /.ruby-version
5
+
6
+ /coverage/
7
+ /doc/
8
+ /.yardoc/
9
+
10
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,85 @@
1
+ ---
2
+ require:
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.3
7
+
8
+ Layout/ArgumentAlignment:
9
+ EnforcedStyle: with_fixed_indentation
10
+
11
+ Layout/ParameterAlignment:
12
+ EnforcedStyle: with_fixed_indentation
13
+
14
+ Layout/EndAlignment:
15
+ EnforcedStyleAlignWith: start_of_line
16
+
17
+ Layout/FirstArgumentIndentation:
18
+ EnforcedStyle: consistent
19
+
20
+ Layout/FirstArrayElementIndentation:
21
+ EnforcedStyle: consistent
22
+
23
+ Layout/FirstHashElementIndentation:
24
+ EnforcedStyle: consistent
25
+
26
+ Layout/LineLength:
27
+ Max: 120
28
+
29
+ Layout/MultilineMethodCallIndentation:
30
+ EnforcedStyle: indented
31
+
32
+ Lint/RaiseException:
33
+ Enabled: true
34
+
35
+ Lint/StructNewOverride:
36
+ Enabled: true
37
+
38
+ RSpec/ExampleLength:
39
+ Enabled: false
40
+
41
+ RSpec/FilePath:
42
+ Enabled: false
43
+
44
+ RSpec/NamedSubject:
45
+ Enabled: false
46
+
47
+ RSpec/MultipleExpectations:
48
+ Enabled: false
49
+
50
+ RSpec/SubjectStub:
51
+ Enabled: false
52
+
53
+ Metrics/AbcSize:
54
+ Max: 35
55
+
56
+ Metrics/BlockLength:
57
+ Enabled: false
58
+
59
+ Metrics/MethodLength:
60
+ Max: 30
61
+
62
+ Style/Documentation:
63
+ Enabled: false
64
+
65
+ Style/EmptyMethod:
66
+ EnforcedStyle: expanded
67
+
68
+ Style/FrozenStringLiteralComment:
69
+ Enabled: true
70
+ EnforcedStyle: always
71
+
72
+ Style/GuardClause:
73
+ Enabled: false
74
+
75
+ Style/HashEachMethods:
76
+ Enabled: true
77
+
78
+ Style/HashTransformKeys:
79
+ Enabled: false
80
+
81
+ Style/HashTransformValues:
82
+ Enabled: false
83
+
84
+ Style/IfUnlessModifier:
85
+ Enabled: false
@@ -0,0 +1,44 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 2.3
5
+ - 2.4
6
+ - 2.5
7
+ - 2.6
8
+ - 2.7
9
+
10
+ env:
11
+ global:
12
+ - COVERAGE=1
13
+ # Encrypted code climate CC_TEST_REPORTER_ID=
14
+ - secure: 'a/qmESTnDq37KvBY4u1VcBEjd7Y3b16L28AiV0y3d7SbrGeuWPmTR7WMvD8b0CV1Ou5wpic9HAxC9u2ZYhy0/BN9I4AOnkyj4IXF9K+ETbTq2XOAix38hNF4a/Hl89mgKD3IPtjxVrQ83h7pxbqtQ2jPnLyiR7mTj0rfOmzTSaItcn/auONuvanHuitGjYTw3xUA1okbhMy8cVh3nbozP0m3eujSbSOpLD3N9+s+A8lE+6VbNvoygqoqdcQqlYH4q5x+SGaD7L1illIywxVWpExygB9jIum3mhaXe7ThJ/FkUfCHqINRejR63QDVwvYGRPnrlg0c8rNiLDjfVyY83ESSqXN1hf/vzyVtGruj4t/35VsFL/fyE/iNGDDmMu/GH5Gbsa9+lykeRSOL97VAlzAuVeb4AwvZ9HFV5QcbGnNkrVG8sTMIOcUzJVGX2C38fnElGxGwrbpV8+ReMZtcQZSg2isRRKDUFRBdFFUrHKK8uqII+a5oZDi5OK7ytZHg8XTbrpJXStYhQjRMfbHsd2YFGeiyLWqzrASd/bpgdyLtZuQ1/r7z1IVGJjb01nNhD4mchU3Lj4mxaQe2zvBEhbUZufz2zrIDPiKxsQvYcqw4nvHJE7zWM0cMgHvQrzcZH44sHVtdVavjSPoOzN6665zaA9sCcYffESCPPQTnopE='
15
+
16
+ services:
17
+ - redis-server
18
+
19
+ before_script:
20
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
21
+ - chmod +x ./cc-test-reporter
22
+ - ./cc-test-reporter before-build
23
+
24
+ script:
25
+ - bin/rubocop
26
+ - bin/rspec --format doc
27
+ - bin/check-version
28
+
29
+ after_script:
30
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
31
+
32
+ jobs:
33
+ include:
34
+ - stage: release
35
+ rvm: 2.7
36
+ script: skip
37
+ deploy:
38
+ provider: rubygems
39
+ api_key:
40
+ secure: 'eS6u+XyT6hHk/0gLXWzoe3RTJzEVFQHcJ+MNGSp7iq+cavJHisndcYBmUxi6/Ttb/aT/YoczhIWYo6WPbcjDqWszDfUsrQyaiOiZy4BBcMwN1WHeHkeRHfO2NX7us0AKO66TAiNfpMqUR2UT/c1LCPtLb+bkG6IFWxRuF5Fo32e0th6Z+hJ4Se2E/Qg1lrZk5zBlhQSOtU88vWQAkT9FdzpwBskVENBuDmH5YTV2Y28QYHsDtSNIxoDiUK9LOoPxaYUQp5ZZ58HZHtPdNydPHvtOXaWcBlakH5HNkh3FUHsxqbA1b3U412PC4TK+0jnxfISH4EOAMZEkL1nmroJORlb5nlwG1eiHpPVbOke1z2cZarGmWWAEf9bGE99GVbuxUxRrm9i1rmPdJY2G6Z0Kz8zoLTDYI/9l2P81/99a7h84rWACeeI3bcdvqViLxUuVMiwQjQsOZhzfq1M6jjETAWAI3AMRLxaGSgp0LV3WtaSWk1T5qvOvOF2HIfQ1EMd74kblJrVGWUiqd94/UgQoB/+lg9PdP/h4aHY5Cq1ec83wzaO6leKNlO+EQfyAVD1nd8kxo7YHEUpcB8wWCNFA5iaLqwIMt7aeWvG/BVUIH0apppzJGDy9UZ0gGsYi6ID3gbRRgmHIdJospr6TN7hksu1ZqkwEaprpXq8zH0KFYW0='
41
+ gem: faulty
42
+ on:
43
+ tags: true
44
+ repo: ParentSquare/faulty
@@ -0,0 +1,3 @@
1
+ --markup markdown
2
+ --markup-provider redcarpet
3
+ - guides/*
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2020 ParentSquare
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the “Software”), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,559 @@
1
+ # Faulty
2
+
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)
7
+ [![Inline docs](http://inch-ci.org/github/ParentSquare/faulty.svg?branch=master)](http://inch-ci.org/github/ParentSquare/faulty)
8
+
9
+ Fault-tolerance tools for ruby based on [circuit-breakers][martin fowler].
10
+
11
+ ```ruby
12
+ users = Faulty.circuit(:api).try_run do
13
+ api.users
14
+ end.or_default([])
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ Add it to your `Gemfile`:
20
+
21
+ ```ruby
22
+ gem 'faulty'
23
+ ```
24
+
25
+ Or install it manually:
26
+
27
+ ```sh
28
+ gem install faulty
29
+ ```
30
+
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.
33
+
34
+ ## API Docs
35
+
36
+ API docs can be read [on rubydoc.info][api docs], inline in the source code, or
37
+ you can generate them yourself with Ruby `yard`:
38
+
39
+ ```sh
40
+ bin/yardoc
41
+ ```
42
+
43
+ Then open `doc/index.html` in your browser.
44
+
45
+ ## Setup
46
+
47
+ Use the default configuration options:
48
+
49
+ ```ruby
50
+ Faulty.init
51
+ ```
52
+
53
+ Or specify your own configuration:
54
+
55
+ ```ruby
56
+ Faulty.init do |config|
57
+ config.storage = Faulty::Storage::Redis.new
58
+
59
+ config.listeners << Faulty::Events::CallbackListener.new do |events|
60
+ events.circuit_open do |payload|
61
+ puts 'Circuit was opened'
62
+ end
63
+ end
64
+ end
65
+ ```
66
+
67
+ For a full list of configuration options, see the
68
+ [Global Configuration](#global-configuration) section.
69
+
70
+ ## Basic Usage
71
+
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.
75
+
76
+ ```ruby
77
+ circuit1 = Faulty.circuit(:api, cache_refreshes_after: 1800)
78
+
79
+ # The options from above are also used when called here
80
+ circuit2 = Faulty.circuit(:api)
81
+ circuit2.options.cache_refreshes_after == 1800 # => true
82
+
83
+ # The same circuit is returned on each consecutive call
84
+ circuit1.equal?(circuit2) # => true
85
+ ```
86
+
87
+ To run a circuit, call the `run` method:
88
+
89
+ ```ruby
90
+ Faulty.circuit(:api).run do
91
+ api.users
92
+ end
93
+ ```
94
+
95
+ See [How it Works](#how-it-works) for more details about how Faulty handles
96
+ circuit failures.
97
+
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:
103
+
104
+ ```ruby
105
+ result = Faulty.circuit(:api).try_run do
106
+ api.users
107
+ end
108
+
109
+ users = if result.ok?
110
+ result.get
111
+ else
112
+ []
113
+ end
114
+ ```
115
+
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:
121
+
122
+ ```ruby
123
+ users = Faulty.circuit(:api).try_run do
124
+ api.users
125
+ end.or_default([])
126
+ ```
127
+
128
+ ## How it Works
129
+
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:
133
+
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
138
+
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.
143
+
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.
147
+
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.
152
+
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.
156
+
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.
160
+
161
+ ## Global Configuration
162
+
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)).
166
+
167
+ ```ruby
168
+ Faulty.init do |config|
169
+ # The cache backend to use. By default, Faulty looks for a Rails cache. If
170
+ # that's not available, it uses an ActiveSupport::Cache::Memory instance.
171
+ # Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
172
+ config.cache = Faulty::Cache::Default.new
173
+
174
+ # The storage backend. By default, Faulty uses an in-memory store. For most
175
+ # production applications, you'll want a more robust backend. Faulty also
176
+ # provides Faulty::Storage::Redis for this.
177
+ config.storage = Faulty::Storage::Memory.new
178
+
179
+ # An array of event listeners. Each object in the array should implement
180
+ # Faulty::Events::ListenerInterface. For ad-hoc custom listeners, Faulty
181
+ # provides Faulty::Events::CallbackListener.
182
+ config.listeners = [Faulty::Events::LogListener.new]
183
+
184
+ # The event notifier. For most use-cases, you don't need to change this,
185
+ # However, Faulty allows substituting your own notifier if necessary.
186
+ # If overridden, config.listeners will be ignored.
187
+ config.notifier = Faulty::Events::Notifier.new(config.listeners)
188
+ end
189
+ ```
190
+
191
+ For all Faulty APIs that have configuration, you can also pass in an options
192
+ hash. For example, `Faulty.init` could be called like this:
193
+
194
+ ```ruby
195
+ Faulty.init(cache: Faulty::Cache::Null.new)
196
+ ```
197
+
198
+ ## Circuit Options
199
+
200
+ A circuit can be created with the following configuration options. Those options
201
+ 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.
205
+
206
+ This is because the circuit objects themselves are internally memoized, and are
207
+ read-only once created.
208
+
209
+ The following example represents the defaults for a new circuit:
210
+
211
+ ```ruby
212
+ Faulty.circuit(:api) do |config|
213
+ # The cache backend for this circuit. Inherits the global cache by default.
214
+ config.cache = Faulty.options.cache
215
+
216
+ # The number of seconds before a cache entry is expired. After this time, the
217
+ # cache entry may be fully deleted. If set to nil, the cache will not expire.
218
+ config.cache_expires_in = 86400
219
+
220
+ # The number of seconds before a cache entry should be refreshed. See the
221
+ # Caching section for more detail. A value of nil disables cache refreshing.
222
+ config.cache_refreshes_after = 900
223
+
224
+ # The number of seconds to add or subtract from cache_refreshes_after
225
+ # when determining whether a cache entry should be refreshed. Helps mitigate
226
+ # the "thundering herd" effect
227
+ config.cache_refresh_jitter = 0.2 * config.cache_refreshes_after
228
+
229
+ # After a circuit is opened, the number of seconds to wait before moving the
230
+ # circuit to half-open.
231
+ config.cool_down = 300
232
+
233
+ # The errors that will be captured by Faulty and used to trigger circuit
234
+ # state changes.
235
+ config.errors = [StandardError]
236
+
237
+ # Errors that should be ignored by Faulty and not captured.
238
+ config.exclude = []
239
+
240
+ # The event notifier. Inherits the global notifier by default
241
+ config.notifier = Faulty.options.notifier
242
+
243
+ # The minimum failure rate required to trip a circuit
244
+ config.rate_threshold = 0.5
245
+
246
+ # The minimum number of runs required before a circuit can trip
247
+ config.sample_threshold = 3
248
+
249
+ # The storage backend for this circuit. Inherits the global storage by default
250
+ config.storage = Faulty.options.storage
251
+ end
252
+ ```
253
+
254
+ Following the same convention as `Faulty.init`, circuits can also be created
255
+ with an options hash:
256
+
257
+ ```ruby
258
+ Faulty.circuit(:api, cache_expires_in: 1800)
259
+ ```
260
+
261
+ ## Caching
262
+
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.
267
+
268
+ Once your cache is configured, you can use the `cache` parameter when running
269
+ a circuit to specify a cache key:
270
+
271
+ ```ruby
272
+ feed = Faulty.circuit(:rss_feeds)
273
+ .try_run(cache: "rss_feeds/#{feed}") do
274
+ fetch_feed(feed)
275
+ end.or_default([])
276
+ ```
277
+
278
+ By default a circuit has the following options:
279
+
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.
295
+
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.
300
+
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.
303
+
304
+ If the circuit is open and the cache is hit, then Faulty will always return the
305
+ cached value.
306
+
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.
311
+
312
+ ## Fault Tolerance
313
+
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.
317
+
318
+ If the storage backend fails, all circuits will default to open. If the cache
319
+ backend fails, all cache queries will miss.
320
+
321
+ ## Event Handling
322
+
323
+ Faulty uses an event-dispatching model to deliver notifications of internal
324
+ events. The full list of events is available from `Faulty::Events::EVENTS`.
325
+
326
+ - `cache_failure` - A cache backend raised an error. Payload: `key`, `action`, `error`
327
+ - `circuit_cache_hit` - A circuit hit the cache. Payload: `circuit`, `key`
328
+ - `circuit_cache_miss` - A circuit hit the cache. Payload: `circuit`, `key`
329
+ - `circuit_cache_write` - A circuit wrote to the cache. Payload: `circuit`, `key`
330
+ - `circuit_closed` - A circuit closed. Payload: `circuit`
331
+ - `circuit_failure` - A circuit execution raised an error. Payload: `circuit`,
332
+ `status`, `error`
333
+ - `circuit_opened` - A circuit execution caused the circuit to open. Payload
334
+ `circuit`, `error`
335
+ - `circuit_reopened` - A circuit execution cause the circuit to reopen from
336
+ half-open. Payload: `circuit`, `error`.
337
+ - `circuit_skipped` - A circuit execution was skipped because the circuit is
338
+ closed. Payload: `circuit`
339
+ - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
340
+ `status`
341
+ - `storage_failure` - A storage backend raised an error. Payload `circuit`,
342
+ `action`, `error`
343
+
344
+ By default events are logged using `Faulty::Events::LogListener`, but that can
345
+ be replaced, or additional listeners can be added.
346
+
347
+ ```ruby
348
+ Faulty.init do |config|
349
+ # Replace the default listener with a custom callback listener
350
+ listener = Faulty::Events::CallbackListener.new do |events|
351
+ events.circuit_opened do |payload|
352
+ MyNotifier.alert("Circuit #{payload[:circuit].name} opened: #{payload[:error].message}")
353
+ end
354
+ end
355
+ config.listeners = [listener]
356
+ end
357
+ ```
358
+
359
+ You can implement your own listener by following the documentation in
360
+ `Faulty::Events::ListenerInterface`. For example:
361
+
362
+ ```ruby
363
+ class MyFaultyListener
364
+ def handle(event, payload)
365
+ MyNotifier.alert(event, payload)
366
+ end
367
+ end
368
+ ```
369
+
370
+ ```ruby
371
+ Faulty.init do |config|
372
+ config.listeners = [MyFaultyListener.new]
373
+ end
374
+ ```
375
+
376
+ ## Configuring the Storage Backend
377
+
378
+ ### Memory
379
+
380
+ The `Faulty::Cache::Memory` backend is the default storage backend. The default
381
+ configuration:
382
+
383
+ ```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
388
+ end
389
+ end
390
+ ```
391
+
392
+ ### Redis
393
+
394
+ The `Faulty::Cache::Redis` backend provides distributed circuit storage using
395
+ Redis. The default configuration:
396
+
397
+ ```ruby
398
+ 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
416
+ end
417
+ ```
418
+
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)).
424
+
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.
428
+
429
+ ```ruby
430
+ statuses = Faulty.list_circuits.map do |name|
431
+ Faulty.circuit(name).status
432
+ end
433
+ ```
434
+
435
+ ## Scopes
436
+
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.
441
+
442
+ ### The default scope
443
+
444
+ When you call `Faulty.init`, you are actually creating the default scope. You
445
+ can access this scope directly by calling `Faulty.default`.
446
+
447
+ ```ruby
448
+ # We create the default scope
449
+ Faulty.init
450
+
451
+ # Access the default scope
452
+ scope = Faulty.default
453
+
454
+ # Alternatively, access the scope by name
455
+ scope = Faulty[:default]
456
+ ```
457
+
458
+ You can rename the default scope if desired:
459
+
460
+ ```ruby
461
+ Faulty.init(:custom_default)
462
+
463
+ scope = Faulty.default
464
+ scope = Faulty[:custom_default]
465
+ ```
466
+
467
+ ### Multiple Scopes
468
+
469
+ If you want multiple scopes, but want global, thread-safe access to
470
+ them, you can use `Faulty.register`:
471
+
472
+ ```ruby
473
+ api_scope = Faulty::Scope.new do |config|
474
+ # This accepts the same options as Faulty.init
475
+ end
476
+
477
+ Faulty.register(:api, api_scope)
478
+
479
+ # Now access the scope globally
480
+ Faulty[:api]
481
+ ```
482
+
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:
486
+
487
+ ```ruby
488
+ Faulty[:api].circuit(:api_circuit).run { 'ok' }
489
+ ```
490
+
491
+ ### Standalone Scopes
492
+
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.
495
+
496
+ ```ruby
497
+ faulty = Faulty::Scope.new
498
+ faulty.circuit(:standalone_circuit)
499
+ ```
500
+
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.
504
+
505
+ ## Implementing a Cache Backend
506
+
507
+ 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:
510
+
511
+ ```ruby
512
+ class MyFaultyCache
513
+ def initialize(my_cache)
514
+ @cache = my_cache
515
+ end
516
+
517
+ def read(key)
518
+ @cache.read(key)
519
+ end
520
+
521
+ def write(key, value, expires_in: nil)
522
+ @cache.write(key, value, expires_in)
523
+ end
524
+
525
+ # Set this to false unless your cache never raises errors
526
+ def fault_tolerant?
527
+ false
528
+ end
529
+ end
530
+ ```
531
+
532
+ Feel free to open a pull request if your cache backend would be useful for other
533
+ users.
534
+
535
+ ## Implementing a Storage Backend
536
+
537
+ 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.
542
+
543
+ ## Alternatives
544
+
545
+ Faulty has its own opinions about how to implement a circuit breaker in Ruby,
546
+ but there are and have been many other options:
547
+
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)
551
+ - [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
552
+ - [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
553
+ (unmaintained)
554
+ - [breaker](https://github.com/ahawkins/breaker) (unmaintained)
555
+ - [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
556
+
557
+ [api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
558
+ [martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
559
+ [hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works