faulty 0.1.2 → 0.1.3
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.
- 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
|