circuitbox 1.0.3 → 2.0.0.pre3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +68 -122
- data/lib/circuitbox.rb +12 -56
- data/lib/circuitbox/circuit_breaker.rb +133 -154
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +55 -0
- data/lib/circuitbox/errors/error.rb +1 -2
- data/lib/circuitbox/errors/open_circuit_error.rb +0 -1
- data/lib/circuitbox/errors/service_failure_error.rb +2 -1
- data/lib/circuitbox/excon_middleware.rb +15 -24
- data/lib/circuitbox/faraday_middleware.rb +38 -61
- data/lib/circuitbox/memory_store.rb +84 -0
- data/lib/circuitbox/memory_store/container.rb +28 -0
- data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
- data/lib/circuitbox/notifier/active_support.rb +19 -0
- data/lib/circuitbox/notifier/null.rb +11 -0
- data/lib/circuitbox/timer/monotonic.rb +17 -0
- data/lib/circuitbox/timer/null.rb +9 -0
- data/lib/circuitbox/timer/simple.rb +13 -0
- data/lib/circuitbox/version.rb +1 -1
- metadata +81 -149
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -8
- data/Gemfile +0 -6
- data/Rakefile +0 -18
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -38
- data/lib/circuitbox/memcache_store.rb +0 -31
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -449
- data/test/circuitbox_test.rb +0 -54
- data/test/excon_middleware_test.rb +0 -131
- data/test/faraday_middleware_test.rb +0 -175
- data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
- data/test/integration/faraday_middleware_test.rb +0 -78
- data/test/integration_helper.rb +0 -48
- data/test/notifier_test.rb +0 -21
- data/test/service_failure_error_test.rb +0 -30
- data/test/test_helper.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91bcf88d24630e489540fd2c149ce6b468ecdd7e
|
4
|
+
data.tar.gz: 8576e094971fb74bb390b2a1508fd6c330644915
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e2246d5b01dddaa5fb1e3637f5f66a1bb1a9ffcbf20d8bec5d28d787328f748b7dc1818a72662ede92aab94eae32f1608eacc42697cd0fbe72f5ef16f7c42c9
|
7
|
+
data.tar.gz: 9b2022385d22e3e6fc31a1be8028fa047a49fb59f09779c77bdae53712ab8cda482513f6dd7909a1ee3b04ca4a6fdcb1d9a110cc57febb1b5e712b19f284b4a0
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/yammer/circuitbox.svg?branch=master)](https://travis-ci.org/yammer/circuitbox) [![Gem Version](https://badge.fury.io/rb/circuitbox.svg)](https://badge.fury.io/rb/circuitbox)
|
4
4
|
|
5
|
-
Circuitbox is a Ruby circuit breaker gem. It protects your application from failures of
|
5
|
+
Circuitbox is a Ruby circuit breaker gem. It protects your application from failures of its service dependencies. It wraps calls to external services and monitors for failures in one minute intervals. Once more than 10 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for one minute. This helps your application gracefully degrade.
|
6
6
|
Resources about the circuit breaker pattern:
|
7
7
|
* [http://martinfowler.com/bliki/CircuitBreaker.html](http://martinfowler.com/bliki/CircuitBreaker.html)
|
8
8
|
* [https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker](https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker)
|
@@ -10,13 +10,13 @@ Resources about the circuit breaker pattern:
|
|
10
10
|
## Usage
|
11
11
|
|
12
12
|
```ruby
|
13
|
-
Circuitbox
|
13
|
+
Circuitbox.circuit(:your_service, exceptions: [Net::ReadTimeout]) do
|
14
14
|
Net::HTTP.get URI('http://example.com/api/messages')
|
15
15
|
end
|
16
16
|
```
|
17
17
|
|
18
18
|
Circuitbox will return nil for failed requests and open circuits.
|
19
|
-
If your HTTP client has
|
19
|
+
If your HTTP client has its own conditions for failure, you can pass an `exceptions` option.
|
20
20
|
|
21
21
|
```ruby
|
22
22
|
class ExampleServiceClient
|
@@ -25,47 +25,82 @@ class ExampleServiceClient
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def http_get
|
28
|
-
circuit.run do
|
28
|
+
circuit.run(circuitbox_exceptions: false) do
|
29
29
|
Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
33
|
```
|
34
34
|
|
35
|
-
Using the `run
|
35
|
+
Using the `run` method will throw an exception when the circuit is open or the underlying service fails.
|
36
36
|
|
37
37
|
```ruby
|
38
38
|
def http_get
|
39
|
-
circuit.run
|
39
|
+
circuit.run do
|
40
40
|
Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
|
41
41
|
end
|
42
42
|
end
|
43
43
|
```
|
44
44
|
|
45
|
+
## Global Configuration
|
46
|
+
Circuitbox has defaults for circuit_store, notifier, timer, 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.
|
45
50
|
|
46
|
-
|
51
|
+
Any circuit created manually through ```Circuitbox::CircuitBreaker``` before updating the configuration
|
52
|
+
will need to be recreated to pick up the new defaults.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Circuitbox.configure do |config|
|
56
|
+
config.default_circuit_store = Circuitbox::MemoryStore.new
|
57
|
+
config.default_notifier = Circuitbox::Notifier::Null.new
|
58
|
+
config.default_timer = Circuitbox::Timer::Simple.new
|
59
|
+
config.default_logger = Rails.logger
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
## Per-Circuit Configuration
|
47
65
|
|
48
66
|
```ruby
|
49
67
|
class ExampleServiceClient
|
50
68
|
def circuit
|
51
69
|
Circuitbox.circuit(:your_service, {
|
70
|
+
# exceptions circuitbox tracks for counting failures (required)
|
52
71
|
exceptions: [YourCustomException],
|
53
72
|
|
54
73
|
# seconds the circuit stays open once it has passed the error threshold
|
55
74
|
sleep_window: 300,
|
56
75
|
|
57
|
-
#
|
76
|
+
# length of interval (in seconds) over which it calculates the error rate
|
77
|
+
time_window: 60,
|
78
|
+
|
79
|
+
# number of requests within `time_window` seconds before it calculates error rates (checked on failures)
|
58
80
|
volume_threshold: 10,
|
59
81
|
|
60
82
|
# the store you want to use to save the circuit state so it can be
|
61
83
|
# tracked, this needs to be Moneta compatible, and support increment
|
62
|
-
|
84
|
+
# this overrides what is set in the global configuration
|
85
|
+
cache: Circuitbox::MemoryStore.new,
|
63
86
|
|
64
|
-
# exceeding this rate will open the circuit
|
87
|
+
# exceeding this rate will open the circuit (checked on failures)
|
65
88
|
error_threshold: 50,
|
66
89
|
|
67
|
-
#
|
68
|
-
|
90
|
+
# Logger to use
|
91
|
+
# This overrides what is set in the global configuration
|
92
|
+
logger: Logger.new(STDOUT),
|
93
|
+
|
94
|
+
# Customized Timer object
|
95
|
+
# Use NullTimer if you don't want to time circuit execution
|
96
|
+
# Use MonotonicTimer to avoid bad time metrics on system time resync
|
97
|
+
# This overrides what is set in the global configuration
|
98
|
+
execution_timer: SimpleTimer,
|
99
|
+
|
100
|
+
# Customized notifier
|
101
|
+
# overrides the default
|
102
|
+
# this overrides what is set in the global configuration
|
103
|
+
notifier: Notifier.new
|
69
104
|
})
|
70
105
|
end
|
71
106
|
end
|
@@ -75,29 +110,26 @@ You can also pass a Proc as an option value which will evaluate each time the ci
|
|
75
110
|
|
76
111
|
```ruby
|
77
112
|
Circuitbox.circuit(:yammer, {
|
78
|
-
sleep_window: Proc.new { Configuration.get(:sleep_window) }
|
113
|
+
sleep_window: Proc.new { Configuration.get(:sleep_window) },
|
114
|
+
exceptions: [Net::ReadTimeout]
|
79
115
|
})
|
80
116
|
```
|
81
117
|
|
82
118
|
## Circuit Store (:cache)
|
83
119
|
|
84
120
|
Holds all the relevant data to trip the circuit if a given number of requests
|
85
|
-
fail in a specified period of time.
|
86
|
-
[Moneta](https://github.com/minad/moneta)
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
-
|
121
|
+
fail in a specified period of time. Circuitbox also supports
|
122
|
+
[Moneta](https://github.com/minad/moneta). As moneta is not a dependency of circuitbox
|
123
|
+
it needs to be loaded prior to use. There are a lot of moneta stores to choose from but
|
124
|
+
some pre-requisits need to be satisfied first:
|
125
|
+
|
126
|
+
- Needs to support increment, this is true for most but not all available stores.
|
127
|
+
- Needs to support expiry.
|
128
|
+
- Needs to support concurrent access if you share them. For example sharing a
|
91
129
|
KyotoCabinet store across process fails because the store is single writer
|
92
130
|
multiple readers, and all circuits sharing the store need to be able to write.
|
93
131
|
|
94
132
|
|
95
|
-
## Monitoring & Statistics
|
96
|
-
|
97
|
-
You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
|
98
|
-
Add `PARTITION={partition_key}` to see the circuit for a particular partition.
|
99
|
-
The stats are aggregated into 1 minute intervals.
|
100
|
-
|
101
133
|
## Notifications
|
102
134
|
|
103
135
|
circuitbox use ActiveSupport Notifications.
|
@@ -117,7 +149,7 @@ ActiveSupport::Notifications.subscribe('circuit_close') do |name, start, finish,
|
|
117
149
|
circuit_name = payload[:circuit]
|
118
150
|
Rails.logger.info("Close circuit for: #{circuit_name}")
|
119
151
|
end
|
120
|
-
|
152
|
+
```
|
121
153
|
|
122
154
|
**generate metrics:**
|
123
155
|
|
@@ -136,9 +168,7 @@ end
|
|
136
168
|
|
137
169
|
`payload[:gauge]` can be:
|
138
170
|
|
139
|
-
- `
|
140
|
-
- `success_count`
|
141
|
-
- `error_rate`
|
171
|
+
- `execution_time` # execution time will only be notified when circuit is closed and block is successfully executed.
|
142
172
|
|
143
173
|
**warnings:**
|
144
174
|
in case of misconfiguration, circuitbox will fire a circuitbox_warning
|
@@ -153,77 +183,6 @@ end
|
|
153
183
|
|
154
184
|
```
|
155
185
|
|
156
|
-
### Multi process Circuits
|
157
|
-
|
158
|
-
`circuit_store` is backed by [Moneta](https://github.com/minad/moneta) which
|
159
|
-
supports multiple backends. This can be configured by passing `cache:
|
160
|
-
Moneta.new(:PStore, file: "myfile.store")` to use for example the built in
|
161
|
-
PStore ruby library for persisted store, which can be shared cross process.
|
162
|
-
|
163
|
-
Depending on your requirements different stores can make sense, see the
|
164
|
-
benchmarks and [moneta
|
165
|
-
feature](https://github.com/minad/moneta#backend-feature-matrix) matrix for
|
166
|
-
details.
|
167
|
-
|
168
|
-
```
|
169
|
-
user system total real
|
170
|
-
memory: 1.440000 0.140000 1.580000 ( 1.579244)
|
171
|
-
lmdb: 4.330000 3.280000 7.610000 ( 13.086398)
|
172
|
-
pstore: 23.680000 4.350000 28.030000 ( 28.094312)
|
173
|
-
daybreak: 2.270000 0.450000 2.720000 ( 2.626748)
|
174
|
-
```
|
175
|
-
|
176
|
-
You can run the benchmarks yourself by running `rake benchmark`.
|
177
|
-
|
178
|
-
### Memory
|
179
|
-
|
180
|
-
An in memory store, which is local to the process. This is not threadsafe so it
|
181
|
-
is not useable with multithreaded webservers for example. It is always going to
|
182
|
-
be the fastest option if no multi-process or thread is required, like in
|
183
|
-
development on Webbrick.
|
184
|
-
|
185
|
-
This is the default.
|
186
|
-
|
187
|
-
```ruby
|
188
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:Memory)
|
189
|
-
```
|
190
|
-
|
191
|
-
### LMDB
|
192
|
-
|
193
|
-
An persisted directory backed store, which is thread and multi process safe.
|
194
|
-
depends on the `lmdb` gem. It is slower than Memory or Daybreak, but can be
|
195
|
-
used in multi thread and multi process environments like like Puma.
|
196
|
-
|
197
|
-
```ruby
|
198
|
-
require "lmdb"
|
199
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:LMDB, dir: "./", db: "mydb")
|
200
|
-
```
|
201
|
-
|
202
|
-
### PStore
|
203
|
-
|
204
|
-
An persisted file backed store, which comes with the ruby
|
205
|
-
[stdlib](http://ruby-doc.org/stdlib-2.3.0/libdoc/pstore/rdoc/PStore.html). It
|
206
|
-
has no external dependecies and works on every ruby implementation. Due to it
|
207
|
-
being file backed it is multi process safe, good for development using Unicorn.
|
208
|
-
|
209
|
-
```ruby
|
210
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:PStore, file: "db.pstore")
|
211
|
-
```
|
212
|
-
|
213
|
-
### Daybreak
|
214
|
-
|
215
|
-
Persisted, file backed key value store in pure ruby. It is process safe and
|
216
|
-
outperforms most other stores in circuitbox. This is recommended for production
|
217
|
-
use with Unicorn. It depends on the `daybreak` gem.
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
require "daybreak"
|
221
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:Daybreak, file: "db.daybreak", expires: true)
|
222
|
-
```
|
223
|
-
|
224
|
-
It is important for the store to have
|
225
|
-
[expires](https://github.com/minad/moneta#backend-feature-matrix) support.
|
226
|
-
|
227
186
|
## Faraday
|
228
187
|
|
229
188
|
Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
@@ -247,24 +206,15 @@ end
|
|
247
206
|
By default the Faraday middleware returns a `503` response when the circuit is
|
248
207
|
open, but this as many other things can be configured via middleware options
|
249
208
|
|
250
|
-
* `exceptions` pass a list of exceptions for the Circuitbreaker to catch,
|
251
|
-
defaults to Timeout and Request failures
|
252
|
-
|
253
|
-
```ruby
|
254
|
-
c.use Circuitbox::FaradayMiddleware, exceptions: [Faraday::Error::TimeoutError]
|
255
|
-
```
|
256
|
-
|
257
209
|
* `default_value` value to return for open circuits, defaults to 503 response
|
258
210
|
wrapping the original response given by the service and stored as
|
259
211
|
`original_response` property of the returned 503, this can be overwritten
|
260
212
|
with either
|
261
213
|
* a static value
|
262
214
|
* a `lambda` which is passed the `original_response` and `original_error`.
|
263
|
-
`original_response` will be populated if Faraday
|
215
|
+
`original_response` will be populated if Faraday returns an error response,
|
264
216
|
`original_error` will be populated if an error was thrown before Faraday
|
265
|
-
returned a response.
|
266
|
-
only passed `original_response`. This use is deprecated and will be removed
|
267
|
-
in the next major version.)
|
217
|
+
returned a response.
|
268
218
|
|
269
219
|
```ruby
|
270
220
|
c.use Circuitbox::FaradayMiddleware, default_value: lambda { |response, error| ... }
|
@@ -276,15 +226,9 @@ c.use Circuitbox::FaradayMiddleware, default_value: lambda { |response, error| .
|
|
276
226
|
c.use Circuitbox::FaradayMiddleware, identifier: "service_name_circuit"
|
277
227
|
```
|
278
228
|
|
279
|
-
* `circuit_breaker_run_options` options passed to the circuit run method, see
|
280
|
-
the main circuitbreaker for those.
|
281
|
-
|
282
|
-
```ruby
|
283
|
-
conn.get("/api", circuit_breaker_run_options: {})
|
284
|
-
```
|
285
|
-
|
286
229
|
* `circuit_breaker_options` options to initialize the circuit with defaults to
|
287
|
-
`{
|
230
|
+
`{ exceptions: Circuitbox::FaradayMiddleware::DEFAULT_EXCEPTIONS }`.
|
231
|
+
Accepts same options as Circuitbox:CircuitBreaker#new
|
288
232
|
|
289
233
|
```ruby
|
290
234
|
c.use Circuitbox::FaradayMiddleware, circuit_breaker_options: {}
|
@@ -298,7 +242,9 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
|
|
298
242
|
```
|
299
243
|
|
300
244
|
## CHANGELOG
|
301
|
-
###
|
245
|
+
### v1.1.0
|
246
|
+
- ruby 2.2 support [#58](https://github.com/yammer/circuitbox/pull/58)
|
247
|
+
- configurable logger [#58](https://github.com/yammer/circuitbox/pull/58)
|
302
248
|
|
303
249
|
### v1.0.3
|
304
250
|
- fix timeout issue for default configuration, as default `:Memory` adapter does
|
@@ -315,7 +261,7 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
|
|
315
261
|
removing the related railtie.
|
316
262
|
|
317
263
|
### v1.0.0
|
318
|
-
- support for cross process circuitbreakers by swapping the circuitbreaker store for a
|
264
|
+
- support for cross process circuitbreakers by swapping the circuitbreaker store for a
|
319
265
|
`Moneta` supported key value store.
|
320
266
|
- Change `FaradayMiddleware` default behaviour to not open on `4xx` errors but just on `5xx`
|
321
267
|
server errors and connection errors
|
@@ -351,7 +297,7 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
|
|
351
297
|
|
352
298
|
### v0.10
|
353
299
|
- configuration option for faraday middleware for what should be considered to open the circuit [enrico-scalavio](https://github.com/enrico-scalavino)
|
354
|
-
- fix for issue 16, support of in_parallel requests in faraday
|
300
|
+
- fix for issue 16, support of in_parallel requests in faraday middleware which were opening the circuit.
|
355
301
|
- deprecate the __run_option__ `:storage_key`
|
356
302
|
|
357
303
|
### v0.9
|
data/lib/circuitbox.rb
CHANGED
@@ -1,67 +1,23 @@
|
|
1
1
|
require 'uri'
|
2
|
-
require 'singleton'
|
3
|
-
require 'active_support'
|
4
2
|
require 'logger'
|
5
|
-
require 'timeout'
|
6
|
-
require 'moneta'
|
7
3
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
require 'circuitbox/errors/open_circuit_error'
|
15
|
-
require 'circuitbox/errors/service_failure_error'
|
4
|
+
require_relative 'circuitbox/version'
|
5
|
+
require_relative 'circuitbox/circuit_breaker'
|
6
|
+
require_relative 'circuitbox/errors/error'
|
7
|
+
require_relative 'circuitbox/errors/open_circuit_error'
|
8
|
+
require_relative 'circuitbox/errors/service_failure_error'
|
9
|
+
require_relative 'circuitbox/configuration'
|
16
10
|
|
17
11
|
class Circuitbox
|
18
|
-
|
19
|
-
|
12
|
+
class << self
|
13
|
+
include Configuration
|
20
14
|
|
21
|
-
|
22
|
-
|
23
|
-
end
|
15
|
+
def circuit(service_name, options)
|
16
|
+
circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
|
24
17
|
|
25
|
-
|
26
|
-
self.instance_eval(&@@configure) if @@configure
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.configure(&block)
|
30
|
-
@@configure = block if block
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.reset
|
34
|
-
@@instance = nil
|
35
|
-
@@configure = nil
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.circuit_store
|
39
|
-
self.instance.circuit_store ||= Moneta.new(:Memory, expires: true)
|
40
|
-
end
|
18
|
+
return circuit unless block_given?
|
41
19
|
|
42
|
-
|
43
|
-
self.instance.circuit_store = store
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.[](service_identifier, options = {})
|
47
|
-
self.circuit(service_identifier, options)
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.circuit(service_identifier, options = {})
|
51
|
-
service_name = self.parameter_to_service_name(service_identifier)
|
52
|
-
|
53
|
-
self.instance.circuits ||= Hash.new
|
54
|
-
self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
|
55
|
-
|
56
|
-
if block_given?
|
57
|
-
self.instance.circuits[service_name].run { yield }
|
58
|
-
else
|
59
|
-
self.instance.circuits[service_name]
|
20
|
+
circuit.run(circuitbox_exceptions: false) { yield }
|
60
21
|
end
|
61
22
|
end
|
62
|
-
|
63
|
-
def self.parameter_to_service_name(param)
|
64
|
-
uri = URI(param.to_s)
|
65
|
-
uri.host.present? ? uri.host : param.to_s
|
66
|
-
end
|
67
23
|
end
|
@@ -1,15 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'circuit_breaker/logger_messages'
|
4
|
+
|
1
5
|
class Circuitbox
|
2
6
|
class CircuitBreaker
|
3
|
-
|
4
|
-
|
7
|
+
include LoggerMessages
|
8
|
+
|
9
|
+
attr_reader :service, :circuit_options, :exceptions,
|
10
|
+
:logger, :circuit_store, :notifier, :time_class, :execution_timer
|
5
11
|
|
6
12
|
DEFAULTS = {
|
7
|
-
sleep_window:
|
13
|
+
sleep_window: 90,
|
8
14
|
volume_threshold: 5,
|
9
15
|
error_threshold: 50,
|
10
|
-
|
11
|
-
|
12
|
-
}
|
16
|
+
time_window: 60
|
17
|
+
}.freeze
|
13
18
|
|
14
19
|
#
|
15
20
|
# Configuration options
|
@@ -17,241 +22,215 @@ class Circuitbox
|
|
17
22
|
# `sleep_window` - seconds to sleep the circuit
|
18
23
|
# `volume_threshold` - number of requests before error rate calculation occurs
|
19
24
|
# `error_threshold` - percentage of failed requests needed to trip circuit
|
20
|
-
# `
|
21
|
-
# `exceptions` - exceptions other than Timeout::Error that count as failures
|
25
|
+
# `exceptions` - exceptions that count as failures
|
22
26
|
# `time_window` - interval of time used to calculate error_rate (in seconds) - default is 60s
|
27
|
+
# `logger` - Logger to use - defaults to Rails.logger if defined, otherwise STDOUT
|
23
28
|
#
|
24
29
|
def initialize(service, options = {})
|
25
|
-
@service = service
|
26
|
-
@circuit_options = options
|
27
|
-
@circuit_store = options.fetch(:cache) { Circuitbox.
|
28
|
-
@
|
30
|
+
@service = service.to_s
|
31
|
+
@circuit_options = DEFAULTS.merge(options)
|
32
|
+
@circuit_store = options.fetch(:cache) { Circuitbox.default_circuit_store }
|
33
|
+
@execution_timer = options.fetch(:execution_timer) { Circuitbox.default_timer }
|
34
|
+
@notifier = options.fetch(:notifier) { Circuitbox.default_notifier }
|
35
|
+
|
36
|
+
if @circuit_options[:timeout_seconds]
|
37
|
+
warn('timeout_seconds was removed in circuitbox 2.0. '\
|
38
|
+
'Check the upgrade guide at https://github.com/yammer/circuitbox')
|
39
|
+
end
|
29
40
|
|
30
|
-
@exceptions = options.fetch(:exceptions)
|
31
|
-
|
41
|
+
@exceptions = options.fetch(:exceptions)
|
42
|
+
raise ArgumentError, 'exceptions need to be an array' unless @exceptions.is_a?(Array)
|
32
43
|
|
33
|
-
@logger =
|
34
|
-
@time_class
|
35
|
-
|
44
|
+
@logger = options.fetch(:logger) { Circuitbox.default_logger }
|
45
|
+
@time_class = options.fetch(:time_class) { Time }
|
46
|
+
@state_change_mutex = Mutex.new
|
47
|
+
check_sleep_window
|
36
48
|
end
|
37
49
|
|
38
50
|
def option_value(name)
|
39
|
-
value = circuit_options
|
51
|
+
value = circuit_options[name]
|
40
52
|
value.is_a?(Proc) ? value.call : value
|
41
53
|
end
|
42
54
|
|
43
|
-
def run
|
44
|
-
@partition = run_options.delete(:partition) # sorry for this hack.
|
45
|
-
|
55
|
+
def run(circuitbox_exceptions: true)
|
46
56
|
if open?
|
47
|
-
|
48
|
-
|
49
|
-
raise Circuitbox::OpenCircuitError.new(service)
|
57
|
+
skipped!
|
58
|
+
raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
|
50
59
|
else
|
51
|
-
|
52
|
-
logger.debug "[CIRCUIT] closed: querying #{service}"
|
60
|
+
logger.debug(circuit_running_message)
|
53
61
|
|
54
62
|
begin
|
55
|
-
response =
|
56
|
-
timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
|
57
|
-
timeout (timeout_seconds) { yield }
|
58
|
-
else
|
63
|
+
response = execution_timer.time(service, notifier, 'execution_time') do
|
59
64
|
yield
|
60
65
|
end
|
61
66
|
|
62
|
-
logger.debug "[CIRCUIT] closed: #{service} querie success"
|
63
67
|
success!
|
64
68
|
rescue *exceptions => exception
|
65
|
-
|
69
|
+
# Other stores could raise an exception that circuitbox is asked to watch.
|
70
|
+
# setting to nil keeps the same behavior as the previous defination of run.
|
71
|
+
response = nil
|
66
72
|
failure!
|
67
|
-
|
68
|
-
raise Circuitbox::ServiceFailureError.new(service, exception)
|
73
|
+
raise Circuitbox::ServiceFailureError.new(service, exception) if circuitbox_exceptions
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
def run(run_options = {})
|
76
|
-
begin
|
77
|
-
run!(run_options, &Proc.new)
|
78
|
-
rescue Circuitbox::Error
|
79
|
-
nil
|
80
|
-
end
|
77
|
+
response
|
81
78
|
end
|
82
79
|
|
83
80
|
def open?
|
84
|
-
|
85
|
-
true
|
86
|
-
elsif passed_volume_threshold? && passed_rate_threshold?
|
87
|
-
true
|
88
|
-
else
|
89
|
-
false
|
90
|
-
end
|
81
|
+
circuit_store.key?(open_storage_key)
|
91
82
|
end
|
92
83
|
|
93
84
|
def error_rate(failures = failure_count, success = success_count)
|
94
85
|
all_count = failures + success
|
95
86
|
return 0.0 unless all_count > 0
|
96
|
-
|
87
|
+
(failures / all_count.to_f) * 100
|
97
88
|
end
|
98
89
|
|
99
90
|
def failure_count
|
100
|
-
circuit_store.load(stat_storage_key(
|
91
|
+
circuit_store.load(stat_storage_key('failure'), raw: true).to_i
|
101
92
|
end
|
102
93
|
|
103
94
|
def success_count
|
104
|
-
circuit_store.load(stat_storage_key(
|
95
|
+
circuit_store.load(stat_storage_key('success'), raw: true).to_i
|
105
96
|
end
|
106
97
|
|
107
98
|
def try_close_next_time
|
108
|
-
circuit_store.delete(
|
99
|
+
circuit_store.delete(open_storage_key)
|
109
100
|
end
|
110
101
|
|
111
102
|
private
|
112
|
-
def open!
|
113
|
-
log_event :open
|
114
|
-
logger.debug "[CIRCUIT] opening #{service} circuit"
|
115
|
-
circuit_store.store(storage_key(:asleep), true, expires: option_value(:sleep_window))
|
116
|
-
half_open!
|
117
|
-
was_open!
|
118
|
-
end
|
119
103
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
end
|
104
|
+
def should_open?
|
105
|
+
failures = failure_count
|
106
|
+
successes = success_count
|
107
|
+
rate = error_rate(failures, successes)
|
125
108
|
|
126
|
-
|
127
|
-
circuit_store.store(storage_key(:was_open), true)
|
109
|
+
passed_volume_threshold?(failures, successes) && passed_rate_threshold?(rate)
|
128
110
|
end
|
129
111
|
|
130
|
-
def
|
131
|
-
|
112
|
+
def passed_volume_threshold?(failures, successes)
|
113
|
+
failures + successes >= option_value(:volume_threshold)
|
132
114
|
end
|
133
|
-
### END
|
134
115
|
|
135
|
-
def
|
136
|
-
|
116
|
+
def passed_rate_threshold?(rate)
|
117
|
+
rate >= option_value(:error_threshold)
|
137
118
|
end
|
138
119
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
120
|
+
def half_open_failure
|
121
|
+
@state_change_mutex.synchronize do
|
122
|
+
return if open? || !half_open?
|
142
123
|
|
143
|
-
|
144
|
-
|
145
|
-
end
|
124
|
+
trip
|
125
|
+
end
|
146
126
|
|
147
|
-
|
148
|
-
|
127
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
128
|
+
# that may be waiting to become unblocked
|
129
|
+
notify_opened
|
149
130
|
end
|
150
131
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
132
|
+
def open!
|
133
|
+
@state_change_mutex.synchronize do
|
134
|
+
return if open?
|
154
135
|
|
155
|
-
|
156
|
-
|
157
|
-
success = success_count
|
158
|
-
rate = error_rate(failures, success)
|
159
|
-
log_metrics(rate, failures, success)
|
160
|
-
rate
|
161
|
-
end
|
136
|
+
trip
|
137
|
+
end
|
162
138
|
|
163
|
-
|
164
|
-
|
165
|
-
|
139
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
140
|
+
# that may be waiting to become unblocked
|
141
|
+
notify_opened
|
166
142
|
end
|
167
143
|
|
168
|
-
def
|
169
|
-
|
144
|
+
def notify_opened
|
145
|
+
notify_event('open')
|
146
|
+
logger.debug(circuit_opened_message)
|
170
147
|
end
|
171
148
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
log_event_to_process(event)
|
149
|
+
def trip
|
150
|
+
circuit_store.store(open_storage_key, true, expires: option_value(:sleep_window))
|
151
|
+
circuit_store.store(half_open_storage_key, true)
|
176
152
|
end
|
177
153
|
|
178
|
-
def
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
def sanitize_options
|
186
|
-
sleep_window = option_value(:sleep_window)
|
187
|
-
time_window = option_value(:time_window)
|
188
|
-
if sleep_window < time_window
|
189
|
-
notifier.new(service,partition).notify_warning("sleep_window:#{sleep_window} is shorter than time_window:#{time_window}, the error_rate could not be reset properly after a sleep. sleep_window as been set to equal time_window.")
|
190
|
-
@circuit_options[:sleep_window] = option_value(:time_window)
|
154
|
+
def close!
|
155
|
+
@state_change_mutex.synchronize do
|
156
|
+
# If the circuit is not open, the half_open key will be deleted from the store
|
157
|
+
# if half_open exists the deleted value is returned and allows us to continue
|
158
|
+
# if half_open doesn't exist nil is returned, causing us to return early
|
159
|
+
return unless !open? && circuit_store.delete(half_open_storage_key)
|
191
160
|
end
|
161
|
+
|
162
|
+
# Running event outside of the synchronize block to allow other threads
|
163
|
+
# that may be waiting to become unblocked
|
164
|
+
notify_event('close')
|
165
|
+
logger.debug(circuit_closed_message)
|
192
166
|
end
|
193
167
|
|
194
|
-
|
195
|
-
|
196
|
-
circuit_store.store(stat_storage_key(:failure), 0, raw: true)
|
168
|
+
def half_open?
|
169
|
+
circuit_store.key?(half_open_storage_key)
|
197
170
|
end
|
198
171
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
else
|
205
|
-
# yes we want a string here, as the underlying stores impement this as a native type.
|
206
|
-
circuit_store.store(key, "1", raw: true)
|
207
|
-
end
|
172
|
+
def success!
|
173
|
+
increment_and_notify_event('success')
|
174
|
+
logger.debug(circuit_success_message)
|
175
|
+
|
176
|
+
close! if half_open?
|
208
177
|
end
|
209
178
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
179
|
+
def failure!
|
180
|
+
increment_and_notify_event('failure')
|
181
|
+
logger.debug(circuit_failure_message)
|
182
|
+
|
183
|
+
if half_open?
|
184
|
+
half_open_failure
|
185
|
+
elsif should_open?
|
186
|
+
open!
|
216
187
|
end
|
217
188
|
end
|
218
189
|
|
219
|
-
|
220
|
-
|
221
|
-
|
190
|
+
def skipped!
|
191
|
+
notify_event('skipped')
|
192
|
+
logger.debug(circuit_skipped_message)
|
222
193
|
end
|
223
194
|
|
224
|
-
|
225
|
-
|
195
|
+
# Send event notification to notifier
|
196
|
+
def notify_event(event)
|
197
|
+
notifier.notify(service, event)
|
226
198
|
end
|
227
199
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
time_window = option_value(:time_window)
|
233
|
-
time - ( time % time_window ) # remove rest of integer division
|
200
|
+
# Increment stat store and send notification
|
201
|
+
def increment_and_notify_event(event)
|
202
|
+
circuit_store.increment(stat_storage_key(event), 1, expires: (option_value(:time_window) * 2))
|
203
|
+
notify_event(event)
|
234
204
|
end
|
235
205
|
|
236
|
-
def
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
"
|
241
|
-
|
242
|
-
|
206
|
+
def check_sleep_window
|
207
|
+
sleep_window = option_value(:sleep_window)
|
208
|
+
time_window = option_value(:time_window)
|
209
|
+
if sleep_window < time_window
|
210
|
+
warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\
|
211
|
+
"the error_rate would not be reset after a sleep."
|
212
|
+
notifier.notify_warning(service, warning_message)
|
213
|
+
warn("Circuit: #{service}, Warning: #{warning_message}")
|
243
214
|
end
|
215
|
+
end
|
244
216
|
|
245
|
-
|
217
|
+
def stat_storage_key(event)
|
218
|
+
"circuits:#{service}:stats:#{align_time_to_window}:#{event}"
|
246
219
|
end
|
247
220
|
|
248
|
-
|
249
|
-
|
221
|
+
# return time representation in seconds
|
222
|
+
def align_time_to_window
|
223
|
+
time = time_class.now.to_i
|
224
|
+
time_window = option_value(:time_window)
|
225
|
+
time - (time % time_window) # remove rest of integer division
|
250
226
|
end
|
251
227
|
|
252
|
-
def
|
253
|
-
|
228
|
+
def open_storage_key
|
229
|
+
@open_storage_key ||= "circuits:#{service}:open"
|
254
230
|
end
|
255
231
|
|
232
|
+
def half_open_storage_key
|
233
|
+
@half_open_storage_key ||= "circuits:#{service}:half_open"
|
234
|
+
end
|
256
235
|
end
|
257
236
|
end
|