faulty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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