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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1da265b4bb2e4ae865726930ec3855a03d31c3b47c92b9ef08d7519cdb9d9d9
4
- data.tar.gz: a2fdbb6d1ccdfb7be6c68467ce45a802644ef4f9a61769e8b7387d626307facd
3
+ metadata.gz: f12337b51cbd0d484e4255078d7818354a79bf4138a5ef2b527d93915834bd9c
4
+ data.tar.gz: bbc9ec4e9e9e4707ec4d88246d77b19faead958a75b0f3e58d623fbb70d1ae59
5
5
  SHA512:
6
- metadata.gz: 2ad680693b76db3f3d420d7ded71269c8ad669c23bffa0e4ab598d8d0772955c70479d1497ea51b7aa9691b8438e06bc83ff13d4f3e3f6842a3327971dcbb4d3
7
- data.tar.gz: 2890441590276ecd72e16ee9866955b027a3e1df26907f9cc71a94f3f1161c01ea30665860bcf526fe871e84a10a7c9c728c050bbf961d8b9d670b4a6fb4bbfa
6
+ metadata.gz: 2d79bbe071f0471735795996ff7f54b0f580d445973c9c048d53bd5fac5352f0f33446e3833a39ff3cb3db88c849eb5dbb2cac253452de61d76ee467bb2e9bc9
7
+ data.tar.gz: a3266ee94d29c63a6e880461378223e95578518575e4b8d4795df028f0e4c717c0099b88d56e51a697fac99f43e1578cda92f48c937b8186003e4a56e0eeec2d
@@ -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 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.
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
- ### Listing Circuits
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
- - [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)
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
@@ -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'
@@ -21,5 +21,6 @@ module Faulty
21
21
  end
22
22
 
23
23
  require 'faulty/events/callback_listener'
24
- require 'faulty/events/notifier'
24
+ require 'faulty/events/honeybadger_listener'
25
25
  require 'faulty/events/log_listener'
26
+ require 'faulty/events/notifier'
@@ -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 { |l| l.handle(event, payload) }
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
@@ -83,7 +83,7 @@ module Faulty
83
83
  memory = fetch(circuit)
84
84
  memory.runs.borrow do |runs|
85
85
  runs.push([time, success])
86
- runs.pop if runs.size > options.max_sample_size
86
+ runs.shift if runs.size > options.max_sample_size
87
87
  end
88
88
  memory.status(circuit.options)
89
89
  end
@@ -3,6 +3,6 @@
3
3
  module Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.1.2')
6
+ Gem::Version.new('0.1.3')
7
7
  end
8
8
  end
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.2
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-08-28 00:00:00.000000000 Z
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