faulty 0.1.1 → 0.3.0

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