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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +60 -122
  3. data/lib/circuitbox.rb +15 -52
  4. data/lib/circuitbox/circuit_breaker.rb +131 -141
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +53 -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 +35 -61
  12. data/lib/circuitbox/memory_store.rb +76 -0
  13. data/lib/circuitbox/memory_store/compactor.rb +35 -0
  14. data/lib/circuitbox/memory_store/container.rb +28 -0
  15. data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
  16. data/lib/circuitbox/notifier/active_support.rb +19 -0
  17. data/lib/circuitbox/notifier/null.rb +11 -0
  18. data/lib/circuitbox/timer/monotonic.rb +17 -0
  19. data/lib/circuitbox/timer/null.rb +9 -0
  20. data/lib/circuitbox/timer/simple.rb +13 -0
  21. data/lib/circuitbox/version.rb +1 -1
  22. metadata +78 -150
  23. data/.gitignore +0 -20
  24. data/.ruby-version +0 -1
  25. data/.travis.yml +0 -9
  26. data/Gemfile +0 -6
  27. data/Rakefile +0 -30
  28. data/benchmark/circuit_store_benchmark.rb +0 -114
  29. data/circuitbox.gemspec +0 -48
  30. data/lib/circuitbox/notifier.rb +0 -34
  31. data/test/circuit_breaker_test.rb +0 -436
  32. data/test/circuitbox_test.rb +0 -45
  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 -23
  40. data/test/test_helper.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6b50bdb4f6ad77ee69e5f67bc61f9ae5e70e32b4
4
- data.tar.gz: 86c89e7e0e10bd67349f76e3fee069218b5bb01a
2
+ SHA256:
3
+ metadata.gz: 98b0453c7aac74192f9ecfe6e1409cd18f11dcc186deac73eeff80b1d8e8b948
4
+ data.tar.gz: 390bebb8b034c0d40de18d35aaa2785ba40bef371fe282fc0f3a726f176d7483
5
5
  SHA512:
6
- metadata.gz: fb6c0076a6689dc2fcc153f11f5060347a544a678b2a74304fa53a1659fa5456037d9642b34d90ce862f48edbc8215dcc0187549d91cface993757afc8413594
7
- data.tar.gz: 6cdecb23e5598ae81f621099d5329fc1d133f9acf5f9163a168ba8645615d777441b15045c51e0d16c1c24df56e9210d4bc235fcc6c158bb1ededaced99c1fdb
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 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[: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,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!` 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
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
- cache: Moneta.new(:Memory)
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
- logger: Logger.new(STDOUT)
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. The store is based on
92
- [Moneta](https://github.com/minad/moneta) so there are a lot of stores to choose
93
- from. There are some pre-requisits they need to satisfy so:
94
-
95
- - Need to support increment, this is true for most but not all available stores.
96
- - 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
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
- - `failure_count`
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. (It will also accept a lambda with arity 1 that is
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
- `{ volume_threshold: 10, exceptions: Circuitbox::FaradayMiddleware::DEFAULT_EXCEPTIONS }`
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 middlware which were opening the circuit.
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
- require 'circuitbox/version'
8
- require 'circuitbox/circuit_breaker'
9
- require 'circuitbox/notifier'
10
- require 'circuitbox/errors/error'
11
- require 'circuitbox/errors/open_circuit_error'
12
- 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'
13
10
 
14
11
  class Circuitbox
15
- attr_accessor :circuits, :circuit_store
16
- cattr_accessor :configure
12
+ class << self
13
+ include Configuration
17
14
 
18
- def self.instance
19
- @@instance ||= new
20
- end
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
- def self.circuit(service_identifier, options = {})
48
- service_name = self.parameter_to_service_name(service_identifier)
19
+ def circuit(service_name, options = {})
20
+ circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
49
21
 
50
- self.instance.circuits ||= Hash.new
51
- self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
22
+ return circuit unless block_given?
52
23
 
53
- if block_given?
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
- 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,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
- # `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
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.circuit_store }
29
- @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
30
40
 
31
- @exceptions = options.fetch(:exceptions) { [] }
32
- @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)
33
43
 
34
- @logger = options.fetch(:logger) { defined?(Rails) ? Rails.logger : Logger.new(STDOUT) }
35
- @time_class = options.fetch(:time_class) { Time }
36
- 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
37
48
  end
38
49
 
39
50
  def option_value(name)
40
- value = circuit_options.fetch(name) { DEFAULTS.fetch(name) }
51
+ value = circuit_options[name]
41
52
  value.is_a?(Proc) ? value.call : value
42
53
  end
43
54
 
44
- def run!(run_options = {})
45
- @partition = run_options.delete(:partition) # sorry for this hack.
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
- close! if was_open?
54
- logger.debug "[CIRCUIT] closed: querying #{service}"
60
+ logger.debug(circuit_running_message)
55
61
 
56
62
  begin
57
- response = if exceptions.include? Timeout::Error
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
- 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
68
72
  failure!
69
- open! if half_open?
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
- return response
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
- failure_count.to_f / all_count.to_f * 100
91
+ (failures / all_count.to_f) * 100
99
92
  end
100
93
 
101
94
  def failure_count
102
- circuit_store.load(stat_storage_key(:failure), raw: true).to_i
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(:success), raw: true).to_i
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(storage_key(:asleep))
103
+ circuit_store.delete(open_storage_key)
111
104
  end
112
105
 
113
106
  private
114
- def open!
115
- log_event :open
116
- logger.debug "[CIRCUIT] opening #{service} circuit"
117
- circuit_store.store(storage_key(:asleep), true, expires: option_value(:sleep_window))
118
- half_open!
119
- was_open!
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
- ### BEGIN - all this is just here to produce a close notification
123
- def close!
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 was_open!
129
- circuit_store.store(storage_key(:was_open), true)
120
+ def passed_rate_threshold?(rate)
121
+ rate >= option_value(:error_threshold)
130
122
  end
131
123
 
132
- def was_open?
133
- circuit_store[storage_key(:was_open)].present?
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 half_open!
138
- circuit_store.store(storage_key(:half_open), true)
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 open_flag?
142
- circuit_store[storage_key(:asleep)].present?
148
+ def notify_opened
149
+ notify_event('open')
150
+ logger.debug(circuit_opened_message)
143
151
  end
144
152
 
145
- def half_open?
146
- circuit_store[storage_key(:half_open)].present?
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 passed_volume_threshold?
150
- success_count + failure_count > option_value(:volume_threshold)
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 passed_rate_threshold?
154
- read_and_log_error_rate >= option_value(:error_threshold)
172
+ def open_flag?
173
+ circuit_store.key?(open_storage_key)
155
174
  end
156
175
 
157
- def read_and_log_error_rate
158
- failures = failure_count
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
- log_event :success
167
- circuit_store.delete(storage_key(:half_open))
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
- log_event :failure
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
- log_event :skipped
199
+ notify_event('skipped')
200
+ logger.debug(circuit_skipped_message)
176
201
  end
177
202
 
178
- # Store success/failure/open/close data in memcache
179
- def log_event(event)
180
- notifier.new(service,partition).notify(event)
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
- def log_metrics(error_rate, failures, successes)
185
- n = notifier.new(service,partition)
186
- n.metric_gauge(:error_rate, error_rate)
187
- n.metric_gauge(:failure_count, failures)
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 sanitize_options
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
- 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.")
196
- @circuit_options[:sleep_window] = option_value(:time_window)
197
- end
198
- end
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
- # For returning stale responses when the circuit is open
217
- def response_key(args)
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 align_time_on_minute(time=nil)
228
- time ||= @time_class.now.to_i
230
+ def align_time_to_window
231
+ time = time_class.now.to_i
229
232
  time_window = option_value(:time_window)
230
- time - ( time % time_window ) # remove rest of integer division
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 timeout(timeout_seconds, &block)
246
- Timeout::timeout(timeout_seconds) { block.call }
236
+ def open_storage_key
237
+ @open_storage_key ||= "circuits:#{service}:open"
247
238
  end
248
239
 
249
- def self.reset
250
- Circuitbox.reset
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