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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -122
  3. data/lib/circuitbox.rb +12 -56
  4. data/lib/circuitbox/circuit_breaker.rb +133 -154
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +55 -0
  7. data/lib/circuitbox/errors/error.rb +1 -2
  8. data/lib/circuitbox/errors/open_circuit_error.rb +0 -1
  9. data/lib/circuitbox/errors/service_failure_error.rb +2 -1
  10. data/lib/circuitbox/excon_middleware.rb +15 -24
  11. data/lib/circuitbox/faraday_middleware.rb +38 -61
  12. data/lib/circuitbox/memory_store.rb +84 -0
  13. data/lib/circuitbox/memory_store/container.rb +28 -0
  14. data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
  15. data/lib/circuitbox/notifier/active_support.rb +19 -0
  16. data/lib/circuitbox/notifier/null.rb +11 -0
  17. data/lib/circuitbox/timer/monotonic.rb +17 -0
  18. data/lib/circuitbox/timer/null.rb +9 -0
  19. data/lib/circuitbox/timer/simple.rb +13 -0
  20. data/lib/circuitbox/version.rb +1 -1
  21. metadata +81 -149
  22. data/.gitignore +0 -20
  23. data/.ruby-version +0 -1
  24. data/.travis.yml +0 -8
  25. data/Gemfile +0 -6
  26. data/Rakefile +0 -18
  27. data/benchmark/circuit_store_benchmark.rb +0 -114
  28. data/circuitbox.gemspec +0 -38
  29. data/lib/circuitbox/memcache_store.rb +0 -31
  30. data/lib/circuitbox/notifier.rb +0 -34
  31. data/test/circuit_breaker_test.rb +0 -449
  32. data/test/circuitbox_test.rb +0 -54
  33. data/test/excon_middleware_test.rb +0 -131
  34. data/test/faraday_middleware_test.rb +0 -175
  35. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  36. data/test/integration/faraday_middleware_test.rb +0 -78
  37. data/test/integration_helper.rb +0 -48
  38. data/test/notifier_test.rb +0 -21
  39. data/test/service_failure_error_test.rb +0 -30
  40. data/test/test_helper.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f75524d25326132f1bac120effbfa9b130f95bf1
4
- data.tar.gz: ee8a589ff7b55ea95f5e5d119e7c30fe7be4e640
3
+ metadata.gz: 91bcf88d24630e489540fd2c149ce6b468ecdd7e
4
+ data.tar.gz: 8576e094971fb74bb390b2a1508fd6c330644915
5
5
  SHA512:
6
- metadata.gz: 280e82433f43b428b48ee4e5a5a713da46deb723f8df9d886b3e2bf1e7eb922a119c367a706f66d82ee5d731090580773de06030f451ea40784ae544218e1953
7
- data.tar.gz: 1b64ba171cfe40c0956ed38f94cc3e37c3e848a051cc987a2944b7578362ab5f51992757208dea50559cb5338d3fcff9c7fa2537ba9df047bfb05f1dabbc4038
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 it's 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.
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.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 it's own conditions for failure, you can pass an `exceptions` option.
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!` method will throw an exception when the circuit is open or the underlying service fails.
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! do
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
- ## Configuration
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
- # number of requests within 1 minute before it calculates error rates
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
- cache: Moneta.new(:Memory)
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
- # seconds before the circuit times out
68
- timeout_seconds: 1
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. The store is based on
86
- [Moneta](https://github.com/minad/moneta) so there are a lot of stores to choose
87
- from. There are some pre-requisits they need to satisfy so:
88
-
89
- - Need to support increment, this is true for most but not all available stores.
90
- - Need to support concurrent access if you share them. For example sharing a
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
- - `failure_count`
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 returne an error response,
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. (It will also accept a lambda with arity 1 that is
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
- `{ volume_threshold: 10, exceptions: Circuitbox::FaradayMiddleware::DEFAULT_EXCEPTIONS }`
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
- ### version next
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 middlware which were opening the circuit.
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
@@ -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
- require 'circuitbox/version'
9
- require 'circuitbox/memcache_store'
10
- require 'circuitbox/circuit_breaker'
11
- require 'circuitbox/notifier'
12
-
13
- require 'circuitbox/errors/error'
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
- attr_accessor :circuits, :circuit_store
19
- cattr_accessor :configure
12
+ class << self
13
+ include Configuration
20
14
 
21
- def self.instance
22
- @@instance ||= new
23
- end
15
+ def circuit(service_name, options)
16
+ circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
24
17
 
25
- def initialize
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
- def self.circuit_store=(store)
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
- attr_accessor :service, :circuit_options, :exceptions, :partition,
4
- :logger, :circuit_store, :notifier
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: 300,
13
+ sleep_window: 90,
8
14
  volume_threshold: 5,
9
15
  error_threshold: 50,
10
- timeout_seconds: 1,
11
- time_window: 60,
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
- # `timeout_seconds` - seconds until it will timeout the request
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.circuit_store }
28
- @notifier = options.fetch(:notifier_class) { Notifier }
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
- @exceptions = [Timeout::Error] if @exceptions.blank?
41
+ @exceptions = options.fetch(:exceptions)
42
+ raise ArgumentError, 'exceptions need to be an array' unless @exceptions.is_a?(Array)
32
43
 
33
- @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
34
- @time_class = options.fetch(:time_class) { Time }
35
- sanitize_options
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.fetch(name) { DEFAULTS.fetch(name) }
51
+ value = circuit_options[name]
40
52
  value.is_a?(Proc) ? value.call : value
41
53
  end
42
54
 
43
- def run!(run_options = {})
44
- @partition = run_options.delete(:partition) # sorry for this hack.
45
-
55
+ def run(circuitbox_exceptions: true)
46
56
  if open?
47
- logger.debug "[CIRCUIT] open: skipping #{service}"
48
- open! unless open_flag?
49
- raise Circuitbox::OpenCircuitError.new(service)
57
+ skipped!
58
+ raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
50
59
  else
51
- close! if was_open?
52
- logger.debug "[CIRCUIT] closed: querying #{service}"
60
+ logger.debug(circuit_running_message)
53
61
 
54
62
  begin
55
- response = if exceptions.include? Timeout::Error
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
- logger.debug "[CIRCUIT] closed: detected #{service} failure"
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
- open! if half_open?
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
- return response
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
- if open_flag?
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
- failure_count.to_f / all_count.to_f * 100
87
+ (failures / all_count.to_f) * 100
97
88
  end
98
89
 
99
90
  def failure_count
100
- circuit_store.load(stat_storage_key(:failure), raw: true).to_i
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(:success), raw: true).to_i
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(storage_key(:asleep))
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
- ### BEGIN - all this is just here to produce a close notification
121
- def close!
122
- log_event :close
123
- circuit_store.delete(storage_key(:was_open))
124
- end
104
+ def should_open?
105
+ failures = failure_count
106
+ successes = success_count
107
+ rate = error_rate(failures, successes)
125
108
 
126
- def was_open!
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 was_open?
131
- circuit_store[storage_key(:was_open)].present?
112
+ def passed_volume_threshold?(failures, successes)
113
+ failures + successes >= option_value(:volume_threshold)
132
114
  end
133
- ### END
134
115
 
135
- def half_open!
136
- circuit_store.store(storage_key(:half_open), true)
116
+ def passed_rate_threshold?(rate)
117
+ rate >= option_value(:error_threshold)
137
118
  end
138
119
 
139
- def open_flag?
140
- circuit_store[storage_key(:asleep)].present?
141
- end
120
+ def half_open_failure
121
+ @state_change_mutex.synchronize do
122
+ return if open? || !half_open?
142
123
 
143
- def half_open?
144
- circuit_store[storage_key(:half_open)].present?
145
- end
124
+ trip
125
+ end
146
126
 
147
- def passed_volume_threshold?
148
- success_count + failure_count > option_value(:volume_threshold)
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 passed_rate_threshold?
152
- read_and_log_error_rate >= option_value(:error_threshold)
153
- end
132
+ def open!
133
+ @state_change_mutex.synchronize do
134
+ return if open?
154
135
 
155
- def read_and_log_error_rate
156
- failures = failure_count
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
- def success!
164
- log_event :success
165
- circuit_store.delete(storage_key(:half_open))
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 failure!
169
- log_event :failure
144
+ def notify_opened
145
+ notify_event('open')
146
+ logger.debug(circuit_opened_message)
170
147
  end
171
148
 
172
- # Store success/failure/open/close data in memcache
173
- def log_event(event)
174
- notifier.new(service,partition).notify(event)
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 log_metrics(error_rate, failures, successes)
179
- n = notifier.new(service,partition)
180
- n.metric_gauge(:error_rate, error_rate)
181
- n.metric_gauge(:failure_count, failures)
182
- n.metric_gauge(:success_count, successes)
183
- end
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
- # When there is a successful response within a count interval, clear the failures.
195
- def clear_failures!
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
- # Logs to process memory.
200
- def log_event_to_process(event)
201
- key = stat_storage_key(event)
202
- if circuit_store.load(key, raw: true)
203
- circuit_store.increment(key)
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
- # Logs to Memcache.
211
- def log_event_to_stat_store(key)
212
- if stat_store.read(key, raw: true)
213
- stat_store.increment(key)
214
- else
215
- stat_store.store(key, 1)
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
- # For returning stale responses when the circuit is open
220
- def response_key(args)
221
- Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
190
+ def skipped!
191
+ notify_event('skipped')
192
+ logger.debug(circuit_skipped_message)
222
193
  end
223
194
 
224
- def stat_storage_key(event, options = {})
225
- storage_key(:stats, align_time_on_minute, event, options)
195
+ # Send event notification to notifier
196
+ def notify_event(event)
197
+ notifier.notify(service, event)
226
198
  end
227
199
 
228
-
229
- # return time representation in seconds
230
- def align_time_on_minute(time=nil)
231
- time ||= @time_class.now.to_i
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 storage_key(*args)
237
- options = args.extract_options!
238
-
239
- key = if options[:without_partition]
240
- "circuits:#{service}:#{args.join(":")}"
241
- else
242
- "circuits:#{service}:#{partition}:#{args.join(":")}"
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
- return key
217
+ def stat_storage_key(event)
218
+ "circuits:#{service}:stats:#{align_time_to_window}:#{event}"
246
219
  end
247
220
 
248
- def timeout(timeout_seconds, &block)
249
- Timeout::timeout(timeout_seconds) { block.call }
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 self.reset
253
- Circuitbox.reset
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