circuitbox 2.0.0.pre4 → 2.0.0
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/README.md +28 -69
- data/lib/circuitbox/circuit_breaker.rb +110 -68
- data/lib/circuitbox/configuration.rb +30 -15
- data/lib/circuitbox/errors/open_circuit_error.rb +4 -0
- data/lib/circuitbox/errors/service_failure_error.rb +2 -2
- data/lib/circuitbox/faraday_middleware.rb +7 -12
- data/lib/circuitbox/memory_store/container.rb +2 -2
- data/lib/circuitbox/memory_store.rb +12 -14
- data/lib/circuitbox/notifier/active_support.rb +5 -5
- data/lib/circuitbox/notifier/null.rb +6 -4
- data/lib/circuitbox/{memory_store/monotonic_time.rb → time_helper/monotonic.rb} +2 -2
- data/lib/circuitbox/time_helper/real.rb +13 -0
- data/lib/circuitbox/version.rb +1 -1
- data/lib/circuitbox.rb +26 -6
- metadata +33 -67
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +0 -31
- data/lib/circuitbox/timer.rb +0 -51
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 206d564d542dc903001281052403166a100732fc9b2b0f1f0999601e4543893b
|
|
4
|
+
data.tar.gz: '0095c6111ab38a311cb48ae8df82906185cef5b2b3ee8e761f08c9bacfb95782'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 314fe848d0dfbcc1aab3b71f469349b950b3dd117df76fcc88d2e37da574ce03000a9f6b62f712d7e4d1b069d5428d12d75d29bb91410ed4c0a83d8a6bfd41ae
|
|
7
|
+
data.tar.gz: 613f95add06ef602442bcde07b62f12d2fdee60c5713fcb72b71d09ddcbe8085340ffdcbd9ad83161cf31b55536c2e9ec5cce0beaddfac3737fd7a39c62aec3f
|
data/README.md
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
 [](https://badge.fury.io/rb/circuitbox)
|
|
4
4
|
|
|
5
|
-
Circuitbox is a Ruby circuit breaker gem.
|
|
5
|
+
Circuitbox is a Ruby circuit breaker gem.
|
|
6
|
+
It protects your application from failures of its service dependencies.
|
|
7
|
+
It wraps calls to external services and monitors for failures in one minute intervals.
|
|
8
|
+
Using a circuit's defaults once more than 5 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for 90 seconds.
|
|
9
|
+
This helps your application gracefully degrade.
|
|
10
|
+
|
|
6
11
|
Resources about the circuit breaker pattern:
|
|
7
12
|
* [http://martinfowler.com/bliki/CircuitBreaker.html](http://martinfowler.com/bliki/CircuitBreaker.html)
|
|
8
|
-
|
|
13
|
+
|
|
14
|
+
*Upgrading to 2.x? See [2.0 upgrade](docs/2.0-upgrade.md)*
|
|
9
15
|
|
|
10
16
|
## Usage
|
|
11
17
|
|
|
@@ -25,7 +31,7 @@ class ExampleServiceClient
|
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def http_get
|
|
28
|
-
circuit.run(
|
|
34
|
+
circuit.run(exception: false) do
|
|
29
35
|
Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
|
|
30
36
|
end
|
|
31
37
|
end
|
|
@@ -43,19 +49,22 @@ Using the `run` method will throw an exception when the circuit is open or the u
|
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
## Global Configuration
|
|
46
|
-
Circuitbox has defaults for circuit_store, notifier, and logger.
|
|
47
|
-
This can be configured through ```Circuitbox.configure```.
|
|
48
|
-
The circuit cache used by ```Circuitbox.circuit``` will be cleared after running ```Circuitbox.configure```.
|
|
49
|
-
This means when accessing the circuit through ```Circuitbox.circuit``` any custom configuration options should always be given.
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
Circuitbox defaults can be configured through ```Circuitbox.configure```.
|
|
54
|
+
There are two defaults that can be configured:
|
|
55
|
+
* `default_circuit_store` - Defaults to a `Circuitbox::MemoryStore`. This can be changed to a compatible Moneta store.
|
|
56
|
+
* `default_notifier` - Defaults to `Circuitbox::Notifier::ActiveSupport` if `ActiveSupport::Notifications` is defined, otherwise defaults to `Circuitbox::Notifier::Null`
|
|
57
|
+
|
|
58
|
+
After configuring circuitbox through `Circuitbox.configure`, the internal circuit cache of `Circuitbox.circuit` is cleared.
|
|
59
|
+
|
|
60
|
+
Any circuit created manually through ```Circuitbox::CircuitBreaker``` before updating the configuration will need to be recreated to pick up the new defaults.
|
|
61
|
+
|
|
62
|
+
The following is an example Circuitbox configuration:
|
|
53
63
|
|
|
54
64
|
```ruby
|
|
55
65
|
Circuitbox.configure do |config|
|
|
56
66
|
config.default_circuit_store = Circuitbox::MemoryStore.new
|
|
57
67
|
config.default_notifier = Circuitbox::Notifier::Null.new
|
|
58
|
-
config.default_logger = Rails.logger
|
|
59
68
|
end
|
|
60
69
|
```
|
|
61
70
|
|
|
@@ -81,17 +90,12 @@ class ExampleServiceClient
|
|
|
81
90
|
# the store you want to use to save the circuit state so it can be
|
|
82
91
|
# tracked, this needs to be Moneta compatible, and support increment
|
|
83
92
|
# this overrides what is set in the global configuration
|
|
84
|
-
|
|
93
|
+
circuit_store: Circuitbox::MemoryStore.new,
|
|
85
94
|
|
|
86
95
|
# exceeding this rate will open the circuit (checked on failures)
|
|
87
96
|
error_threshold: 50,
|
|
88
97
|
|
|
89
|
-
# Logger to use
|
|
90
|
-
# This overrides what is set in the global configuration
|
|
91
|
-
logger: Logger.new(STDOUT),
|
|
92
|
-
|
|
93
98
|
# Customized notifier
|
|
94
|
-
# overrides the default
|
|
95
99
|
# this overrides what is set in the global configuration
|
|
96
100
|
notifier: Notifier.new
|
|
97
101
|
})
|
|
@@ -108,16 +112,17 @@ Circuitbox.circuit(:yammer, {
|
|
|
108
112
|
})
|
|
109
113
|
```
|
|
110
114
|
|
|
111
|
-
## Circuit Store
|
|
115
|
+
## Circuit Store
|
|
112
116
|
|
|
113
117
|
Holds all the relevant data to trip the circuit if a given number of requests
|
|
114
118
|
fail in a specified period of time. Circuitbox also supports
|
|
115
|
-
[Moneta](https://github.com/
|
|
119
|
+
[Moneta](https://github.com/moneta-rb/moneta). As moneta is not a dependency of circuitbox
|
|
116
120
|
it needs to be loaded prior to use. There are a lot of moneta stores to choose from but
|
|
117
121
|
some pre-requisits need to be satisfied first:
|
|
118
122
|
|
|
119
123
|
- Needs to support increment, this is true for most but not all available stores.
|
|
120
124
|
- Needs to support expiry.
|
|
125
|
+
- Needs to support bulk read.
|
|
121
126
|
- Needs to support concurrent access if you share them. For example sharing a
|
|
122
127
|
KyotoCabinet store across process fails because the store is single writer
|
|
123
128
|
multiple readers, and all circuits sharing the store need to be able to write.
|
|
@@ -125,60 +130,14 @@ some pre-requisits need to be satisfied first:
|
|
|
125
130
|
|
|
126
131
|
## Notifications
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Usage example:
|
|
131
|
-
|
|
132
|
-
**Log on circuit open/close:**
|
|
133
|
-
|
|
134
|
-
```ruby
|
|
135
|
-
class CircuitOpenException < StandardError ; end
|
|
136
|
-
|
|
137
|
-
ActiveSupport::Notifications.subscribe('circuit_open') do |name, start, finish, id, payload|
|
|
138
|
-
circuit_name = payload[:circuit]
|
|
139
|
-
Rails.logger.warn("Open circuit for: #{circuit_name}")
|
|
140
|
-
end
|
|
141
|
-
ActiveSupport::Notifications.subscribe('circuit_close') do |name, start, finish, id, payload|
|
|
142
|
-
circuit_name = payload[:circuit]
|
|
143
|
-
Rails.logger.info("Close circuit for: #{circuit_name}")
|
|
144
|
-
end
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
**generate metrics:**
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
$statsd = Statsd.new 'localhost', 9125
|
|
151
|
-
|
|
152
|
-
ActiveSupport::Notifications.subscribe('circuit_gauge') do |name, start, finish, id, payload|
|
|
153
|
-
circuit_name = payload[:circuit]
|
|
154
|
-
gauge = payload[:gauge]
|
|
155
|
-
value = payload[:value]
|
|
156
|
-
metrics_key = "circuitbox.circuit.#{circuit_name}.#{gauge}"
|
|
157
|
-
|
|
158
|
-
$statsd.gauge(metrics_key, value)
|
|
159
|
-
end
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
`payload[:gauge]` can be:
|
|
163
|
-
|
|
164
|
-
- `runtime` # runtime will only be notified when circuit is closed and block is successfully executed.
|
|
165
|
-
|
|
166
|
-
**warnings:**
|
|
167
|
-
in case of misconfiguration, circuitbox will fire a circuitbox_warning
|
|
168
|
-
notification.
|
|
169
|
-
|
|
170
|
-
```ruby
|
|
171
|
-
ActiveSupport::Notifications.subscribe('circuit_warning') do |name, start, finish, id, payload|
|
|
172
|
-
circuit_name = payload[:circuit]
|
|
173
|
-
warning = payload[:message]
|
|
174
|
-
Rails.logger.warning("#{circuit_name} - #{warning}")
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
```
|
|
133
|
+
See [Circuit Notifications](docs/circuit_notifications.md)
|
|
178
134
|
|
|
179
135
|
## Faraday
|
|
180
136
|
|
|
181
|
-
Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
|
137
|
+
Circuitbox ships with a [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
|
138
|
+
The versions of faraday the middleware has been tested against is `>= 0.17` through `~> 2.0`.
|
|
139
|
+
The middleware does not support parallel requests through a connections `in_parallel` method.
|
|
140
|
+
|
|
182
141
|
|
|
183
142
|
```ruby
|
|
184
143
|
require 'faraday'
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'time_helper/monotonic'
|
|
4
|
+
require_relative 'time_helper/real'
|
|
4
5
|
|
|
5
6
|
class Circuitbox
|
|
6
7
|
class CircuitBreaker
|
|
7
|
-
include LoggerMessages
|
|
8
|
-
|
|
9
8
|
attr_reader :service, :circuit_options, :exceptions,
|
|
10
|
-
:
|
|
9
|
+
:circuit_store, :notifier, :time_class
|
|
11
10
|
|
|
12
11
|
DEFAULTS = {
|
|
13
12
|
sleep_window: 90,
|
|
@@ -16,20 +15,23 @@ class Circuitbox
|
|
|
16
15
|
time_window: 60
|
|
17
16
|
}.freeze
|
|
18
17
|
|
|
18
|
+
# Initialize a CircuitBreaker
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
20
|
+
# @param service [String, Symbol] Name of the circuit for notifications and metrics store
|
|
21
|
+
# @param options [Hash] Options to create the circuit with
|
|
22
|
+
# @option options [Integer] :time_window (60) Interval of time, in seconds, used to calculate the error_rate
|
|
23
|
+
# @option options [Integer, Proc] :sleep_window (90) Seconds for the circuit to stay open when tripped
|
|
24
|
+
# @option options [Integer, Proc] :volume_threshold (5) Number of requests before error rate is first calculated
|
|
25
|
+
# @option options [Integer, Proc] :error_threshold (50) Percentage of failed requests needed to trip the circuit
|
|
26
|
+
# @option options [Array] :exceptions The exceptions that should be monitored and counted as failures
|
|
27
|
+
# @option options [Circuitbox::MemoryStore, Moneta] :circuit_store (Circuitbox.default_circuit_store) Class to store circuit open/close statistics
|
|
28
|
+
# @option options [Object] :notifier (Circuitbox.default_notifier) Class notifications are sent to
|
|
28
29
|
#
|
|
30
|
+
# @raise [ArgumentError] If the exceptions option is not an Array
|
|
29
31
|
def initialize(service, options = {})
|
|
30
32
|
@service = service.to_s
|
|
31
33
|
@circuit_options = DEFAULTS.merge(options)
|
|
32
|
-
@circuit_store = options.fetch(:
|
|
34
|
+
@circuit_store = options.fetch(:circuit_store) { Circuitbox.default_circuit_store }
|
|
33
35
|
@notifier = options.fetch(:notifier) { Circuitbox.default_notifier }
|
|
34
36
|
|
|
35
37
|
if @circuit_options[:timeout_seconds]
|
|
@@ -37,47 +39,73 @@ class Circuitbox
|
|
|
37
39
|
'Check the upgrade guide at https://github.com/yammer/circuitbox')
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
if @circuit_options[:cache]
|
|
43
|
+
warn('cache was changed to circuit_store in circuitbox 2.0. '\
|
|
44
|
+
'Check the upgrade guide at https://github.com/yammer/circuitbox')
|
|
45
|
+
end
|
|
46
|
+
|
|
40
47
|
@exceptions = options.fetch(:exceptions)
|
|
41
|
-
raise ArgumentError.new('exceptions
|
|
48
|
+
raise ArgumentError.new('exceptions must be an array') unless @exceptions.is_a?(Array)
|
|
49
|
+
|
|
50
|
+
@time_class = options.fetch(:time_class) { default_time_klass }
|
|
42
51
|
|
|
43
|
-
@logger = options.fetch(:logger) { Circuitbox.default_logger }
|
|
44
|
-
@time_class = options.fetch(:time_class) { Time }
|
|
45
52
|
@state_change_mutex = Mutex.new
|
|
53
|
+
@open_storage_key = "circuits:#{@service}:open"
|
|
54
|
+
@half_open_storage_key = "circuits:#{@service}:half_open"
|
|
46
55
|
check_sleep_window
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
def option_value(name)
|
|
50
|
-
value = circuit_options[name]
|
|
59
|
+
value = @circuit_options[name]
|
|
51
60
|
value.is_a?(Proc) ? value.call : value
|
|
52
61
|
end
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
# Run the circuit with the given block.
|
|
64
|
+
# If the circuit is closed or half_open the block will run.
|
|
65
|
+
# If the circuit is open the block will not be run.
|
|
66
|
+
#
|
|
67
|
+
# @param exception [Boolean] If exceptions should be raised when the circuit is open
|
|
68
|
+
# or when a watched exception is raised from the block
|
|
69
|
+
# @yield Block to run if circuit is not open
|
|
70
|
+
#
|
|
71
|
+
# @raise [Circuitbox::OpenCircuitError] If the circuit is open and exception is true
|
|
72
|
+
# @raise [Circuitbox::ServiceFailureError] If a tracked exception is raised from the block and exception is true
|
|
73
|
+
#
|
|
74
|
+
# @return [Object] The result from the block
|
|
75
|
+
# @return [Nil] If the circuit is open and exception is false
|
|
76
|
+
# In cases where an exception that circuitbox is watching is raised from either a notifier
|
|
77
|
+
# or from a custom circuit store nil can be returned even though the block ran successfully
|
|
78
|
+
def run(exception: true, &block)
|
|
55
79
|
if open?
|
|
56
80
|
skipped!
|
|
57
|
-
raise Circuitbox::OpenCircuitError.new(service) if
|
|
81
|
+
raise Circuitbox::OpenCircuitError.new(@service) if exception
|
|
58
82
|
else
|
|
59
|
-
logger.debug(circuit_running_message)
|
|
60
|
-
|
|
61
83
|
begin
|
|
62
|
-
response =
|
|
84
|
+
response = @notifier.notify_run(@service, &block)
|
|
63
85
|
|
|
64
86
|
success!
|
|
65
|
-
rescue
|
|
87
|
+
rescue *@exceptions => e
|
|
66
88
|
# Other stores could raise an exception that circuitbox is asked to watch.
|
|
67
|
-
# setting to nil keeps the same behavior as the previous
|
|
89
|
+
# setting to nil keeps the same behavior as the previous definition of run.
|
|
68
90
|
response = nil
|
|
69
91
|
failure!
|
|
70
|
-
raise Circuitbox::ServiceFailureError.new(service, e) if
|
|
92
|
+
raise Circuitbox::ServiceFailureError.new(@service, e) if exception
|
|
71
93
|
end
|
|
72
94
|
end
|
|
73
95
|
|
|
74
96
|
response
|
|
75
97
|
end
|
|
76
98
|
|
|
99
|
+
# Check if the circuit is open
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean] True if circuit is open, False if closed
|
|
77
102
|
def open?
|
|
78
|
-
circuit_store.key?(open_storage_key)
|
|
103
|
+
@circuit_store.key?(@open_storage_key)
|
|
79
104
|
end
|
|
80
105
|
|
|
106
|
+
# Calculates the current error rate of the circuit
|
|
107
|
+
#
|
|
108
|
+
# @return [Float] Error Rate
|
|
81
109
|
def error_rate(failures = failure_count, success = success_count)
|
|
82
110
|
all_count = failures + success
|
|
83
111
|
return 0.0 unless all_count.positive?
|
|
@@ -85,34 +113,52 @@ class Circuitbox
|
|
|
85
113
|
(failures / all_count.to_f) * 100
|
|
86
114
|
end
|
|
87
115
|
|
|
116
|
+
# Number of Failures the circuit has encountered in the current time window
|
|
117
|
+
#
|
|
118
|
+
# @return [Integer] Number of failures
|
|
88
119
|
def failure_count
|
|
89
|
-
circuit_store.load(stat_storage_key('failure'), raw: true).to_i
|
|
120
|
+
@circuit_store.load(stat_storage_key('failure'), raw: true).to_i
|
|
90
121
|
end
|
|
91
122
|
|
|
123
|
+
# Number of successes the circuit has encountered in the current time window
|
|
124
|
+
#
|
|
125
|
+
# @return [Integer] Number of successes
|
|
92
126
|
def success_count
|
|
93
|
-
circuit_store.load(stat_storage_key('success'), raw: true).to_i
|
|
127
|
+
@circuit_store.load(stat_storage_key('success'), raw: true).to_i
|
|
94
128
|
end
|
|
95
129
|
|
|
130
|
+
# If the circuit is open the key indicating that the circuit is open
|
|
131
|
+
# On the next call to run the circuit would run as if it were in the half open state
|
|
132
|
+
#
|
|
133
|
+
# This does not reset any of the circuit success/failure state so future failures
|
|
134
|
+
# in the same time window may cause the circuit to open sooner
|
|
96
135
|
def try_close_next_time
|
|
97
|
-
circuit_store.delete(open_storage_key)
|
|
136
|
+
@circuit_store.delete(@open_storage_key)
|
|
98
137
|
end
|
|
99
138
|
|
|
100
139
|
private
|
|
101
140
|
|
|
102
141
|
def should_open?
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
aligned_time = align_time_to_window
|
|
143
|
+
|
|
144
|
+
failures, successes = @circuit_store.values_at(stat_storage_key('failure', aligned_time),
|
|
145
|
+
stat_storage_key('success', aligned_time),
|
|
146
|
+
raw: true)
|
|
147
|
+
# Calling to_i is only needed for moneta stores which can return a string representation of an integer.
|
|
148
|
+
# While readability could increase by adding .map(&:to_i) to the end of the values_at call it's also slightly
|
|
149
|
+
# less performant when we only have two values to convert.
|
|
150
|
+
failures = failures.to_i
|
|
151
|
+
successes = successes.to_i
|
|
152
|
+
|
|
153
|
+
passed_volume_threshold?(failures, successes) && passed_rate_threshold?(failures, successes)
|
|
108
154
|
end
|
|
109
155
|
|
|
110
156
|
def passed_volume_threshold?(failures, successes)
|
|
111
157
|
failures + successes >= option_value(:volume_threshold)
|
|
112
158
|
end
|
|
113
159
|
|
|
114
|
-
def passed_rate_threshold?(
|
|
115
|
-
|
|
160
|
+
def passed_rate_threshold?(failures, successes)
|
|
161
|
+
error_rate(failures, successes) >= option_value(:error_threshold)
|
|
116
162
|
end
|
|
117
163
|
|
|
118
164
|
def half_open_failure
|
|
@@ -122,7 +168,7 @@ class Circuitbox
|
|
|
122
168
|
trip
|
|
123
169
|
end
|
|
124
170
|
|
|
125
|
-
# Running event
|
|
171
|
+
# Running event outside of the synchronize block to allow other threads
|
|
126
172
|
# that may be waiting to become unblocked
|
|
127
173
|
notify_opened
|
|
128
174
|
end
|
|
@@ -134,19 +180,18 @@ class Circuitbox
|
|
|
134
180
|
trip
|
|
135
181
|
end
|
|
136
182
|
|
|
137
|
-
# Running event
|
|
183
|
+
# Running event outside of the synchronize block to allow other threads
|
|
138
184
|
# that may be waiting to become unblocked
|
|
139
185
|
notify_opened
|
|
140
186
|
end
|
|
141
187
|
|
|
142
188
|
def notify_opened
|
|
143
189
|
notify_event('open')
|
|
144
|
-
logger.debug(circuit_opened_message)
|
|
145
190
|
end
|
|
146
191
|
|
|
147
192
|
def trip
|
|
148
|
-
circuit_store.store(open_storage_key, true, expires: option_value(:sleep_window))
|
|
149
|
-
circuit_store.store(half_open_storage_key, true)
|
|
193
|
+
@circuit_store.store(@open_storage_key, true, expires: option_value(:sleep_window))
|
|
194
|
+
@circuit_store.store(@half_open_storage_key, true)
|
|
150
195
|
end
|
|
151
196
|
|
|
152
197
|
def close!
|
|
@@ -154,29 +199,26 @@ class Circuitbox
|
|
|
154
199
|
# If the circuit is not open, the half_open key will be deleted from the store
|
|
155
200
|
# if half_open exists the deleted value is returned and allows us to continue
|
|
156
201
|
# if half_open doesn't exist nil is returned, causing us to return early
|
|
157
|
-
return unless !open? && circuit_store.delete(half_open_storage_key)
|
|
202
|
+
return unless !open? && @circuit_store.delete(@half_open_storage_key)
|
|
158
203
|
end
|
|
159
204
|
|
|
160
205
|
# Running event outside of the synchronize block to allow other threads
|
|
161
206
|
# that may be waiting to become unblocked
|
|
162
207
|
notify_event('close')
|
|
163
|
-
logger.debug(circuit_closed_message)
|
|
164
208
|
end
|
|
165
209
|
|
|
166
210
|
def half_open?
|
|
167
|
-
circuit_store.key?(half_open_storage_key)
|
|
211
|
+
@circuit_store.key?(@half_open_storage_key)
|
|
168
212
|
end
|
|
169
213
|
|
|
170
214
|
def success!
|
|
171
215
|
increment_and_notify_event('success')
|
|
172
|
-
logger.debug(circuit_success_message)
|
|
173
216
|
|
|
174
217
|
close! if half_open?
|
|
175
218
|
end
|
|
176
219
|
|
|
177
220
|
def failure!
|
|
178
221
|
increment_and_notify_event('failure')
|
|
179
|
-
logger.debug(circuit_failure_message)
|
|
180
222
|
|
|
181
223
|
if half_open?
|
|
182
224
|
half_open_failure
|
|
@@ -187,20 +229,31 @@ class Circuitbox
|
|
|
187
229
|
|
|
188
230
|
def skipped!
|
|
189
231
|
notify_event('skipped')
|
|
190
|
-
logger.debug(circuit_skipped_message)
|
|
191
232
|
end
|
|
192
233
|
|
|
193
234
|
# Send event notification to notifier
|
|
194
235
|
def notify_event(event)
|
|
195
|
-
notifier.notify(service, event)
|
|
236
|
+
@notifier.notify(@service, event)
|
|
196
237
|
end
|
|
197
238
|
|
|
198
239
|
# Increment stat store and send notification
|
|
199
240
|
def increment_and_notify_event(event)
|
|
200
|
-
|
|
241
|
+
time_window = option_value(:time_window)
|
|
242
|
+
aligned_time = align_time_to_window(time_window)
|
|
243
|
+
@circuit_store.increment(stat_storage_key(event, aligned_time), 1, expires: time_window)
|
|
201
244
|
notify_event(event)
|
|
202
245
|
end
|
|
203
246
|
|
|
247
|
+
def stat_storage_key(event, aligned_time = align_time_to_window)
|
|
248
|
+
"circuits:#{@service}:stats:#{aligned_time}:#{event}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# return time representation in seconds
|
|
252
|
+
def align_time_to_window(window = option_value(:time_window))
|
|
253
|
+
time = @time_class.current_second
|
|
254
|
+
time - (time % window) # remove rest of integer division
|
|
255
|
+
end
|
|
256
|
+
|
|
204
257
|
def check_sleep_window
|
|
205
258
|
sleep_window = option_value(:sleep_window)
|
|
206
259
|
time_window = option_value(:time_window)
|
|
@@ -208,27 +261,16 @@ class Circuitbox
|
|
|
208
261
|
|
|
209
262
|
warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\
|
|
210
263
|
"the error_rate would not be reset after a sleep."
|
|
211
|
-
notifier.notify_warning(service, warning_message)
|
|
212
|
-
warn("Circuit: #{service}, Warning: #{warning_message}")
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def stat_storage_key(event)
|
|
216
|
-
"circuits:#{service}:stats:#{align_time_to_window}:#{event}"
|
|
264
|
+
@notifier.notify_warning(@service, warning_message)
|
|
265
|
+
warn("Circuit: #{@service}, Warning: #{warning_message}")
|
|
217
266
|
end
|
|
218
267
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def open_storage_key
|
|
227
|
-
@open_storage_key ||= "circuits:#{service}:open"
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def half_open_storage_key
|
|
231
|
-
@half_open_storage_key ||= "circuits:#{service}:half_open"
|
|
268
|
+
def default_time_klass
|
|
269
|
+
if @circuit_store.is_a?(Circuitbox::MemoryStore)
|
|
270
|
+
Circuitbox::TimeHelper::Monotonic
|
|
271
|
+
else
|
|
272
|
+
Circuitbox::TimeHelper::Real
|
|
273
|
+
end
|
|
232
274
|
end
|
|
233
275
|
end
|
|
234
276
|
end
|
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'memory_store'
|
|
4
|
-
require_relative 'timer'
|
|
5
4
|
require_relative 'notifier/active_support'
|
|
6
5
|
require_relative 'notifier/null'
|
|
7
6
|
|
|
8
7
|
class Circuitbox
|
|
9
8
|
module Configuration
|
|
10
9
|
attr_writer :default_circuit_store,
|
|
11
|
-
:default_notifier
|
|
12
|
-
:default_timer,
|
|
13
|
-
:default_logger
|
|
10
|
+
:default_notifier
|
|
14
11
|
|
|
12
|
+
def self.extended(base)
|
|
13
|
+
base.instance_eval do
|
|
14
|
+
@cached_circuits_mutex = Mutex.new
|
|
15
|
+
@cached_circuits = {}
|
|
16
|
+
|
|
17
|
+
# preload circuit_store because it has no other dependencies
|
|
18
|
+
default_circuit_store
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Configure Circuitbox's defaults
|
|
23
|
+
# After configuring the cached circuits are cleared
|
|
24
|
+
#
|
|
25
|
+
# @yieldparam [Circuitbox::Configuration] Circuitbox configuration
|
|
26
|
+
#
|
|
15
27
|
def configure
|
|
16
28
|
yield self
|
|
17
29
|
clear_cached_circuits!
|
|
18
30
|
nil
|
|
19
31
|
end
|
|
20
32
|
|
|
33
|
+
# Circuit store used by circuits that are not configured with a specific circuit store
|
|
34
|
+
# Defaults to Circuitbox::MemoryStore
|
|
35
|
+
#
|
|
36
|
+
# @return [Circuitbox::MemoryStore, Moneta] Circuit store
|
|
21
37
|
def default_circuit_store
|
|
22
38
|
@default_circuit_store ||= MemoryStore.new
|
|
23
39
|
end
|
|
24
40
|
|
|
41
|
+
# Notifier used by circuits that are not configured with a specific notifier.
|
|
42
|
+
# If ActiveSupport::Notifications is defined it defaults to Circuitbox::Notifier::ActiveSupport
|
|
43
|
+
# Otherwise it defaults to Circuitbox::Notifier::Null
|
|
44
|
+
#
|
|
45
|
+
# @return [Circuitbox::Notifier::ActiveSupport, Circuitbox::Notifier::Null] Notifier
|
|
25
46
|
def default_notifier
|
|
26
47
|
@default_notifier ||= if defined?(ActiveSupport::Notifications)
|
|
27
48
|
Notifier::ActiveSupport.new
|
|
@@ -30,22 +51,16 @@ class Circuitbox
|
|
|
30
51
|
end
|
|
31
52
|
end
|
|
32
53
|
|
|
33
|
-
def default_logger
|
|
34
|
-
@default_logger ||= if defined?(Rails)
|
|
35
|
-
Rails.logger
|
|
36
|
-
else
|
|
37
|
-
Logger.new($stdout)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
54
|
private
|
|
42
55
|
|
|
43
|
-
def
|
|
44
|
-
@
|
|
56
|
+
def find_or_create_circuit_breaker(service_name, options)
|
|
57
|
+
@cached_circuits_mutex.synchronize do
|
|
58
|
+
@cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options)
|
|
59
|
+
end
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
def clear_cached_circuits!
|
|
48
|
-
@cached_circuits = {}
|
|
63
|
+
@cached_circuits_mutex.synchronize { @cached_circuits = {} }
|
|
49
64
|
end
|
|
50
65
|
end
|
|
51
66
|
end
|
|
@@ -8,13 +8,13 @@ class Circuitbox
|
|
|
8
8
|
super()
|
|
9
9
|
@service = service
|
|
10
10
|
@original = exception
|
|
11
|
-
#
|
|
11
|
+
# We copy over the original exceptions backtrace if there is one
|
|
12
12
|
backtrace = exception.backtrace
|
|
13
13
|
set_backtrace(backtrace) unless backtrace.empty?
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def to_s
|
|
17
|
-
"#{self.class.
|
|
17
|
+
"#{self.class}: Service #{service.inspect} was unavailable (original: #{original})"
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -21,7 +21,7 @@ class Circuitbox
|
|
|
21
21
|
open_circuit: lambda do |response|
|
|
22
22
|
# response.status:
|
|
23
23
|
# nil -> connection could not be established, or failed very hard
|
|
24
|
-
# 5xx -> non recoverable server error,
|
|
24
|
+
# 5xx -> non recoverable server error, opposed to 4xx which are client errors
|
|
25
25
|
response.status.nil? || (response.status >= 500 && response.status <= 599)
|
|
26
26
|
end,
|
|
27
27
|
default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) },
|
|
@@ -34,10 +34,7 @@ class Circuitbox
|
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
36
|
DEFAULT_EXCEPTIONS = [
|
|
37
|
-
|
|
38
|
-
# Faraday >= 0.9.0 defines Faraday::TimeoutError and this can be used for all versions up to 1.0.0 that
|
|
39
|
-
# also define and raise Faraday::Error::TimeoutError as Faraday::TimeoutError is an ancestor
|
|
40
|
-
defined?(Faraday::TimeoutError) ? Faraday::TimeoutError : Faraday::Error::TimeoutError,
|
|
37
|
+
Faraday::TimeoutError,
|
|
41
38
|
RequestFailed
|
|
42
39
|
].freeze
|
|
43
40
|
|
|
@@ -45,8 +42,6 @@ class Circuitbox
|
|
|
45
42
|
exceptions: DEFAULT_EXCEPTIONS
|
|
46
43
|
}.freeze
|
|
47
44
|
|
|
48
|
-
attr_reader :opts
|
|
49
|
-
|
|
50
45
|
def initialize(app, opts = {})
|
|
51
46
|
@app = app
|
|
52
47
|
@opts = DEFAULT_OPTIONS.merge(opts)
|
|
@@ -70,12 +65,12 @@ class Circuitbox
|
|
|
70
65
|
private
|
|
71
66
|
|
|
72
67
|
def call_default_value(response, exception)
|
|
73
|
-
default_value = opts[:default_value]
|
|
68
|
+
default_value = @opts[:default_value]
|
|
74
69
|
default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
|
|
75
70
|
end
|
|
76
71
|
|
|
77
72
|
def open_circuit?(response)
|
|
78
|
-
opts[:open_circuit].call(response)
|
|
73
|
+
@opts[:open_circuit].call(response)
|
|
79
74
|
end
|
|
80
75
|
|
|
81
76
|
def circuit_open_value(env, service_response, exception)
|
|
@@ -83,12 +78,12 @@ class Circuitbox
|
|
|
83
78
|
end
|
|
84
79
|
|
|
85
80
|
def circuit(env)
|
|
86
|
-
identifier = opts[:identifier]
|
|
81
|
+
identifier = @opts[:identifier]
|
|
87
82
|
id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
|
|
88
83
|
|
|
89
|
-
Circuitbox.circuit(id, opts[:circuit_breaker_options])
|
|
84
|
+
Circuitbox.circuit(id, @opts[:circuit_breaker_options])
|
|
90
85
|
end
|
|
91
86
|
end
|
|
92
87
|
end
|
|
93
88
|
|
|
94
|
-
Faraday::Middleware.register_middleware
|
|
89
|
+
Faraday::Middleware.register_middleware(circuitbox: Circuitbox::FaradayMiddleware)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative '../time_helper/monotonic'
|
|
4
4
|
|
|
5
5
|
class Circuitbox
|
|
6
6
|
class MemoryStore
|
|
7
7
|
class Container
|
|
8
|
-
include
|
|
8
|
+
include TimeHelper::Monotonic
|
|
9
9
|
|
|
10
10
|
attr_accessor :value
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'time_helper/monotonic'
|
|
4
4
|
require_relative 'memory_store/container'
|
|
5
5
|
|
|
6
6
|
class Circuitbox
|
|
7
7
|
class MemoryStore
|
|
8
|
-
include
|
|
8
|
+
include TimeHelper::Monotonic
|
|
9
9
|
|
|
10
10
|
def initialize(compaction_frequency: 60)
|
|
11
11
|
@store = {}
|
|
@@ -27,7 +27,7 @@ class Circuitbox
|
|
|
27
27
|
@mutex.synchronize do
|
|
28
28
|
existing_container = fetch_container(key)
|
|
29
29
|
|
|
30
|
-
# reusing the existing container is a small
|
|
30
|
+
# reusing the existing container is a small optimization
|
|
31
31
|
# to reduce the amount of objects created
|
|
32
32
|
if existing_container
|
|
33
33
|
existing_container.expires_after(seconds_to_expire)
|
|
@@ -40,7 +40,14 @@ class Circuitbox
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def load(key, _opts = {})
|
|
43
|
-
@mutex.synchronize {
|
|
43
|
+
@mutex.synchronize { fetch_container(key)&.value }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def values_at(*keys, **_opts)
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
current_time = current_second
|
|
49
|
+
keys.map! { |key| fetch_container(key, current_time)&.value }
|
|
50
|
+
end
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
def key?(key)
|
|
@@ -53,9 +60,7 @@ class Circuitbox
|
|
|
53
60
|
|
|
54
61
|
private
|
|
55
62
|
|
|
56
|
-
def fetch_container(key)
|
|
57
|
-
current_time = current_second
|
|
58
|
-
|
|
63
|
+
def fetch_container(key, current_time = current_second)
|
|
59
64
|
compact(current_time) if @compact_after < current_time
|
|
60
65
|
|
|
61
66
|
container = @store[key]
|
|
@@ -70,13 +75,6 @@ class Circuitbox
|
|
|
70
75
|
end
|
|
71
76
|
end
|
|
72
77
|
|
|
73
|
-
def fetch_value(key)
|
|
74
|
-
container = fetch_container(key)
|
|
75
|
-
return unless container
|
|
76
|
-
|
|
77
|
-
container.value
|
|
78
|
-
end
|
|
79
|
-
|
|
80
78
|
def compact(current_time)
|
|
81
79
|
@store.delete_if { |_, value| value.expired_at?(current_time) }
|
|
82
80
|
@compact_after = current_time + @compaction_frequency
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Circuitbox
|
|
4
|
-
|
|
4
|
+
module Notifier
|
|
5
5
|
class ActiveSupport
|
|
6
6
|
def notify(circuit_name, event)
|
|
7
|
-
::ActiveSupport::Notifications.instrument("
|
|
7
|
+
::ActiveSupport::Notifications.instrument("#{event}.circuitbox", circuit: circuit_name)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def notify_warning(circuit_name, message)
|
|
11
|
-
::ActiveSupport::Notifications.instrument('
|
|
11
|
+
::ActiveSupport::Notifications.instrument('warning.circuitbox', circuit: circuit_name, message: message)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
::ActiveSupport::Notifications.instrument('
|
|
14
|
+
def notify_run(circuit_name, &block)
|
|
15
|
+
::ActiveSupport::Notifications.instrument('run.circuitbox', circuit: circuit_name, &block)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Circuitbox
|
|
4
|
-
|
|
4
|
+
module Notifier
|
|
5
5
|
class Null
|
|
6
|
-
def notify(
|
|
6
|
+
def notify(_circuit_name, _event); end
|
|
7
7
|
|
|
8
|
-
def notify_warning(
|
|
8
|
+
def notify_warning(_circuit_name, _message); end
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def notify_run(_circuit_name)
|
|
11
|
+
yield
|
|
12
|
+
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
data/lib/circuitbox/version.rb
CHANGED
data/lib/circuitbox.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'logger'
|
|
4
|
-
|
|
5
3
|
require_relative 'circuitbox/version'
|
|
6
4
|
require_relative 'circuitbox/circuit_breaker'
|
|
7
5
|
require_relative 'circuitbox/errors/error'
|
|
@@ -10,15 +8,37 @@ require_relative 'circuitbox/errors/service_failure_error'
|
|
|
10
8
|
require_relative 'circuitbox/configuration'
|
|
11
9
|
|
|
12
10
|
class Circuitbox
|
|
13
|
-
|
|
14
|
-
include Configuration
|
|
11
|
+
extend Configuration
|
|
15
12
|
|
|
13
|
+
class << self
|
|
14
|
+
# @overload circuit(service_name, options = {})
|
|
15
|
+
# Returns a Circuitbox::CircuitBreaker for the given service_name
|
|
16
|
+
#
|
|
17
|
+
# @param service_name [String, Symbol] Name of the service
|
|
18
|
+
# Mixing Symbols/Strings for the same service (:test/'test') will result in
|
|
19
|
+
# multiple circuits being created that point to the same service.
|
|
20
|
+
# @param options [Hash] Options for the circuit (See Circuitbox::CircuitBreaker#initialize options)
|
|
21
|
+
# Any configuration options should always be passed when calling this method.
|
|
22
|
+
# @return [Circuitbox::CircuitBreaker] CircuitBreaker for the given service_name
|
|
23
|
+
#
|
|
24
|
+
# @overload circuit(service_name, options = {}, &block)
|
|
25
|
+
# Runs the circuit with the given block
|
|
26
|
+
# The circuit's run method is called with `exception` set to false
|
|
27
|
+
#
|
|
28
|
+
# @param service_name [String, Symbol] Name of the service
|
|
29
|
+
# Mixing Symbols/Strings for the same service (:test/'test') will result in
|
|
30
|
+
# multiple circuits being created that point to the same service.
|
|
31
|
+
# @param options [Hash] Options for the circuit (See Circuitbox::CircuitBreaker#initialize options)
|
|
32
|
+
# Any configuration options should always be passed when calling this method.
|
|
33
|
+
#
|
|
34
|
+
# @return [Object] The result of the block
|
|
35
|
+
# @return [nil] If the circuit is open
|
|
16
36
|
def circuit(service_name, options, &block)
|
|
17
|
-
circuit = (
|
|
37
|
+
circuit = find_or_create_circuit_breaker(service_name, options)
|
|
18
38
|
|
|
19
39
|
return circuit unless block
|
|
20
40
|
|
|
21
|
-
circuit.run(
|
|
41
|
+
circuit.run(exception: false, &block)
|
|
22
42
|
end
|
|
23
43
|
end
|
|
24
44
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: circuitbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fahim Ferdous
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2023-05-04 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: bundler
|
|
@@ -17,14 +17,14 @@ dependencies:
|
|
|
17
17
|
requirements:
|
|
18
18
|
- - ">"
|
|
19
19
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: '
|
|
20
|
+
version: '2.0'
|
|
21
21
|
type: :development
|
|
22
22
|
prerelease: false
|
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
24
|
requirements:
|
|
25
25
|
- - ">"
|
|
26
26
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: '
|
|
27
|
+
version: '2.0'
|
|
28
28
|
- !ruby/object:Gem::Dependency
|
|
29
29
|
name: excon
|
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -45,20 +45,14 @@ dependencies:
|
|
|
45
45
|
requirements:
|
|
46
46
|
- - ">="
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: '0.
|
|
49
|
-
- - "<"
|
|
50
|
-
- !ruby/object:Gem::Version
|
|
51
|
-
version: '2.0'
|
|
48
|
+
version: '0.17'
|
|
52
49
|
type: :development
|
|
53
50
|
prerelease: false
|
|
54
51
|
version_requirements: !ruby/object:Gem::Requirement
|
|
55
52
|
requirements:
|
|
56
53
|
- - ">="
|
|
57
54
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: '0.
|
|
59
|
-
- - "<"
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '2.0'
|
|
55
|
+
version: '0.17'
|
|
62
56
|
- !ruby/object:Gem::Dependency
|
|
63
57
|
name: gimme
|
|
64
58
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,14 +73,14 @@ dependencies:
|
|
|
79
73
|
requirements:
|
|
80
74
|
- - "~>"
|
|
81
75
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '5.
|
|
76
|
+
version: '5.14'
|
|
83
77
|
type: :development
|
|
84
78
|
prerelease: false
|
|
85
79
|
version_requirements: !ruby/object:Gem::Requirement
|
|
86
80
|
requirements:
|
|
87
81
|
- - "~>"
|
|
88
82
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '5.
|
|
83
|
+
version: '5.14'
|
|
90
84
|
- !ruby/object:Gem::Dependency
|
|
91
85
|
name: minitest-excludes
|
|
92
86
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,14 +101,14 @@ dependencies:
|
|
|
107
101
|
requirements:
|
|
108
102
|
- - "~>"
|
|
109
103
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '1.
|
|
104
|
+
version: '1.12'
|
|
111
105
|
type: :development
|
|
112
106
|
prerelease: false
|
|
113
107
|
version_requirements: !ruby/object:Gem::Requirement
|
|
114
108
|
requirements:
|
|
115
109
|
- - "~>"
|
|
116
110
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '1.
|
|
111
|
+
version: '1.12'
|
|
118
112
|
- !ruby/object:Gem::Dependency
|
|
119
113
|
name: moneta
|
|
120
114
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -158,89 +152,61 @@ dependencies:
|
|
|
158
152
|
- !ruby/object:Gem::Version
|
|
159
153
|
version: '13.0'
|
|
160
154
|
- !ruby/object:Gem::Dependency
|
|
161
|
-
name:
|
|
162
|
-
requirement: !ruby/object:Gem::Requirement
|
|
163
|
-
requirements:
|
|
164
|
-
- - '='
|
|
165
|
-
- !ruby/object:Gem::Version
|
|
166
|
-
version: 1.8.1
|
|
167
|
-
type: :development
|
|
168
|
-
prerelease: false
|
|
169
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
170
|
-
requirements:
|
|
171
|
-
- - '='
|
|
172
|
-
- !ruby/object:Gem::Version
|
|
173
|
-
version: 1.8.1
|
|
174
|
-
- !ruby/object:Gem::Dependency
|
|
175
|
-
name: rubocop-minitest
|
|
176
|
-
requirement: !ruby/object:Gem::Requirement
|
|
177
|
-
requirements:
|
|
178
|
-
- - '='
|
|
179
|
-
- !ruby/object:Gem::Version
|
|
180
|
-
version: 0.10.3
|
|
181
|
-
type: :development
|
|
182
|
-
prerelease: false
|
|
183
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
184
|
-
requirements:
|
|
185
|
-
- - '='
|
|
186
|
-
- !ruby/object:Gem::Version
|
|
187
|
-
version: 0.10.3
|
|
188
|
-
- !ruby/object:Gem::Dependency
|
|
189
|
-
name: rubocop-performance
|
|
155
|
+
name: timecop
|
|
190
156
|
requirement: !ruby/object:Gem::Requirement
|
|
191
157
|
requirements:
|
|
192
|
-
- -
|
|
158
|
+
- - "~>"
|
|
193
159
|
- !ruby/object:Gem::Version
|
|
194
|
-
version:
|
|
160
|
+
version: '0.9'
|
|
195
161
|
type: :development
|
|
196
162
|
prerelease: false
|
|
197
163
|
version_requirements: !ruby/object:Gem::Requirement
|
|
198
164
|
requirements:
|
|
199
|
-
- -
|
|
165
|
+
- - "~>"
|
|
200
166
|
- !ruby/object:Gem::Version
|
|
201
|
-
version:
|
|
167
|
+
version: '0.9'
|
|
202
168
|
- !ruby/object:Gem::Dependency
|
|
203
|
-
name:
|
|
169
|
+
name: typhoeus
|
|
204
170
|
requirement: !ruby/object:Gem::Requirement
|
|
205
171
|
requirements:
|
|
206
|
-
- -
|
|
172
|
+
- - "~>"
|
|
207
173
|
- !ruby/object:Gem::Version
|
|
208
|
-
version:
|
|
174
|
+
version: '1.4'
|
|
209
175
|
type: :development
|
|
210
176
|
prerelease: false
|
|
211
177
|
version_requirements: !ruby/object:Gem::Requirement
|
|
212
178
|
requirements:
|
|
213
|
-
- -
|
|
179
|
+
- - "~>"
|
|
214
180
|
- !ruby/object:Gem::Version
|
|
215
|
-
version:
|
|
181
|
+
version: '1.4'
|
|
216
182
|
- !ruby/object:Gem::Dependency
|
|
217
|
-
name:
|
|
183
|
+
name: webrick
|
|
218
184
|
requirement: !ruby/object:Gem::Requirement
|
|
219
185
|
requirements:
|
|
220
186
|
- - "~>"
|
|
221
187
|
- !ruby/object:Gem::Version
|
|
222
|
-
version: '
|
|
188
|
+
version: '1.7'
|
|
223
189
|
type: :development
|
|
224
190
|
prerelease: false
|
|
225
191
|
version_requirements: !ruby/object:Gem::Requirement
|
|
226
192
|
requirements:
|
|
227
193
|
- - "~>"
|
|
228
194
|
- !ruby/object:Gem::Version
|
|
229
|
-
version: '
|
|
195
|
+
version: '1.7'
|
|
230
196
|
- !ruby/object:Gem::Dependency
|
|
231
|
-
name:
|
|
197
|
+
name: yard
|
|
232
198
|
requirement: !ruby/object:Gem::Requirement
|
|
233
199
|
requirements:
|
|
234
200
|
- - "~>"
|
|
235
201
|
- !ruby/object:Gem::Version
|
|
236
|
-
version:
|
|
202
|
+
version: 0.9.26
|
|
237
203
|
type: :development
|
|
238
204
|
prerelease: false
|
|
239
205
|
version_requirements: !ruby/object:Gem::Requirement
|
|
240
206
|
requirements:
|
|
241
207
|
- - "~>"
|
|
242
208
|
- !ruby/object:Gem::Version
|
|
243
|
-
version:
|
|
209
|
+
version: 0.9.26
|
|
244
210
|
description:
|
|
245
211
|
email:
|
|
246
212
|
- fahimfmf@gmail.com
|
|
@@ -252,7 +218,6 @@ files:
|
|
|
252
218
|
- README.md
|
|
253
219
|
- lib/circuitbox.rb
|
|
254
220
|
- lib/circuitbox/circuit_breaker.rb
|
|
255
|
-
- lib/circuitbox/circuit_breaker/logger_messages.rb
|
|
256
221
|
- lib/circuitbox/configuration.rb
|
|
257
222
|
- lib/circuitbox/errors/error.rb
|
|
258
223
|
- lib/circuitbox/errors/open_circuit_error.rb
|
|
@@ -261,10 +226,10 @@ files:
|
|
|
261
226
|
- lib/circuitbox/faraday_middleware.rb
|
|
262
227
|
- lib/circuitbox/memory_store.rb
|
|
263
228
|
- lib/circuitbox/memory_store/container.rb
|
|
264
|
-
- lib/circuitbox/memory_store/monotonic_time.rb
|
|
265
229
|
- lib/circuitbox/notifier/active_support.rb
|
|
266
230
|
- lib/circuitbox/notifier/null.rb
|
|
267
|
-
- lib/circuitbox/
|
|
231
|
+
- lib/circuitbox/time_helper/monotonic.rb
|
|
232
|
+
- lib/circuitbox/time_helper/real.rb
|
|
268
233
|
- lib/circuitbox/version.rb
|
|
269
234
|
homepage: https://github.com/yammer/circuitbox
|
|
270
235
|
licenses:
|
|
@@ -273,6 +238,7 @@ metadata:
|
|
|
273
238
|
bug_tracker_uri: https://github.com/yammer/circuitbox/issues
|
|
274
239
|
changelog_uri: https://github.com/yammer/circuitbox/blob/main/CHANGELOG.md
|
|
275
240
|
source_code_uri: https://github.com/yammer/circuitbox
|
|
241
|
+
rubygems_mfa_required: 'true'
|
|
276
242
|
post_install_message:
|
|
277
243
|
rdoc_options: []
|
|
278
244
|
require_paths:
|
|
@@ -281,14 +247,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
281
247
|
requirements:
|
|
282
248
|
- - ">="
|
|
283
249
|
- !ruby/object:Gem::Version
|
|
284
|
-
version: 2.
|
|
250
|
+
version: 2.6.0
|
|
285
251
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
286
252
|
requirements:
|
|
287
|
-
- - "
|
|
253
|
+
- - ">="
|
|
288
254
|
- !ruby/object:Gem::Version
|
|
289
|
-
version:
|
|
255
|
+
version: '0'
|
|
290
256
|
requirements: []
|
|
291
|
-
rubygems_version: 3.1.
|
|
257
|
+
rubygems_version: 3.1.6
|
|
292
258
|
signing_key:
|
|
293
259
|
specification_version: 4
|
|
294
260
|
summary: A robust circuit breaker that manages failing external services.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Circuitbox
|
|
4
|
-
class CircuitBreaker
|
|
5
|
-
module LoggerMessages
|
|
6
|
-
def circuit_skipped_message
|
|
7
|
-
@circuit_skipped_message ||= "[CIRCUIT] #{service}: skipped"
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def circuit_running_message
|
|
11
|
-
@circuit_running_message ||= "[CIRCUIT] #{service}: running"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def circuit_success_message
|
|
15
|
-
@circuit_success_message ||= "[CIRCUIT] #{service}: success"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def circuit_failure_message
|
|
19
|
-
@circuit_failure_message ||= "[CIRCUIT] #{service}: failure"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def circuit_opened_message
|
|
23
|
-
@circuit_opened_message ||= "[CIRCUIT] #{service}: opened"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def circuit_closed_message
|
|
27
|
-
@circuit_closed_message ||= "[CIRCUIT] #{service}: closed"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
data/lib/circuitbox/timer.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Circuitbox
|
|
4
|
-
class Timer
|
|
5
|
-
class Monotonic
|
|
6
|
-
class << self
|
|
7
|
-
def supported?
|
|
8
|
-
defined?(Process::CLOCK_MONOTONIC)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def now
|
|
12
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
class Default
|
|
18
|
-
class << self
|
|
19
|
-
def supported?
|
|
20
|
-
true
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def now
|
|
24
|
-
Time.now.to_f
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
class << self
|
|
30
|
-
def measure(service, notifier, metric_name)
|
|
31
|
-
before = now
|
|
32
|
-
result = yield
|
|
33
|
-
total_time = now - before
|
|
34
|
-
notifier.metric_gauge(service, metric_name, total_time)
|
|
35
|
-
result
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
if Monotonic.supported?
|
|
41
|
-
def now
|
|
42
|
-
Monotonic.now
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
def now
|
|
46
|
-
Default.now
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|