circuitbox 1.1.1 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +60 -122
- data/lib/circuitbox.rb +15 -52
- data/lib/circuitbox/circuit_breaker.rb +131 -141
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +53 -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 +35 -61
- data/lib/circuitbox/memory_store.rb +76 -0
- data/lib/circuitbox/memory_store/compactor.rb +35 -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 +78 -150
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -9
- data/Gemfile +0 -6
- data/Rakefile +0 -30
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -48
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -436
- data/test/circuitbox_test.rb +0 -45
- 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 -23
- data/test/test_helper.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 98b0453c7aac74192f9ecfe6e1409cd18f11dcc186deac73eeff80b1d8e8b948
|
4
|
+
data.tar.gz: 390bebb8b034c0d40de18d35aaa2785ba40bef371fe282fc0f3a726f176d7483
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9cb247b2544c06f9223457ea52a9c67453c3994a7f441597c1cc3f20d39c4219cbda02c9a944255cbe7d96d8052d2d9fc3b3fe292ae9a7ee0cea6091026396d
|
7
|
+
data.tar.gz: e9aef10565a19824743a88698ea82c6756e2e2d55c5941f1b919c4cf9c8979cc7731d1ce07d3b3d73af6b2adc49e7ae29e75d8bf7a50af6fc472482874490ea8
|
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[:your_service] do
|
13
|
+
Circuitbox[: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,53 +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
|
58
77
|
time_window: 60,
|
59
78
|
|
60
|
-
# number of requests within `time_window` seconds before it calculates error rates
|
79
|
+
# number of requests within `time_window` seconds before it calculates error rates (checked on failures)
|
61
80
|
volume_threshold: 10,
|
62
81
|
|
63
82
|
# the store you want to use to save the circuit state so it can be
|
64
83
|
# tracked, this needs to be Moneta compatible, and support increment
|
65
|
-
|
84
|
+
# this overrides what is set in the global configuration
|
85
|
+
cache: Circuitbox::MemoryStore.new,
|
66
86
|
|
67
|
-
# exceeding this rate will open the circuit
|
87
|
+
# exceeding this rate will open the circuit (checked on failures)
|
68
88
|
error_threshold: 50,
|
69
89
|
|
70
|
-
# seconds before the circuit times out
|
71
|
-
timeout_seconds: 1
|
72
|
-
|
73
90
|
# Logger to use
|
74
|
-
|
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
|
75
104
|
})
|
76
105
|
end
|
77
106
|
end
|
@@ -81,29 +110,26 @@ You can also pass a Proc as an option value which will evaluate each time the ci
|
|
81
110
|
|
82
111
|
```ruby
|
83
112
|
Circuitbox.circuit(:yammer, {
|
84
|
-
sleep_window: Proc.new { Configuration.get(:sleep_window) }
|
113
|
+
sleep_window: Proc.new { Configuration.get(:sleep_window) },
|
114
|
+
exceptions: [Net::ReadTimeout]
|
85
115
|
})
|
86
116
|
```
|
87
117
|
|
88
118
|
## Circuit Store (:cache)
|
89
119
|
|
90
120
|
Holds all the relevant data to trip the circuit if a given number of requests
|
91
|
-
fail in a specified period of time.
|
92
|
-
[Moneta](https://github.com/minad/moneta)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
-
|
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
|
97
129
|
KyotoCabinet store across process fails because the store is single writer
|
98
130
|
multiple readers, and all circuits sharing the store need to be able to write.
|
99
131
|
|
100
132
|
|
101
|
-
## Monitoring & Statistics
|
102
|
-
|
103
|
-
You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
|
104
|
-
Add `PARTITION={partition_key}` to see the circuit for a particular partition.
|
105
|
-
The stats are aggregated into 1 minute intervals.
|
106
|
-
|
107
133
|
## Notifications
|
108
134
|
|
109
135
|
circuitbox use ActiveSupport Notifications.
|
@@ -123,7 +149,7 @@ ActiveSupport::Notifications.subscribe('circuit_close') do |name, start, finish,
|
|
123
149
|
circuit_name = payload[:circuit]
|
124
150
|
Rails.logger.info("Close circuit for: #{circuit_name}")
|
125
151
|
end
|
126
|
-
|
152
|
+
```
|
127
153
|
|
128
154
|
**generate metrics:**
|
129
155
|
|
@@ -142,9 +168,7 @@ end
|
|
142
168
|
|
143
169
|
`payload[:gauge]` can be:
|
144
170
|
|
145
|
-
- `
|
146
|
-
- `success_count`
|
147
|
-
- `error_rate`
|
171
|
+
- `execution_time` # execution time will only be notified when circuit is closed and block is successfully executed.
|
148
172
|
|
149
173
|
**warnings:**
|
150
174
|
in case of misconfiguration, circuitbox will fire a circuitbox_warning
|
@@ -159,77 +183,6 @@ end
|
|
159
183
|
|
160
184
|
```
|
161
185
|
|
162
|
-
### Multi process Circuits
|
163
|
-
|
164
|
-
`circuit_store` is backed by [Moneta](https://github.com/minad/moneta) which
|
165
|
-
supports multiple backends. This can be configured by passing `cache:
|
166
|
-
Moneta.new(:PStore, file: "myfile.store")` to use for example the built in
|
167
|
-
PStore ruby library for persisted store, which can be shared cross process.
|
168
|
-
|
169
|
-
Depending on your requirements different stores can make sense, see the
|
170
|
-
benchmarks and [moneta
|
171
|
-
feature](https://github.com/minad/moneta#backend-feature-matrix) matrix for
|
172
|
-
details.
|
173
|
-
|
174
|
-
```
|
175
|
-
user system total real
|
176
|
-
memory: 1.440000 0.140000 1.580000 ( 1.579244)
|
177
|
-
lmdb: 4.330000 3.280000 7.610000 ( 13.086398)
|
178
|
-
pstore: 23.680000 4.350000 28.030000 ( 28.094312)
|
179
|
-
daybreak: 2.270000 0.450000 2.720000 ( 2.626748)
|
180
|
-
```
|
181
|
-
|
182
|
-
You can run the benchmarks yourself by running `rake benchmark`.
|
183
|
-
|
184
|
-
### Memory
|
185
|
-
|
186
|
-
An in memory store, which is local to the process. This is not threadsafe so it
|
187
|
-
is not useable with multithreaded webservers for example. It is always going to
|
188
|
-
be the fastest option if no multi-process or thread is required, like in
|
189
|
-
development on Webbrick.
|
190
|
-
|
191
|
-
This is the default.
|
192
|
-
|
193
|
-
```ruby
|
194
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:Memory)
|
195
|
-
```
|
196
|
-
|
197
|
-
### LMDB
|
198
|
-
|
199
|
-
An persisted directory backed store, which is thread and multi process safe.
|
200
|
-
depends on the `lmdb` gem. It is slower than Memory or Daybreak, but can be
|
201
|
-
used in multi thread and multi process environments like like Puma.
|
202
|
-
|
203
|
-
```ruby
|
204
|
-
require "lmdb"
|
205
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:LMDB, dir: "./", db: "mydb")
|
206
|
-
```
|
207
|
-
|
208
|
-
### PStore
|
209
|
-
|
210
|
-
An persisted file backed store, which comes with the ruby
|
211
|
-
[stdlib](http://ruby-doc.org/stdlib-2.3.0/libdoc/pstore/rdoc/PStore.html). It
|
212
|
-
has no external dependecies and works on every ruby implementation. Due to it
|
213
|
-
being file backed it is multi process safe, good for development using Unicorn.
|
214
|
-
|
215
|
-
```ruby
|
216
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:PStore, file: "db.pstore")
|
217
|
-
```
|
218
|
-
|
219
|
-
### Daybreak
|
220
|
-
|
221
|
-
Persisted, file backed key value store in pure ruby. It is process safe and
|
222
|
-
outperforms most other stores in circuitbox. This is recommended for production
|
223
|
-
use with Unicorn. It depends on the `daybreak` gem.
|
224
|
-
|
225
|
-
```ruby
|
226
|
-
require "daybreak"
|
227
|
-
Circuitbox.circuit :identifier, cache: Moneta.new(:Daybreak, file: "db.daybreak", expires: true)
|
228
|
-
```
|
229
|
-
|
230
|
-
It is important for the store to have
|
231
|
-
[expires](https://github.com/minad/moneta#backend-feature-matrix) support.
|
232
|
-
|
233
186
|
## Faraday
|
234
187
|
|
235
188
|
Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
@@ -253,13 +206,6 @@ end
|
|
253
206
|
By default the Faraday middleware returns a `503` response when the circuit is
|
254
207
|
open, but this as many other things can be configured via middleware options
|
255
208
|
|
256
|
-
* `exceptions` pass a list of exceptions for the Circuitbreaker to catch,
|
257
|
-
defaults to Timeout and Request failures
|
258
|
-
|
259
|
-
```ruby
|
260
|
-
c.use Circuitbox::FaradayMiddleware, exceptions: [Faraday::Error::TimeoutError]
|
261
|
-
```
|
262
|
-
|
263
209
|
* `default_value` value to return for open circuits, defaults to 503 response
|
264
210
|
wrapping the original response given by the service and stored as
|
265
211
|
`original_response` property of the returned 503, this can be overwritten
|
@@ -268,9 +214,7 @@ c.use Circuitbox::FaradayMiddleware, exceptions: [Faraday::Error::TimeoutError]
|
|
268
214
|
* a `lambda` which is passed the `original_response` and `original_error`.
|
269
215
|
`original_response` will be populated if Faraday returns an error response,
|
270
216
|
`original_error` will be populated if an error was thrown before Faraday
|
271
|
-
returned a response.
|
272
|
-
only passed `original_response`. This use is deprecated and will be removed
|
273
|
-
in the next major version.)
|
217
|
+
returned a response.
|
274
218
|
|
275
219
|
```ruby
|
276
220
|
c.use Circuitbox::FaradayMiddleware, default_value: lambda { |response, error| ... }
|
@@ -282,15 +226,9 @@ c.use Circuitbox::FaradayMiddleware, default_value: lambda { |response, error| .
|
|
282
226
|
c.use Circuitbox::FaradayMiddleware, identifier: "service_name_circuit"
|
283
227
|
```
|
284
228
|
|
285
|
-
* `circuit_breaker_run_options` options passed to the circuit run method, see
|
286
|
-
the main circuitbreaker for those.
|
287
|
-
|
288
|
-
```ruby
|
289
|
-
conn.get("/api", circuit_breaker_run_options: {})
|
290
|
-
```
|
291
|
-
|
292
229
|
* `circuit_breaker_options` options to initialize the circuit with defaults to
|
293
|
-
`{
|
230
|
+
`{ exceptions: Circuitbox::FaradayMiddleware::DEFAULT_EXCEPTIONS }`.
|
231
|
+
Accepts same options as Circuitbox:CircuitBreaker#new
|
294
232
|
|
295
233
|
```ruby
|
296
234
|
c.use Circuitbox::FaradayMiddleware, circuit_breaker_options: {}
|
@@ -359,7 +297,7 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
|
|
359
297
|
|
360
298
|
### v0.10
|
361
299
|
- configuration option for faraday middleware for what should be considered to open the circuit [enrico-scalavio](https://github.com/enrico-scalavino)
|
362
|
-
- 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.
|
363
301
|
- deprecate the __run_option__ `:storage_key`
|
364
302
|
|
365
303
|
### v0.9
|
data/lib/circuitbox.rb
CHANGED
@@ -1,64 +1,27 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'logger'
|
3
|
-
require 'timeout'
|
4
|
-
require 'moneta'
|
5
|
-
require 'active_support/all'
|
6
3
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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'
|
13
10
|
|
14
11
|
class Circuitbox
|
15
|
-
|
16
|
-
|
12
|
+
class << self
|
13
|
+
include Configuration
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def initialize
|
23
|
-
self.instance_eval(&@@configure) if @@configure
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.configure(&block)
|
27
|
-
@@configure = block if block
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.reset
|
31
|
-
@@instance = nil
|
32
|
-
@@configure = nil
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.circuit_store
|
36
|
-
self.instance.circuit_store ||= Moneta.new(:Memory, expires: true)
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.circuit_store=(store)
|
40
|
-
self.instance.circuit_store = store
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.[](service_identifier, options = {})
|
44
|
-
self.circuit(service_identifier, options)
|
45
|
-
end
|
15
|
+
def [](service_name, options = {})
|
16
|
+
circuit(service_name, options)
|
17
|
+
end
|
46
18
|
|
47
|
-
|
48
|
-
|
19
|
+
def circuit(service_name, options = {})
|
20
|
+
circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
|
49
21
|
|
50
|
-
|
51
|
-
self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
|
22
|
+
return circuit unless block_given?
|
52
23
|
|
53
|
-
|
54
|
-
self.instance.circuits[service_name].run { yield }
|
55
|
-
else
|
56
|
-
self.instance.circuits[service_name]
|
24
|
+
circuit.run(circuitbox_exceptions: false) { yield }
|
57
25
|
end
|
58
26
|
end
|
59
|
-
|
60
|
-
def self.parameter_to_service_name(param)
|
61
|
-
uri = URI(param.to_s)
|
62
|
-
uri.host.present? ? uri.host : param.to_s
|
63
|
-
end
|
64
27
|
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,76 +22,64 @@ 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
|
23
27
|
# `logger` - Logger to use - defaults to Rails.logger if defined, otherwise STDOUT
|
24
28
|
#
|
25
29
|
def initialize(service, options = {})
|
26
|
-
@service = service
|
27
|
-
@circuit_options = options
|
28
|
-
@circuit_store = options.fetch(:cache) { Circuitbox.
|
29
|
-
@
|
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
|
30
40
|
|
31
|
-
@exceptions = options.fetch(:exceptions)
|
32
|
-
|
41
|
+
@exceptions = options.fetch(:exceptions)
|
42
|
+
raise ArgumentError, 'exceptions need to be an array' unless @exceptions.is_a?(Array)
|
33
43
|
|
34
|
-
@logger = options.fetch(:logger) {
|
35
|
-
@time_class
|
36
|
-
|
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
|
37
48
|
end
|
38
49
|
|
39
50
|
def option_value(name)
|
40
|
-
value = circuit_options
|
51
|
+
value = circuit_options[name]
|
41
52
|
value.is_a?(Proc) ? value.call : value
|
42
53
|
end
|
43
54
|
|
44
|
-
def run
|
45
|
-
|
46
|
-
|
47
|
-
if open?
|
48
|
-
logger.debug "[CIRCUIT] open: skipping #{service}"
|
49
|
-
open! unless open_flag?
|
55
|
+
def run(circuitbox_exceptions: true)
|
56
|
+
if open_flag?
|
50
57
|
skipped!
|
51
|
-
raise Circuitbox::OpenCircuitError.new(service)
|
58
|
+
raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
|
52
59
|
else
|
53
|
-
|
54
|
-
logger.debug "[CIRCUIT] closed: querying #{service}"
|
60
|
+
logger.debug(circuit_running_message)
|
55
61
|
|
56
62
|
begin
|
57
|
-
response =
|
58
|
-
timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
|
59
|
-
timeout (timeout_seconds) { yield }
|
60
|
-
else
|
63
|
+
response = execution_timer.time(service, notifier, 'execution_time') do
|
61
64
|
yield
|
62
65
|
end
|
63
66
|
|
64
|
-
logger.debug "[CIRCUIT] closed: #{service} querie success"
|
65
67
|
success!
|
66
68
|
rescue *exceptions => exception
|
67
|
-
|
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
|
68
72
|
failure!
|
69
|
-
|
70
|
-
raise Circuitbox::ServiceFailureError.new(service, exception)
|
73
|
+
raise Circuitbox::ServiceFailureError.new(service, exception) if circuitbox_exceptions
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
74
|
-
|
75
|
-
end
|
76
|
-
|
77
|
-
def run(run_options = {})
|
78
|
-
begin
|
79
|
-
run!(run_options, &Proc.new)
|
80
|
-
rescue Circuitbox::Error
|
81
|
-
nil
|
82
|
-
end
|
77
|
+
response
|
83
78
|
end
|
84
79
|
|
85
80
|
def open?
|
86
81
|
if open_flag?
|
87
82
|
true
|
88
|
-
elsif passed_volume_threshold? && passed_rate_threshold?
|
89
|
-
true
|
90
83
|
else
|
91
84
|
false
|
92
85
|
end
|
@@ -95,160 +88,157 @@ class Circuitbox
|
|
95
88
|
def error_rate(failures = failure_count, success = success_count)
|
96
89
|
all_count = failures + success
|
97
90
|
return 0.0 unless all_count > 0
|
98
|
-
|
91
|
+
(failures / all_count.to_f) * 100
|
99
92
|
end
|
100
93
|
|
101
94
|
def failure_count
|
102
|
-
circuit_store.load(stat_storage_key(
|
95
|
+
circuit_store.load(stat_storage_key('failure'), raw: true).to_i
|
103
96
|
end
|
104
97
|
|
105
98
|
def success_count
|
106
|
-
circuit_store.load(stat_storage_key(
|
99
|
+
circuit_store.load(stat_storage_key('success'), raw: true).to_i
|
107
100
|
end
|
108
101
|
|
109
102
|
def try_close_next_time
|
110
|
-
circuit_store.delete(
|
103
|
+
circuit_store.delete(open_storage_key)
|
111
104
|
end
|
112
105
|
|
113
106
|
private
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
107
|
+
|
108
|
+
def should_open?
|
109
|
+
failures = failure_count
|
110
|
+
successes = success_count
|
111
|
+
rate = error_rate(failures, successes)
|
112
|
+
|
113
|
+
passed_volume_threshold?(failures, successes) && passed_rate_threshold?(rate)
|
120
114
|
end
|
121
115
|
|
122
|
-
|
123
|
-
|
124
|
-
log_event :close
|
125
|
-
circuit_store.delete(storage_key(:was_open))
|
116
|
+
def passed_volume_threshold?(failures, successes)
|
117
|
+
failures + successes >= option_value(:volume_threshold)
|
126
118
|
end
|
127
119
|
|
128
|
-
def
|
129
|
-
|
120
|
+
def passed_rate_threshold?(rate)
|
121
|
+
rate >= option_value(:error_threshold)
|
130
122
|
end
|
131
123
|
|
132
|
-
def
|
133
|
-
|
124
|
+
def half_open_failure
|
125
|
+
@state_change_mutex.synchronize do
|
126
|
+
return if open_flag? || !half_open?
|
127
|
+
|
128
|
+
trip
|
129
|
+
end
|
130
|
+
|
131
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
132
|
+
# that may be waiting to become unblocked
|
133
|
+
notify_opened
|
134
134
|
end
|
135
|
-
### END
|
136
135
|
|
137
|
-
def
|
138
|
-
|
136
|
+
def open!
|
137
|
+
@state_change_mutex.synchronize do
|
138
|
+
return if open_flag?
|
139
|
+
|
140
|
+
trip
|
141
|
+
end
|
142
|
+
|
143
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
144
|
+
# that may be waiting to become unblocked
|
145
|
+
notify_opened
|
139
146
|
end
|
140
147
|
|
141
|
-
def
|
142
|
-
|
148
|
+
def notify_opened
|
149
|
+
notify_event('open')
|
150
|
+
logger.debug(circuit_opened_message)
|
143
151
|
end
|
144
152
|
|
145
|
-
def
|
146
|
-
circuit_store
|
153
|
+
def trip
|
154
|
+
circuit_store.store(open_storage_key, true, expires: option_value(:sleep_window))
|
155
|
+
circuit_store.store(half_open_storage_key, true)
|
147
156
|
end
|
148
157
|
|
149
|
-
def
|
150
|
-
|
158
|
+
def close!
|
159
|
+
@state_change_mutex.synchronize do
|
160
|
+
# If the circuit is not open, the half_open key will be deleted from the store
|
161
|
+
# if half_open exists the deleted value is returned and allows us to continue
|
162
|
+
# if half_open doesn't exist nil is returned, causing us to return early
|
163
|
+
return unless !open_flag? && circuit_store.delete(half_open_storage_key)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Running event outside of the synchronize block to allow other threads
|
167
|
+
# that may be waiting to become unblocked
|
168
|
+
notify_event('close')
|
169
|
+
logger.debug(circuit_closed_message)
|
151
170
|
end
|
152
171
|
|
153
|
-
def
|
154
|
-
|
172
|
+
def open_flag?
|
173
|
+
circuit_store.key?(open_storage_key)
|
155
174
|
end
|
156
175
|
|
157
|
-
def
|
158
|
-
|
159
|
-
success = success_count
|
160
|
-
rate = error_rate(failures, success)
|
161
|
-
log_metrics(rate, failures, success)
|
162
|
-
rate
|
176
|
+
def half_open?
|
177
|
+
circuit_store.key?(half_open_storage_key)
|
163
178
|
end
|
164
179
|
|
165
180
|
def success!
|
166
|
-
|
167
|
-
|
181
|
+
notify_and_increment_event('success')
|
182
|
+
logger.debug(circuit_success_message)
|
183
|
+
|
184
|
+
close! if half_open?
|
168
185
|
end
|
169
186
|
|
170
187
|
def failure!
|
171
|
-
|
188
|
+
notify_and_increment_event('failure')
|
189
|
+
logger.debug(circuit_failure_message)
|
190
|
+
|
191
|
+
if half_open?
|
192
|
+
half_open_failure
|
193
|
+
elsif should_open?
|
194
|
+
open!
|
195
|
+
end
|
172
196
|
end
|
173
197
|
|
174
198
|
def skipped!
|
175
|
-
|
199
|
+
notify_event('skipped')
|
200
|
+
logger.debug(circuit_skipped_message)
|
176
201
|
end
|
177
202
|
|
178
|
-
#
|
179
|
-
def
|
180
|
-
notifier.
|
181
|
-
log_event_to_process(event)
|
203
|
+
# Send event notification to notifier
|
204
|
+
def notify_event(event)
|
205
|
+
notifier.notify(service, event)
|
182
206
|
end
|
183
207
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
n.metric_gauge(:success_count, successes)
|
208
|
+
# Send notification and increment stat store
|
209
|
+
def notify_and_increment_event(event)
|
210
|
+
notify_event(event)
|
211
|
+
circuit_store.increment(stat_storage_key(event), 1, expires: (option_value(:time_window) * 2))
|
189
212
|
end
|
190
213
|
|
191
|
-
def
|
214
|
+
def check_sleep_window
|
192
215
|
sleep_window = option_value(:sleep_window)
|
193
216
|
time_window = option_value(:time_window)
|
194
217
|
if sleep_window < time_window
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
# When there is a successful response within a count interval, clear the failures.
|
201
|
-
def clear_failures!
|
202
|
-
circuit_store.store(stat_storage_key(:failure), 0, raw: true)
|
203
|
-
end
|
204
|
-
|
205
|
-
# Logs to process memory.
|
206
|
-
def log_event_to_process(event)
|
207
|
-
key = stat_storage_key(event)
|
208
|
-
if circuit_store.load(key, raw: true)
|
209
|
-
circuit_store.increment(key)
|
210
|
-
else
|
211
|
-
# yes we want a string here, as the underlying stores impement this as a native type.
|
212
|
-
circuit_store.store(key, "1", raw: true)
|
218
|
+
warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\
|
219
|
+
"the error_rate would not be reset after a sleep."
|
220
|
+
notifier.notify_warning(service, warning_message)
|
221
|
+
warn("Circuit: #{service}, Warning: #{warning_message}")
|
213
222
|
end
|
214
223
|
end
|
215
224
|
|
216
|
-
|
217
|
-
|
218
|
-
Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
|
219
|
-
end
|
220
|
-
|
221
|
-
def stat_storage_key(event, options = {})
|
222
|
-
storage_key(:stats, align_time_on_minute, event, options)
|
225
|
+
def stat_storage_key(event)
|
226
|
+
"circuits:#{service}:stats:#{align_time_to_window}:#{event}"
|
223
227
|
end
|
224
228
|
|
225
|
-
|
226
229
|
# return time representation in seconds
|
227
|
-
def
|
228
|
-
time
|
230
|
+
def align_time_to_window
|
231
|
+
time = time_class.now.to_i
|
229
232
|
time_window = option_value(:time_window)
|
230
|
-
time - (
|
231
|
-
end
|
232
|
-
|
233
|
-
def storage_key(*args)
|
234
|
-
options = args.extract_options!
|
235
|
-
|
236
|
-
key = if options[:without_partition]
|
237
|
-
"circuits:#{service}:#{args.join(":")}"
|
238
|
-
else
|
239
|
-
"circuits:#{service}:#{partition}:#{args.join(":")}"
|
240
|
-
end
|
241
|
-
|
242
|
-
return key
|
233
|
+
time - (time % time_window) # remove rest of integer division
|
243
234
|
end
|
244
235
|
|
245
|
-
def
|
246
|
-
|
236
|
+
def open_storage_key
|
237
|
+
@open_storage_key ||= "circuits:#{service}:open"
|
247
238
|
end
|
248
239
|
|
249
|
-
def
|
250
|
-
|
240
|
+
def half_open_storage_key
|
241
|
+
@half_open_storage_key ||= "circuits:#{service}:half_open"
|
251
242
|
end
|
252
|
-
|
253
243
|
end
|
254
244
|
end
|