faulty 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +34 -0
- data/Gemfile +17 -0
- data/README.md +177 -47
- data/bin/console +1 -1
- data/faulty.gemspec +3 -10
- data/lib/faulty.rb +155 -43
- data/lib/faulty/cache.rb +1 -1
- data/lib/faulty/cache/default.rb +1 -1
- data/lib/faulty/cache/fault_tolerant_proxy.rb +2 -2
- data/lib/faulty/cache/interface.rb +1 -1
- data/lib/faulty/cache/mock.rb +1 -1
- data/lib/faulty/cache/null.rb +1 -1
- data/lib/faulty/cache/rails.rb +1 -1
- data/lib/faulty/circuit.rb +1 -1
- data/lib/faulty/error.rb +4 -4
- data/lib/faulty/events.rb +3 -2
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/honeybadger_listener.rb +53 -0
- data/lib/faulty/events/listener_interface.rb +1 -1
- data/lib/faulty/events/log_listener.rb +1 -1
- data/lib/faulty/events/notifier.rb +11 -2
- data/lib/faulty/immutable_options.rb +1 -1
- data/lib/faulty/result.rb +2 -2
- data/lib/faulty/status.rb +1 -1
- data/lib/faulty/storage.rb +1 -1
- data/lib/faulty/storage/fault_tolerant_proxy.rb +8 -10
- data/lib/faulty/storage/interface.rb +1 -1
- data/lib/faulty/storage/memory.rb +2 -2
- data/lib/faulty/storage/redis.rb +9 -9
- data/lib/faulty/version.rb +2 -2
- metadata +14 -123
- data/lib/faulty/scope.rb +0 -117
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
|
4
|
+
data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
|
7
|
+
data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -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
|
|
@@ -59,6 +62,9 @@ Metrics/BlockLength:
|
|
59
62
|
Metrics/MethodLength:
|
60
63
|
Max: 30
|
61
64
|
|
65
|
+
Naming/MethodParameterName:
|
66
|
+
MinNameLength: 1
|
67
|
+
|
62
68
|
Style/Documentation:
|
63
69
|
Enabled: false
|
64
70
|
|
data/.travis.yml
CHANGED
@@ -6,6 +6,8 @@ rvm:
|
|
6
6
|
- 2.5
|
7
7
|
- 2.6
|
8
8
|
- 2.7
|
9
|
+
- jruby
|
10
|
+
- truffleruby
|
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
|
-
-
|
26
|
-
-
|
27
|
+
- bundle exec rubocop
|
28
|
+
- bundle exec rspec --format doc
|
27
29
|
- bin/check-version
|
28
30
|
|
29
31
|
after_script:
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
## Release v0.2.0
|
2
|
+
|
3
|
+
* Remove Scopes and replace them with Faulty instances #9
|
4
|
+
|
5
|
+
### Breaking Changes
|
6
|
+
|
7
|
+
* `Faulty::Scope` has been removed. Use `Faulty.new` instead.
|
8
|
+
* `Faulty` is now a class, not a module
|
9
|
+
|
10
|
+
## Release v0.1.5
|
11
|
+
|
12
|
+
* Fix redis storage to expire state key when using CAS #8
|
13
|
+
|
14
|
+
## Release v0.1.4
|
15
|
+
|
16
|
+
* Improve spec coverage for supporting classes #6
|
17
|
+
* Fix redis bug where concurrent CAS requests could crash #7
|
18
|
+
|
19
|
+
## Release v0.1.3
|
20
|
+
|
21
|
+
* Fix bug where memory storage would delete the newest entries #5
|
22
|
+
* Add HoneybadgerListener for error reporting #4
|
23
|
+
|
24
|
+
## Release v0.1.2
|
25
|
+
|
26
|
+
* Fix Storage::FaultTolerantProxy open and reopen methods #2
|
27
|
+
|
28
|
+
## Release v0.1.1
|
29
|
+
|
30
|
+
* Fix a crash when Storage::FaultTolerantProxy created a status stub #1
|
31
|
+
|
32
|
+
## Release v0.1.0
|
33
|
+
|
34
|
+
Initial public release
|
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
|
@@ -137,9 +182,9 @@ Faulty's implementation are:
|
|
137
182
|
- Event-based monitoring
|
138
183
|
|
139
184
|
Following the principals of the circuit-breaker pattern, the block given to
|
140
|
-
`run` or `try_run` will always be executed as long as
|
141
|
-
|
142
|
-
|
185
|
+
`run` or `try_run` will always be executed as long as it never raises an error.
|
186
|
+
If the block _does_ raise an error, then the circuit keeps track of the number
|
187
|
+
of runs and the failure rate.
|
143
188
|
|
144
189
|
Once both thresholds are breached, the circuit is opened. Once open, the
|
145
190
|
circuit starts the cool-down period. Any executions within that cool-down are
|
@@ -158,11 +203,12 @@ In addition to the classic circuit breaker design, Faulty implements caching
|
|
158
203
|
that is integrated with the circuit state. See [Caching](#caching) for more
|
159
204
|
detail.
|
160
205
|
|
161
|
-
##
|
206
|
+
## Configuration
|
162
207
|
|
163
|
-
|
164
|
-
illustrates the default values.
|
165
|
-
|
208
|
+
Faulty can be configured with the following configuration options. This example
|
209
|
+
illustrates the default values. In the first example, we configure Faulty
|
210
|
+
globally. The second example shows the same configuration using an instance of
|
211
|
+
Faulty instead of global configuration.
|
166
212
|
|
167
213
|
```ruby
|
168
214
|
Faulty.init do |config|
|
@@ -188,6 +234,25 @@ Faulty.init do |config|
|
|
188
234
|
end
|
189
235
|
```
|
190
236
|
|
237
|
+
Here is the same configuration using an instance of `Faulty`. This is a more
|
238
|
+
object-oriented approach.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
faulty = Faulty.new do |config|
|
242
|
+
config.cache = Faulty::Cache::Default.new
|
243
|
+
config.storage = Faulty::Storage::Memory.new
|
244
|
+
config.listeners = [Faulty::Events::LogListener.new]
|
245
|
+
config.notifier = Faulty::Events::Notifier.new(config.listeners)
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
Most of the examples in this README use the global Faulty class methods, but
|
250
|
+
they work the same way when using an instance. Just substitute your instance
|
251
|
+
instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever
|
252
|
+
configuration mechanism works best for your application. Also see
|
253
|
+
[Multiple Configurations](#multiple-configurations) if your application needs
|
254
|
+
to set different options in different scenarios.
|
255
|
+
|
191
256
|
For all Faulty APIs that have configuration, you can also pass in an options
|
192
257
|
hash. For example, `Faulty.init` could be called like this:
|
193
258
|
|
@@ -199,9 +264,9 @@ Faulty.init(cache: Faulty::Cache::Null.new)
|
|
199
264
|
|
200
265
|
A circuit can be created with the following configuration options. Those options
|
201
266
|
are only set once, synchronized across threads, and will persist in-memory until
|
202
|
-
the process exits. If you're using [
|
203
|
-
within the context of each
|
204
|
-
`Faulty.circuit` (or `
|
267
|
+
the process exits. If you're using [multiple configurations](#multiple-configurations),
|
268
|
+
the options are retained within the context of each instance. All options given
|
269
|
+
after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
|
205
270
|
|
206
271
|
This is because the circuit objects themselves are internally memoized, and are
|
207
272
|
read-only once created.
|
@@ -262,8 +327,8 @@ Faulty.circuit(:api, cache_expires_in: 1800)
|
|
262
327
|
|
263
328
|
Faulty integrates caching into it's circuits in a way that is particularly
|
264
329
|
suited to fault-tolerance. To make use of caching, you must specify the `cache`
|
265
|
-
configuration option when initializing Faulty or creating a
|
266
|
-
using Rails, this is automatically set to the Rails cache.
|
330
|
+
configuration option when initializing Faulty or creating a new Faulty instance.
|
331
|
+
If you're using Rails, this is automatically set to the Rails cache.
|
267
332
|
|
268
333
|
Once your cache is configured, you can use the `cache` parameter when running
|
269
334
|
a circuit to specify a cache key:
|
@@ -338,12 +403,17 @@ events. The full list of events is available from `Faulty::Events::EVENTS`.
|
|
338
403
|
closed. Payload: `circuit`
|
339
404
|
- `circuit_success` - A circuit execution was successful. Payload: `circuit`,
|
340
405
|
`status`
|
341
|
-
- `storage_failure` - A storage backend raised an error. Payload `circuit
|
342
|
-
`action`, `error`
|
406
|
+
- `storage_failure` - A storage backend raised an error. Payload `circuit` (can
|
407
|
+
be nil), `action`, `error`
|
343
408
|
|
344
409
|
By default events are logged using `Faulty::Events::LogListener`, but that can
|
345
410
|
be replaced, or additional listeners can be added.
|
346
411
|
|
412
|
+
### CallbackListener
|
413
|
+
|
414
|
+
The callback listener is useful for ad-hoc handling of events. You can specify
|
415
|
+
an event handler by calling a method on the callback handler by the same name.
|
416
|
+
|
347
417
|
```ruby
|
348
418
|
Faulty.init do |config|
|
349
419
|
# Replace the default listener with a custom callback listener
|
@@ -356,6 +426,17 @@ Faulty.init do |config|
|
|
356
426
|
end
|
357
427
|
```
|
358
428
|
|
429
|
+
### Other Built-in Listeners
|
430
|
+
|
431
|
+
In addition to the log and callback listeners, Faulty intends to implement
|
432
|
+
built-in service-specific handlers to make it easy to integrate with monitoring
|
433
|
+
and reporting software.
|
434
|
+
|
435
|
+
- `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
|
436
|
+
the Honeybadger error reporting service.
|
437
|
+
|
438
|
+
### Custom Listeners
|
439
|
+
|
359
440
|
You can implement your own listener by following the documentation in
|
360
441
|
`Faulty::Events::ListenerInterface`. For example:
|
361
442
|
|
@@ -405,7 +486,7 @@ Faulty.init do |config|
|
|
405
486
|
storage.key_prefix = 'faulty'
|
406
487
|
|
407
488
|
# A string to separate the parts of the redis key
|
408
|
-
storage.key_separator
|
489
|
+
storage.key_separator = ':'
|
409
490
|
|
410
491
|
# The maximum number of circuit runs that will be stored
|
411
492
|
storage.max_sample_size = 100
|
@@ -416,11 +497,11 @@ Faulty.init do |config|
|
|
416
497
|
end
|
417
498
|
```
|
418
499
|
|
419
|
-
|
500
|
+
## Listing Circuits
|
420
501
|
|
421
502
|
For monitoring or debugging, you may need to retrieve a list of all circuit
|
422
|
-
names. This is possible with `Faulty.list_circuits` (or
|
423
|
-
|
503
|
+
names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
|
504
|
+
if you're using an instance).
|
424
505
|
|
425
506
|
You can get a list of all circuit statuses by mapping those names to their
|
426
507
|
status objects. Be careful though, since this could cause performance issues for
|
@@ -432,73 +513,103 @@ statuses = Faulty.list_circuits.map do |name|
|
|
432
513
|
end
|
433
514
|
```
|
434
515
|
|
435
|
-
##
|
516
|
+
## Locking Circuits
|
517
|
+
|
518
|
+
It is possible to lock a circuit open or closed. A circuit that is locked open
|
519
|
+
will never execute its block, and always raise an `Faulty::OpenCircuitError`.
|
520
|
+
This is useful in cases where you need to manually disable a dependency
|
521
|
+
entirely. If a cached value is available, that will be returned from the circuit
|
522
|
+
until it expires, even outside its refresh period.
|
523
|
+
|
524
|
+
```ruby
|
525
|
+
Faulty.circuit(:broken_api).lock_open!
|
526
|
+
```
|
527
|
+
|
528
|
+
A circuit that is locked closed will never trip. This is useful in cases where a
|
529
|
+
circuit is continuously tripping incorrectly. If a cached value is available, it
|
530
|
+
will have the same behavior as an unlocked circuit.
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
Faulty.circuit(:false_positive).lock_closed!
|
534
|
+
```
|
535
|
+
|
536
|
+
To remove a lock of either type:
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
Faulty.circuit(:fixed).unlock!
|
540
|
+
```
|
541
|
+
|
542
|
+
Locking or unlocking a circuit has no concurrency guarantees, so it's not
|
543
|
+
recommended to lock or unlock circuits from production code. Instead, locks are
|
544
|
+
intended as an emergency tool for troubleshooting and debugging.
|
545
|
+
|
546
|
+
## Multiple Configurations
|
436
547
|
|
437
548
|
It is possible to have multiple configurations of Faulty running within the same
|
438
|
-
process. The most common
|
549
|
+
process. The most common setup is to simply use `Faulty.init` to
|
439
550
|
configure Faulty globally, however it is possible to have additional
|
440
|
-
configurations
|
551
|
+
configurations.
|
441
552
|
|
442
|
-
### The default
|
553
|
+
### The default instance
|
443
554
|
|
444
|
-
When you call `Faulty.init`, you are actually creating the default
|
445
|
-
can access this
|
555
|
+
When you call `Faulty.init`, you are actually creating the default instance of
|
556
|
+
`Faulty`. You can access this instance directly by calling `Faulty.default`.
|
446
557
|
|
447
558
|
```ruby
|
448
|
-
# We create the default
|
559
|
+
# We create the default instance
|
449
560
|
Faulty.init
|
450
561
|
|
451
|
-
# Access the default
|
452
|
-
|
562
|
+
# Access the default instance
|
563
|
+
faulty = Faulty.default
|
453
564
|
|
454
|
-
# Alternatively, access the
|
455
|
-
|
565
|
+
# Alternatively, access the instance by name
|
566
|
+
faulty = Faulty[:default]
|
456
567
|
```
|
457
568
|
|
458
|
-
You can rename the default
|
569
|
+
You can rename the default instance if desired:
|
459
570
|
|
460
571
|
```ruby
|
461
572
|
Faulty.init(:custom_default)
|
462
573
|
|
463
|
-
|
464
|
-
|
574
|
+
instance = Faulty.default
|
575
|
+
instance = Faulty[:custom_default]
|
465
576
|
```
|
466
577
|
|
467
|
-
### Multiple
|
578
|
+
### Multiple Instances
|
468
579
|
|
469
|
-
If you want multiple
|
580
|
+
If you want multiple instance, but want global, thread-safe access to
|
470
581
|
them, you can use `Faulty.register`:
|
471
582
|
|
472
583
|
```ruby
|
473
|
-
|
584
|
+
api_faulty = Faulty.new do |config|
|
474
585
|
# This accepts the same options as Faulty.init
|
475
586
|
end
|
476
587
|
|
477
|
-
Faulty.register(:api,
|
588
|
+
Faulty.register(:api, api_faulty)
|
478
589
|
|
479
|
-
# Now access the
|
590
|
+
# Now access the instance globally
|
480
591
|
Faulty[:api]
|
481
592
|
```
|
482
593
|
|
483
594
|
When you call `Faulty.circuit`, that's the same as calling
|
484
|
-
`Faulty.default.circuit`, so you can apply the same
|
485
|
-
|
595
|
+
`Faulty.default.circuit`, so you can apply the same principal to any other
|
596
|
+
registered Faulty instance:
|
486
597
|
|
487
598
|
```ruby
|
488
599
|
Faulty[:api].circuit(:api_circuit).run { 'ok' }
|
489
600
|
```
|
490
601
|
|
491
|
-
### Standalone
|
602
|
+
### Standalone Instances
|
492
603
|
|
493
|
-
If you choose, you can use Faulty
|
494
|
-
|
604
|
+
If you choose, you can use Faulty instances without registering them globally.
|
605
|
+
This is more object-oriented and is necessary if you use dependency injection.
|
495
606
|
|
496
607
|
```ruby
|
497
|
-
faulty = Faulty
|
608
|
+
faulty = Faulty.new
|
498
609
|
faulty.circuit(:standalone_circuit)
|
499
610
|
```
|
500
611
|
|
501
|
-
Calling
|
612
|
+
Calling `#circuit` on the instance still has the same memoization behavior that
|
502
613
|
`Faulty.circuit` has, so subsequent calls to the same circuit will return a
|
503
614
|
memoized circuit object.
|
504
615
|
|
@@ -545,15 +656,34 @@ would be useful for other users.
|
|
545
656
|
Faulty has its own opinions about how to implement a circuit breaker in Ruby,
|
546
657
|
but there are and have been many other options:
|
547
658
|
|
548
|
-
|
549
|
-
|
550
|
-
- [
|
659
|
+
### Currently Active
|
660
|
+
|
661
|
+
- [semian](https://github.com/Shopify/semian): A resiliency toolkit that
|
662
|
+
includes circuit breakers. It uses adapters to auto-wire circuits, and it has
|
663
|
+
only host-local storage by design.
|
664
|
+
- [circuitbox](https://github.com/yammer/circuitbox): Similar in function to
|
665
|
+
Faulty, but with a different API. It uses Moneta to abstract circuit storage
|
666
|
+
to allow any key-value store.
|
667
|
+
|
668
|
+
### Previous Work
|
669
|
+
|
670
|
+
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
|
671
|
+
recent activity)
|
672
|
+
- [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
|
551
673
|
- [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
|
552
674
|
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
553
675
|
(unmaintained)
|
554
676
|
- [breaker](https://github.com/ahawkins/breaker) (unmaintained)
|
555
677
|
- [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
|
556
678
|
|
679
|
+
### Faulty's Unique Features
|
680
|
+
|
681
|
+
- Simple API but configurable for advanced users
|
682
|
+
- Pluggable storage backends (circuitbox also has this)
|
683
|
+
- Global, or object-oriented configuration with multiple instances
|
684
|
+
- Integrated caching support tailored for fault-tolerance
|
685
|
+
- Manually lock circuits open or closed
|
686
|
+
|
557
687
|
[api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
|
558
688
|
[martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
|
559
689
|
[hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works
|