faulty 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -2
- data/README.md +121 -11
- data/faulty.gemspec +1 -0
- data/lib/faulty/events.rb +2 -1
- data/lib/faulty/events/honeybadger_listener.rb +53 -0
- data/lib/faulty/events/notifier.rb +10 -1
- data/lib/faulty/storage/memory.rb +1 -1
- data/lib/faulty/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f12337b51cbd0d484e4255078d7818354a79bf4138a5ef2b527d93915834bd9c
|
4
|
+
data.tar.gz: bbc9ec4e9e9e4707ec4d88246d77b19faead958a75b0f3e58d623fbb70d1ae59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d79bbe071f0471735795996ff7f54b0f580d445973c9c048d53bd5fac5352f0f33446e3833a39ff3cb3db88c849eb5dbb2cac253452de61d76ee467bb2e9bc9
|
7
|
+
data.tar.gz: a3266ee94d29c63a6e880461378223e95578518575e4b8d4795df028f0e4c717c0099b88d56e51a697fac99f43e1578cda92f48c937b8186003e4a56e0eeec2d
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
## Release v0.1.3
|
2
|
+
|
3
|
+
* Fix bug where memory storage would delete the newest entries #5
|
4
|
+
* Add HoneybadgerListener for error reporting #4
|
5
|
+
|
1
6
|
## Release v0.1.2
|
2
7
|
|
3
|
-
* Fix Storage::FaultTolerantProxy open and reopen methods
|
8
|
+
* Fix Storage::FaultTolerantProxy open and reopen methods #2
|
4
9
|
|
5
10
|
## Release v0.1.1
|
6
11
|
|
7
|
-
* Fix a crash when Storage::FaultTolerantProxy created a status stub
|
12
|
+
* Fix a crash when Storage::FaultTolerantProxy created a status stub #1
|
8
13
|
|
9
14
|
## Release v0.1.0
|
10
15
|
|
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
|
@@ -201,7 +246,7 @@ A circuit can be created with the following configuration options. Those options
|
|
201
246
|
are only set once, synchronized across threads, and will persist in-memory until
|
202
247
|
the process exits. If you're using [scopes](#scopes), the options are retained
|
203
248
|
within the context of each scope. All options given after the first call to
|
204
|
-
`Faulty.circuit` (or `Scope.circuit` are ignored.
|
249
|
+
`Faulty.circuit` (or `Scope.circuit`) are ignored.
|
205
250
|
|
206
251
|
This is because the circuit objects themselves are internally memoized, and are
|
207
252
|
read-only once created.
|
@@ -338,12 +383,17 @@ events. The full list of events is available from `Faulty::Events::EVENTS`.
|
|
338
383
|
closed. Payload: `circuit`
|
339
384
|
- `circuit_success` - A circuit execution was successful. Payload: `circuit`,
|
340
385
|
`status`
|
341
|
-
- `storage_failure` - A storage backend raised an error. Payload `circuit
|
342
|
-
`action`, `error`
|
386
|
+
- `storage_failure` - A storage backend raised an error. Payload `circuit` (can
|
387
|
+
be nil), `action`, `error`
|
343
388
|
|
344
389
|
By default events are logged using `Faulty::Events::LogListener`, but that can
|
345
390
|
be replaced, or additional listeners can be added.
|
346
391
|
|
392
|
+
### CallbackListener
|
393
|
+
|
394
|
+
The callback listener is useful for ad-hoc handling of events. You can specify
|
395
|
+
an event handler by calling a method on the callback handler by the same name.
|
396
|
+
|
347
397
|
```ruby
|
348
398
|
Faulty.init do |config|
|
349
399
|
# Replace the default listener with a custom callback listener
|
@@ -356,6 +406,17 @@ Faulty.init do |config|
|
|
356
406
|
end
|
357
407
|
```
|
358
408
|
|
409
|
+
### Other Built-in Listeners
|
410
|
+
|
411
|
+
In addition to the log and callback listeners, Faulty intends to implement
|
412
|
+
built-in service-specific handlers to make it easy to integrate with monitoring
|
413
|
+
and reporting software.
|
414
|
+
|
415
|
+
- `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
|
416
|
+
the Honeybadger error reporting service.
|
417
|
+
|
418
|
+
### Custom Listeners
|
419
|
+
|
359
420
|
You can implement your own listener by following the documentation in
|
360
421
|
`Faulty::Events::ListenerInterface`. For example:
|
361
422
|
|
@@ -405,7 +466,7 @@ Faulty.init do |config|
|
|
405
466
|
storage.key_prefix = 'faulty'
|
406
467
|
|
407
468
|
# A string to separate the parts of the redis key
|
408
|
-
storage.key_separator
|
469
|
+
storage.key_separator = ':'
|
409
470
|
|
410
471
|
# The maximum number of circuit runs that will be stored
|
411
472
|
storage.max_sample_size = 100
|
@@ -416,7 +477,7 @@ Faulty.init do |config|
|
|
416
477
|
end
|
417
478
|
```
|
418
479
|
|
419
|
-
|
480
|
+
## Listing Circuits
|
420
481
|
|
421
482
|
For monitoring or debugging, you may need to retrieve a list of all circuit
|
422
483
|
names. This is possible with `Faulty.list_circuits` (or the equivalent method on
|
@@ -432,6 +493,36 @@ statuses = Faulty.list_circuits.map do |name|
|
|
432
493
|
end
|
433
494
|
```
|
434
495
|
|
496
|
+
## Locking Circuits
|
497
|
+
|
498
|
+
It is possible to lock a circuit open or closed. A circuit that is locked open
|
499
|
+
will never execute its block, and always raise an `Faulty::OpenCircuitError`.
|
500
|
+
This is useful in cases where you need to manually disable a dependency
|
501
|
+
entirely. If a cached value is available, that will be returned from the circuit
|
502
|
+
until it expires, even outside its refresh period.
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
Faulty.circuit(:broken_api).lock_open!
|
506
|
+
```
|
507
|
+
|
508
|
+
A circuit that is locked closed will never trip. This is useful in cases where a
|
509
|
+
circuit is continuously tripping incorrectly. If a cached value is available, it
|
510
|
+
will have the same behavior as an unlocked circuit.
|
511
|
+
|
512
|
+
```ruby
|
513
|
+
Faulty.circuit(:false_positive).lock_closed!
|
514
|
+
```
|
515
|
+
|
516
|
+
To remove a lock of either type:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
Faulty.circuit(:fixed).unlock!
|
520
|
+
```
|
521
|
+
|
522
|
+
Locking or unlocking a circuit has no concurrency guarantees, so it's not
|
523
|
+
recommended to lock or unlock circuits from production code. Instead, locks are
|
524
|
+
intended as an emergency tool for troubleshooting and debugging.
|
525
|
+
|
435
526
|
## Scopes
|
436
527
|
|
437
528
|
It is possible to have multiple configurations of Faulty running within the same
|
@@ -545,15 +636,34 @@ would be useful for other users.
|
|
545
636
|
Faulty has its own opinions about how to implement a circuit breaker in Ruby,
|
546
637
|
but there are and have been many other options:
|
547
638
|
|
548
|
-
|
549
|
-
|
550
|
-
- [
|
639
|
+
### Currently Active
|
640
|
+
|
641
|
+
- [semian](https://github.com/Shopify/semian): A resiliency toolkit that
|
642
|
+
includes circuit breakers. It uses adapters to auto-wire circuits, and it has
|
643
|
+
only host-local storage by design.
|
644
|
+
- [circuitbox](https://github.com/yammer/circuitbox): Similar in function to
|
645
|
+
Faulty, but with a different API. It uses Moneta to abstract circuit storage
|
646
|
+
to allow any key-value store.
|
647
|
+
|
648
|
+
### Previous Work
|
649
|
+
|
650
|
+
- [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
|
651
|
+
recent activity)
|
652
|
+
- [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
|
551
653
|
- [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
|
552
654
|
- [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
|
553
655
|
(unmaintained)
|
554
656
|
- [breaker](https://github.com/ahawkins/breaker) (unmaintained)
|
555
657
|
- [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
|
556
658
|
|
659
|
+
### Faulty's Unique Features
|
660
|
+
|
661
|
+
- Simple API but configurable for advanced users
|
662
|
+
- Pluggable storage backends (circuitbox also has this)
|
663
|
+
- Global, or local configuration with scopes
|
664
|
+
- Integrated caching support tailored for fault-tolerance
|
665
|
+
- Manually lock circuits open or closed
|
666
|
+
|
557
667
|
[api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
|
558
668
|
[martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
|
559
669
|
[hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works
|
data/faulty.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency 'bundler', '>= 1.17', '< 3'
|
28
28
|
spec.add_development_dependency 'byebug', '~> 11.0'
|
29
29
|
spec.add_development_dependency 'connection_pool', '~> 2.0'
|
30
|
+
spec.add_development_dependency 'honeybadger', '>= 2.0'
|
30
31
|
spec.add_development_dependency 'irb', '~> 1.0'
|
31
32
|
spec.add_development_dependency 'redcarpet', '~> 3.5'
|
32
33
|
spec.add_development_dependency 'redis', '~> 3.0'
|
data/lib/faulty/events.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Events
|
5
|
+
# Reports circuit errors to Honeybadger
|
6
|
+
#
|
7
|
+
# https://www.honeybadger.io/
|
8
|
+
#
|
9
|
+
# The honeybadger gem must be available.
|
10
|
+
class HoneybadgerListener
|
11
|
+
# (see ListenerInterface#handle)
|
12
|
+
def handle(event, payload)
|
13
|
+
return unless EVENTS.include?(event)
|
14
|
+
|
15
|
+
send(event, payload) if respond_to?(event, true)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def circuit_failure(payload)
|
21
|
+
_circuit_error(payload)
|
22
|
+
end
|
23
|
+
|
24
|
+
def circuit_opened(payload)
|
25
|
+
_circuit_error(payload)
|
26
|
+
end
|
27
|
+
|
28
|
+
def circuit_reopened(payload)
|
29
|
+
_circuit_error(payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def cache_failure(payload)
|
33
|
+
Honeybadger.notify(payload[:error], context: {
|
34
|
+
action: payload[:action],
|
35
|
+
key: payload[:key]
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def storage_failure(payload)
|
40
|
+
Honeybadger.notify(payload[:error], context: {
|
41
|
+
action: payload[:action],
|
42
|
+
circuit: payload[:circuit]&.name
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
def _circuit_error(payload)
|
47
|
+
Honeybadger.notify(payload[:error], context: {
|
48
|
+
circuit: payload[:circuit].name
|
49
|
+
})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -11,6 +11,9 @@ module Faulty
|
|
11
11
|
|
12
12
|
# Notify all listeners of an event
|
13
13
|
#
|
14
|
+
# If a listener raises an error while handling an event, that error will
|
15
|
+
# be captured and written to STDERR.
|
16
|
+
#
|
14
17
|
# @param event [Symbol] The event name
|
15
18
|
# @param payload [Hash] A hash of event payload data. The payload keys
|
16
19
|
# differ between events, but should be consistent across calls for a
|
@@ -18,7 +21,13 @@ module Faulty
|
|
18
21
|
def notify(event, payload)
|
19
22
|
raise ArgumentError, "Unknown event #{event}" unless EVENTS.include?(event)
|
20
23
|
|
21
|
-
@listeners.each
|
24
|
+
@listeners.each do |listener|
|
25
|
+
begin
|
26
|
+
listener.handle(event, payload)
|
27
|
+
rescue StandardError => e
|
28
|
+
warn "Faulty listener #{listener.class.name} crashed: #{e.message}"
|
29
|
+
end
|
30
|
+
end
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
data/lib/faulty/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faulty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Howard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -86,6 +86,20 @@ dependencies:
|
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '2.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: honeybadger
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '2.0'
|
89
103
|
- !ruby/object:Gem::Dependency
|
90
104
|
name: irb
|
91
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -268,6 +282,7 @@ files:
|
|
268
282
|
- lib/faulty/error.rb
|
269
283
|
- lib/faulty/events.rb
|
270
284
|
- lib/faulty/events/callback_listener.rb
|
285
|
+
- lib/faulty/events/honeybadger_listener.rb
|
271
286
|
- lib/faulty/events/listener_interface.rb
|
272
287
|
- lib/faulty/events/log_listener.rb
|
273
288
|
- lib/faulty/events/notifier.rb
|