faulty 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/.travis.yml +4 -2
  4. data/CHANGELOG.md +37 -1
  5. data/Gemfile +17 -0
  6. data/README.md +333 -55
  7. data/bin/check-version +5 -1
  8. data/bin/console +1 -1
  9. data/faulty.gemspec +3 -10
  10. data/lib/faulty.rb +149 -43
  11. data/lib/faulty/cache.rb +3 -1
  12. data/lib/faulty/cache/auto_wire.rb +65 -0
  13. data/lib/faulty/cache/circuit_proxy.rb +61 -0
  14. data/lib/faulty/cache/default.rb +10 -21
  15. data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
  16. data/lib/faulty/cache/interface.rb +1 -1
  17. data/lib/faulty/cache/mock.rb +1 -1
  18. data/lib/faulty/cache/null.rb +1 -1
  19. data/lib/faulty/cache/rails.rb +9 -10
  20. data/lib/faulty/circuit.rb +10 -5
  21. data/lib/faulty/error.rb +18 -4
  22. data/lib/faulty/events.rb +3 -2
  23. data/lib/faulty/events/callback_listener.rb +1 -1
  24. data/lib/faulty/events/honeybadger_listener.rb +53 -0
  25. data/lib/faulty/events/listener_interface.rb +1 -1
  26. data/lib/faulty/events/log_listener.rb +1 -1
  27. data/lib/faulty/events/notifier.rb +11 -2
  28. data/lib/faulty/immutable_options.rb +1 -1
  29. data/lib/faulty/result.rb +2 -2
  30. data/lib/faulty/status.rb +1 -1
  31. data/lib/faulty/storage.rb +4 -1
  32. data/lib/faulty/storage/auto_wire.rb +122 -0
  33. data/lib/faulty/storage/circuit_proxy.rb +64 -0
  34. data/lib/faulty/storage/fallback_chain.rb +207 -0
  35. data/lib/faulty/storage/fault_tolerant_proxy.rb +55 -60
  36. data/lib/faulty/storage/interface.rb +1 -1
  37. data/lib/faulty/storage/memory.rb +8 -4
  38. data/lib/faulty/storage/redis.rb +75 -13
  39. data/lib/faulty/version.rb +2 -2
  40. metadata +13 -118
  41. data/lib/faulty/scope.rb +0 -117
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2d947943e6b9d5bf8481bc3b37f417c14f6ee37f7ccc359b37eb83c5d1da63f
4
- data.tar.gz: 2ba752577fbf1a9e9ddf4b9754a1382d603435efb463158b55f5ac1eabe1b75e
3
+ metadata.gz: c0712dd797dfe2922e4e52a792f48c5ac72c4b9766d218dfa597e7ad630f9fd0
4
+ data.tar.gz: fb4a787507006131f34a301fc164466fd8befb72051550ca0a63fc2d69ec949c
5
5
  SHA512:
6
- metadata.gz: 3d23f2e4b3aee70fceca72c2cc73f712a01ee6b86fdcc8d6fb68433573e224724d9f181300695bba7780265d1a294be1b991c4bff5ee24f6eb152d6650f834f9
7
- data.tar.gz: 589ac68d52e00f82b7f7b562ae64f140bb7accc86b34247324e95b8e4c67ba9b79f40d71b6929398ab75738d9ea27c1a61a1162cde8003cc54dd0ade2171c79d
6
+ metadata.gz: 5381dd13f058045906330c67ff80aeefac55ae81077eb4552d5461d743344fa097e205b8ac3a39b385c52976e64315aec4e903c1a0c298a05452fab7788d1df2
7
+ data.tar.gz: af31cbf86cac54226189f8f63b1d8b0bcf82f9023085e98d07e4e9db65e402883c2219dbf255e21427c40b0c0a66ec71bd4dc9dfdd94fb9bc7b574ddf8426674
@@ -29,6 +29,9 @@ Layout/LineLength:
29
29
  Layout/MultilineMethodCallIndentation:
30
30
  EnforcedStyle: indented
31
31
 
32
+ Layout/RescueEnsureAlignment:
33
+ Enabled: false
34
+
32
35
  Lint/RaiseException:
33
36
  Enabled: true
34
37
 
@@ -44,6 +47,9 @@ RSpec/FilePath:
44
47
  RSpec/NamedSubject:
45
48
  Enabled: false
46
49
 
50
+ RSpec/MessageSpies:
51
+ Enabled: false
52
+
47
53
  RSpec/MultipleExpectations:
48
54
  Enabled: false
49
55
 
@@ -59,6 +65,9 @@ Metrics/BlockLength:
59
65
  Metrics/MethodLength:
60
66
  Max: 30
61
67
 
68
+ Naming/MethodParameterName:
69
+ MinNameLength: 1
70
+
62
71
  Style/Documentation:
63
72
  Enabled: false
64
73
 
@@ -6,6 +6,8 @@ rvm:
6
6
  - 2.5
7
7
  - 2.6
8
8
  - 2.7
9
+ - jruby-9.2.10
10
+ - truffleruby-20.2.0
9
11
 
10
12
  env:
11
13
  global:
@@ -22,8 +24,8 @@ before_script:
22
24
  - ./cc-test-reporter before-build
23
25
 
24
26
  script:
25
- - bin/rubocop
26
- - bin/rspec --format doc
27
+ - bundle exec rubocop
28
+ - bundle exec rspec --format doc
27
29
  - bin/check-version
28
30
 
29
31
  after_script:
@@ -1,6 +1,42 @@
1
+ ## Release v0.3.0
2
+
3
+ * Add tools for backend fault-tolerance #10
4
+ * CircuitProxy for wrapping storage in an internal circuit
5
+ * FallbackChain storage backend for falling back to stable storage
6
+ * Timeout warnings for Redis backend
7
+ * AutoWire wrappers for automatically configuring storage and cache
8
+ * Better documentation for fault-tolerance
9
+
10
+ ## Release v0.2.0
11
+
12
+ * Remove Scopes and replace them with Faulty instances #9
13
+
14
+ ### Breaking Changes
15
+
16
+ * `Faulty::Scope` has been removed. Use `Faulty.new` instead.
17
+ * `Faulty` is now a class, not a module
18
+
19
+ ## Release v0.1.5
20
+
21
+ * Fix redis storage to expire state key when using CAS #8
22
+
23
+ ## Release v0.1.4
24
+
25
+ * Improve spec coverage for supporting classes #6
26
+ * Fix redis bug where concurrent CAS requests could crash #7
27
+
28
+ ## Release v0.1.3
29
+
30
+ * Fix bug where memory storage would delete the newest entries #5
31
+ * Add HoneybadgerListener for error reporting #4
32
+
33
+ ## Release v0.1.2
34
+
35
+ * Fix Storage::FaultTolerantProxy open and reopen methods #2
36
+
1
37
  ## Release v0.1.1
2
38
 
3
- * Fix a crash when Storage::FaultTolerantProxy created a status stub
39
+ * Fix a crash when Storage::FaultTolerantProxy created a status stub #1
4
40
 
5
41
  ## Release v0.1.0
6
42
 
data/Gemfile CHANGED
@@ -3,3 +3,20 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ # We add non-essential gems like debugging tools and CI dependencies
8
+ # here. This also allows us to use conditional dependencies that depend on the
9
+ # platform
10
+
11
+ not_jruby = %i[ruby mingw x64_mingw].freeze
12
+
13
+ gem 'activesupport', '>= 4.2'
14
+ gem 'bundler', '>= 1.17', '< 3'
15
+ gem 'byebug', platforms: not_jruby
16
+ gem 'irb', '~> 1.0'
17
+ gem 'redcarpet', '~> 3.5', platforms: not_jruby
18
+ gem 'rspec_junit_formatter', '~> 0.4'
19
+ # 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'
22
+ gem 'yard', '~> 0.9.25', platforms: not_jruby
data/README.md CHANGED
@@ -67,6 +67,51 @@ end
67
67
  For a full list of configuration options, see the
68
68
  [Global Configuration](#global-configuration) section.
69
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.
92
+
93
+ ```ruby
94
+ Faulty.circuit(:text_api).run do
95
+ TextApi.send(message, timeout: 5)
96
+ end
97
+ ```
98
+
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
+ ```
114
+
70
115
  ## Basic Usage
71
116
 
72
117
  To create a circuit, call `Faulty.circuit`. This can be done as you use the
@@ -127,7 +172,8 @@ end.or_default([])
127
172
 
128
173
  ## How it Works
129
174
 
130
- Faulty implements a version of circuit breakers inspired by
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
131
177
  [Martin Fowler's post][martin fowler] on the subject. A few notable features of
132
178
  Faulty's implementation are:
133
179
 
@@ -135,11 +181,12 @@ Faulty's implementation are:
135
181
  - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
136
182
  cache jitter and error fallback.
137
183
  - Event-based monitoring
184
+ - Flexible fault-tolerant storage with optional fallbacks
138
185
 
139
186
  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.
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.
143
190
 
144
191
  Once both thresholds are breached, the circuit is opened. Once open, the
145
192
  circuit starts the cool-down period. Any executions within that cool-down are
@@ -158,22 +205,30 @@ In addition to the classic circuit breaker design, Faulty implements caching
158
205
  that is integrated with the circuit state. See [Caching](#caching) for more
159
206
  detail.
160
207
 
161
- ## Global Configuration
208
+ ## Configuration
162
209
 
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)).
210
+ Faulty can be configured with the following configuration options. This example
211
+ illustrates the default values. In the first example, we configure Faulty
212
+ globally. The second example shows the same configuration using an instance of
213
+ Faulty instead of global configuration.
166
214
 
167
215
  ```ruby
168
216
  Faulty.init do |config|
169
217
  # The cache backend to use. By default, Faulty looks for a Rails cache. If
170
218
  # that's not available, it uses an ActiveSupport::Cache::Memory instance.
171
219
  # Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
220
+ # Whatever backend is given here is automatically wrapped in
221
+ # Faulty::Cache::AutoWire. This adds fault-tolerance features, see the
222
+ # AutoWire API docs for more details.
172
223
  config.cache = Faulty::Cache::Default.new
173
224
 
174
225
  # The storage backend. By default, Faulty uses an in-memory store. For most
175
226
  # production applications, you'll want a more robust backend. Faulty also
176
227
  # provides Faulty::Storage::Redis for this.
228
+ # Whatever backend is given here is automatically wrapped in
229
+ # Faulty::Storage::AutoWire. This adds fault-tolerance features, see the
230
+ # AutoWire APi docs for more details. If an array of storage backends is
231
+ # given, each one will be tried in order until one succeeds.
177
232
  config.storage = Faulty::Storage::Memory.new
178
233
 
179
234
  # An array of event listeners. Each object in the array should implement
@@ -188,6 +243,25 @@ Faulty.init do |config|
188
243
  end
189
244
  ```
190
245
 
246
+ Here is the same configuration using an instance of `Faulty`. This is a more
247
+ object-oriented approach.
248
+
249
+ ```ruby
250
+ faulty = Faulty.new do |config|
251
+ config.cache = Faulty::Cache::Default.new
252
+ config.storage = Faulty::Storage::Memory.new
253
+ config.listeners = [Faulty::Events::LogListener.new]
254
+ config.notifier = Faulty::Events::Notifier.new(config.listeners)
255
+ end
256
+ ```
257
+
258
+ Most of the examples in this README use the global Faulty class methods, but
259
+ they work the same way when using an instance. Just substitute your instance
260
+ instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever
261
+ configuration mechanism works best for your application. Also see
262
+ [Multiple Configurations](#multiple-configurations) if your application needs
263
+ to set different options in different scenarios.
264
+
191
265
  For all Faulty APIs that have configuration, you can also pass in an options
192
266
  hash. For example, `Faulty.init` could be called like this:
193
267
 
@@ -199,9 +273,9 @@ Faulty.init(cache: Faulty::Cache::Null.new)
199
273
 
200
274
  A circuit can be created with the following configuration options. Those options
201
275
  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.
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.
205
279
 
206
280
  This is because the circuit objects themselves are internally memoized, and are
207
281
  read-only once created.
@@ -262,8 +336,8 @@ Faulty.circuit(:api, cache_expires_in: 1800)
262
336
 
263
337
  Faulty integrates caching into it's circuits in a way that is particularly
264
338
  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.
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.
267
341
 
268
342
  Once your cache is configured, you can use the `cache` parameter when running
269
343
  a circuit to specify a cache key:
@@ -315,7 +389,12 @@ Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
315
389
  the storage or cache backends are captured and suppressed. Failure events for
316
390
  these errors are sent to the notifier.
317
391
 
318
- If the storage backend fails, all circuits will default to open. If the cache
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.
396
+
397
+ If the storage backend fails, circuits will default to closed. If the cache
319
398
  backend fails, all cache queries will miss.
320
399
 
321
400
  ## Event Handling
@@ -338,12 +417,17 @@ events. The full list of events is available from `Faulty::Events::EVENTS`.
338
417
  closed. Payload: `circuit`
339
418
  - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
340
419
  `status`
341
- - `storage_failure` - A storage backend raised an error. Payload `circuit`,
342
- `action`, `error`
420
+ - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
421
+ be nil), `action`, `error`
343
422
 
344
423
  By default events are logged using `Faulty::Events::LogListener`, but that can
345
424
  be replaced, or additional listeners can be added.
346
425
 
426
+ ### CallbackListener
427
+
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.
430
+
347
431
  ```ruby
348
432
  Faulty.init do |config|
349
433
  # Replace the default listener with a custom callback listener
@@ -356,6 +440,17 @@ Faulty.init do |config|
356
440
  end
357
441
  ```
358
442
 
443
+ ### Other Built-in Listeners
444
+
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.
448
+
449
+ - `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
450
+ the Honeybadger error reporting service.
451
+
452
+ ### Custom Listeners
453
+
359
454
  You can implement your own listener by following the documentation in
360
455
  `Faulty::Events::ListenerInterface`. For example:
361
456
 
@@ -375,10 +470,19 @@ end
375
470
 
376
471
  ## Configuring the Storage Backend
377
472
 
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.
476
+
378
477
  ### Memory
379
478
 
380
- The `Faulty::Cache::Memory` backend is the default storage backend. The default
381
- configuration:
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.
484
+
485
+ The default configuration:
382
486
 
383
487
  ```ruby
384
488
  Faulty.init do |config|
@@ -391,36 +495,159 @@ end
391
495
 
392
496
  ### Redis
393
497
 
394
- The `Faulty::Cache::Redis` backend provides distributed circuit storage using
395
- Redis. The default configuration:
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.
504
+
505
+ The default configuration:
396
506
 
397
507
  ```ruby
398
508
  Faulty.init do |config|
399
509
  config.storage = Faulty::Storage::Redis.new do |storage|
400
510
  # The Redis client. Accepts either a Redis instance, or a ConnectionPool
401
- # of Redis instances.
402
- storage.client = ::Redis.new
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)
403
514
 
404
515
  # The prefix to prepend to all redis keys used by Faulty circuits
405
516
  storage.key_prefix = 'faulty'
406
517
 
407
518
  # A string to separate the parts of the redis key
408
- storage.key_separator: ':'
519
+ storage.key_separator = ':'
409
520
 
410
521
  # The maximum number of circuit runs that will be stored
411
522
  storage.max_sample_size = 100
412
523
 
413
524
  # The maximum number of seconds that a circuit run will be stored
414
525
  storage.sample_ttl = 1800
526
+
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
530
+
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
534
+
535
+ # If true, disables warnings about recommended client settings like timeouts
536
+ storage.disable_warnings = false
415
537
  end
416
538
  end
417
539
  ```
418
540
 
419
- ### Listing Circuits
541
+ ### FallbackChain
542
+
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.
547
+
548
+ For example, you may configure Redis as your primary storage backend, with an
549
+ in-memory storage backend as a fallback:
550
+
551
+ ```ruby
552
+ Faulty.init do |config|
553
+ config.storage = Faulty::Storage::FallbackChain.new([
554
+ Faulty::Storage::Redis.new,
555
+ Faulty::Storage::Memory.new
556
+ ])
557
+ end
558
+ ```
559
+
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:
562
+
563
+ ```ruby
564
+ Faulty.init do |config|
565
+ config.storage = [
566
+ Faulty::Storage::Redis.new,
567
+ Faulty::Storage::Memory.new
568
+ ]
569
+ end
570
+ ```
571
+
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.
576
+
577
+ ### Storage::FaultTolerantProxy
578
+
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)).
581
+
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.
586
+
587
+ If you wish your application to use a secondary storage backend instead of
588
+ failing closed, use `FallbackChain`.
589
+
590
+ ### Storage::CircuitProxy
591
+
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)).
594
+
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.
598
+
599
+ Typically this is used inside a `FaultTolerantProxy` or `FallbackChain` so that
600
+ these storage failures are handled gracefully.
601
+
602
+ ## Configuring the Cache Backend
603
+
604
+ ### Null
605
+
606
+ The `Faulty::Cache::Null` cache disables caching. It is the default if Rails and
607
+ ActiveSupport are not present.
608
+
609
+ ### Rails
610
+
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`.
616
+
617
+ ```ruby
618
+ Faulty.init do |config|
619
+ config.cache = Faulty::Cache::Rails.new(
620
+ ActiveSupport::Cache::RedisCacheStore.new
621
+ )
622
+ end
623
+ ```
624
+
625
+ ### Cache::FaultTolerantProxy
626
+
627
+ This wrapper is applied to all non-fault-tolerant cache backends by default
628
+ (see the API docs for `Faulty::Cache::AutoWire`).
629
+
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.
633
+
634
+ ### Cache::CircuitProxy
635
+
636
+ This wrapper is applied to all non-fault-tolerant circuit backends by default
637
+ (see the API docs for `Faulty::Circuit::AutoWire`).
638
+
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.
642
+
643
+ Typically this is used inside a `FaultTolerantProxy` so that these cache
644
+ failures are handled gracefully.
645
+
646
+ ## Listing Circuits
420
647
 
421
648
  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)).
649
+ names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
650
+ if you're using an instance).
424
651
 
425
652
  You can get a list of all circuit statuses by mapping those names to their
426
653
  status objects. Be careful though, since this could cause performance issues for
@@ -432,73 +659,103 @@ statuses = Faulty.list_circuits.map do |name|
432
659
  end
433
660
  ```
434
661
 
435
- ## Scopes
662
+ ## Locking Circuits
663
+
664
+ It is possible to lock a circuit open or closed. A circuit that is locked open
665
+ will never execute its block, and always raise an `Faulty::OpenCircuitError`.
666
+ This is useful in cases where you need to manually disable a dependency
667
+ entirely. If a cached value is available, that will be returned from the circuit
668
+ until it expires, even outside its refresh period.
669
+
670
+ ```ruby
671
+ Faulty.circuit(:broken_api).lock_open!
672
+ ```
673
+
674
+ A circuit that is locked closed will never trip. This is useful in cases where a
675
+ circuit is continuously tripping incorrectly. If a cached value is available, it
676
+ will have the same behavior as an unlocked circuit.
677
+
678
+ ```ruby
679
+ Faulty.circuit(:false_positive).lock_closed!
680
+ ```
681
+
682
+ To remove a lock of either type:
683
+
684
+ ```ruby
685
+ Faulty.circuit(:fixed).unlock!
686
+ ```
687
+
688
+ Locking or unlocking a circuit has no concurrency guarantees, so it's not
689
+ recommended to lock or unlock circuits from production code. Instead, locks are
690
+ intended as an emergency tool for troubleshooting and debugging.
691
+
692
+ ## Multiple Configurations
436
693
 
437
694
  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
695
+ process. The most common setup is to simply use `Faulty.init` to
439
696
  configure Faulty globally, however it is possible to have additional
440
- configurations using scopes.
697
+ configurations.
441
698
 
442
- ### The default scope
699
+ ### The default instance
443
700
 
444
- When you call `Faulty.init`, you are actually creating the default scope. You
445
- can access this scope directly by calling `Faulty.default`.
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`.
446
703
 
447
704
  ```ruby
448
- # We create the default scope
705
+ # We create the default instance
449
706
  Faulty.init
450
707
 
451
- # Access the default scope
452
- scope = Faulty.default
708
+ # Access the default instance
709
+ faulty = Faulty.default
453
710
 
454
- # Alternatively, access the scope by name
455
- scope = Faulty[:default]
711
+ # Alternatively, access the instance by name
712
+ faulty = Faulty[:default]
456
713
  ```
457
714
 
458
- You can rename the default scope if desired:
715
+ You can rename the default instance if desired:
459
716
 
460
717
  ```ruby
461
718
  Faulty.init(:custom_default)
462
719
 
463
- scope = Faulty.default
464
- scope = Faulty[:custom_default]
720
+ instance = Faulty.default
721
+ instance = Faulty[:custom_default]
465
722
  ```
466
723
 
467
- ### Multiple Scopes
724
+ ### Multiple Instances
468
725
 
469
- If you want multiple scopes, but want global, thread-safe access to
726
+ If you want multiple instance, but want global, thread-safe access to
470
727
  them, you can use `Faulty.register`:
471
728
 
472
729
  ```ruby
473
- api_scope = Faulty::Scope.new do |config|
730
+ api_faulty = Faulty.new do |config|
474
731
  # This accepts the same options as Faulty.init
475
732
  end
476
733
 
477
- Faulty.register(:api, api_scope)
734
+ Faulty.register(:api, api_faulty)
478
735
 
479
- # Now access the scope globally
736
+ # Now access the instance globally
480
737
  Faulty[:api]
481
738
  ```
482
739
 
483
740
  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:
741
+ `Faulty.default.circuit`, so you can apply the same principal to any other
742
+ registered Faulty instance:
486
743
 
487
744
  ```ruby
488
745
  Faulty[:api].circuit(:api_circuit).run { 'ok' }
489
746
  ```
490
747
 
491
- ### Standalone Scopes
748
+ ### Standalone Instances
492
749
 
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.
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.
495
752
 
496
753
  ```ruby
497
- faulty = Faulty::Scope.new
754
+ faulty = Faulty.new
498
755
  faulty.circuit(:standalone_circuit)
499
756
  ```
500
757
 
501
- Calling `circuit` on the scope still has the same memoization behavior that
758
+ Calling `#circuit` on the instance still has the same memoization behavior that
502
759
  `Faulty.circuit` has, so subsequent calls to the same circuit will return a
503
760
  memoized circuit object.
504
761
 
@@ -545,15 +802,36 @@ would be useful for other users.
545
802
  Faulty has its own opinions about how to implement a circuit breaker in Ruby,
546
803
  but there are and have been many other options:
547
804
 
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)
805
+ ### Currently Active
806
+
807
+ - [semian](https://github.com/Shopify/semian): A resiliency toolkit that
808
+ includes circuit breakers. It uses adapters to auto-wire circuits, and it has
809
+ only host-local storage by design.
810
+ - [circuitbox](https://github.com/yammer/circuitbox): Similar in design to
811
+ Faulty, but with a different API. It uses Moneta to abstract circuit storage
812
+ to allow any key-value store.
813
+
814
+ ### Previous Work
815
+
816
+ - [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
817
+ recent activity)
818
+ - [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
551
819
  - [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
552
820
  - [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
553
821
  (unmaintained)
554
822
  - [breaker](https://github.com/ahawkins/breaker) (unmaintained)
555
823
  - [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
556
824
 
825
+ ### Faulty's Unique Features
826
+
827
+ - Simple API but configurable for advanced users
828
+ - Pluggable storage backends (circuitbox also has this)
829
+ - Protected storage access with fallback to safe storage
830
+ - Global, or object-oriented configuration with multiple instances
831
+ - Integrated caching support tailored for fault-tolerance
832
+ - Manually lock circuits open or closed
833
+
557
834
  [api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
835
+ [michael nygard]: https://www.michaelnygard.com/
558
836
  [martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
559
837
  [hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works