circuitbox 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5dd97fd3064e288e32ebae417b38a8e4e95439e4
4
- data.tar.gz: 0b5ede74d70a2bfeffaf8178304afb24e3ac1c61
3
+ metadata.gz: d0fe08dafcf12318b2880700034caf98461a2f3e
4
+ data.tar.gz: f136784e4d4d32ffa0b53580fb64eded3bf4d5ba
5
5
  SHA512:
6
- metadata.gz: 7b6dfa97e8ea99ddf7d3ff06c08460e7820a07461f8c4c9cedf84c012879ba72870ae9f032cc6047e1121024ba64329ae2f9c96929f05e90268569f6279a0c9c
7
- data.tar.gz: 9870a54f290f71a377ae850cc02945d6f76eeec7c5dc3627cf710e67adeb844cc14eaaf4341683508fb9c81e1e8d35f7fc51f841874dbf5163c1bca251b49cf7
6
+ metadata.gz: 0e2870b5f3b3cffd2623f04937be7794fe2ee0085655b65b2533da3ba421e9e7772c4b85593097f300aa4f0984b484f590a8811eb22b667a31e7a8d81c9070d2
7
+ data.tar.gz: c98b3b8e64b4def135717ae7eedb833807d4a484cb6772d284c38c08cb992013a8adc64041ffaddb529fdfecda187287160216467940b63c252a5d3ce765bcb7
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  *.gem
2
+ .vagrant
2
3
  *.rbc
3
4
  .bundle
4
5
  .config
@@ -13,6 +14,7 @@ pkg
13
14
  rdoc
14
15
  spec/reports
15
16
  test/tmp
17
+ test/reports
16
18
  test/version_tmp
17
19
  tmp
18
20
  .idea
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.5
data/.travis.yml CHANGED
@@ -4,3 +4,5 @@ rvm:
4
4
  - 2.0.0
5
5
  - 2.1
6
6
  - 2.2
7
+ before_install:
8
+ - gem install bundler
data/Gemfile CHANGED
@@ -3,3 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in circuitbox.gemspec
4
4
  gemspec
5
5
 
6
+ gem "pry"
data/README.md CHANGED
@@ -57,6 +57,10 @@ class ExampleServiceClient
57
57
  # number of requests within 1 minute before it calculates error rates
58
58
  volume_threshold: 10,
59
59
 
60
+ # the store you want to use to save the circuit state so it can be
61
+ # tracked, this needs to be Moneta compatible, and support increment
62
+ cache: Moneta.new(:Memory)
63
+
60
64
  # exceeding this rate will open the circuit
61
65
  error_threshold: 50,
62
66
 
@@ -75,6 +79,19 @@ Circuitbox.circuit(:yammer, {
75
79
  })
76
80
  ```
77
81
 
82
+ ## Circuit Store (:cache)
83
+
84
+ 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
91
+ KyotoCabinet store across process fails because the store is single writer
92
+ multiple readers, and all circuits sharing the store need to be able to write.
93
+
94
+
78
95
  ## Monitoring & Statistics
79
96
 
80
97
  You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
@@ -136,6 +153,74 @@ end
136
153
 
137
154
  ```
138
155
 
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 save.
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 save, 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 save 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")
222
+ ```
223
+
139
224
  ## Faraday
140
225
 
141
226
  Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
@@ -212,6 +297,8 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
212
297
  ## CHANGELOG
213
298
 
214
299
  ### version next
300
+ - fix URI require missing (https://github.com/yammer/circuitbox/pull/42 @gottfrois)
301
+ - configurable circuitbox store backend via Moneta supporting multi process circuits
215
302
 
216
303
  ### v0.10.4
217
304
  - Issue #39, keep the original backtrace for the wrapped exception around when
data/Rakefile CHANGED
@@ -6,5 +6,13 @@ Rake::TestTask.new do |t|
6
6
  t.test_files = FileList['test/**/*_test.rb']
7
7
  end
8
8
 
9
+ desc "run the circuitbox benchmark scripts"
10
+ task :benchmark do
11
+ benchmark_scripts = FileList.new("./benchmark/*_benchmark.rb")
12
+ benchmark_scripts.each do |script|
13
+ system "bundle exec ruby #{script}"
14
+ end
15
+ end
16
+
9
17
  desc "Run tests"
10
18
  task :default => :test
@@ -0,0 +1,116 @@
1
+ require 'circuitbox'
2
+ require 'benchmark'
3
+ require 'pstore'
4
+ require 'tempfile'
5
+ require 'tmpdir'
6
+ require 'lmdb'
7
+ require 'pry'
8
+
9
+
10
+ class Circuitbox
11
+ class CircuitBreaker
12
+ # silence the circuitbreaker logger
13
+ DEV_NULL = (RUBY_PLATFORM =~ /mswin|mingw/ ? "NUL" : "/dev/null")
14
+ def logger
15
+ @_dev_null_logger ||= Logger.new DEV_NULL
16
+ end
17
+ end
18
+ end
19
+
20
+ def service
21
+ # 10% success rate to make the circuitbreaker flip flop
22
+ if rand(10) <= 0
23
+ "success"
24
+ else
25
+ raise RuntimeError, "fail"
26
+ end
27
+ end
28
+
29
+ def run_flip_flopping circuit
30
+ before = circuit.open?
31
+ circuit.run { service }
32
+ after = circuit.open?
33
+ circuit.try_close_next_time if circuit.open?
34
+ end
35
+
36
+ def without_gc
37
+ GC.start
38
+ GC.disable
39
+ yield
40
+ GC.enable
41
+ end
42
+
43
+ def benchmark_circuitbox_method_with_reporter method, reporter
44
+ without_gc { send(method, reporter) }
45
+ Circuitbox.reset
46
+ end
47
+
48
+ def circuit_with_cache cache
49
+ Circuitbox.circuit :performance, CIRCUIT_OPTIONS.merge(cache: cache)
50
+ end
51
+
52
+ CIRCUIT_OPTIONS = {
53
+ exceptions: [RuntimeError],
54
+ sleep_window: 0,
55
+ time_window: 1
56
+ }
57
+
58
+ RUNS = 10000
59
+
60
+ def circuit_store_memory_one_process reporter
61
+ circuit = circuit_with_cache Moneta.new(:Memory)
62
+
63
+ reporter.report "memory:" do
64
+ RUNS.times { run_flip_flopping circuit }
65
+ end
66
+
67
+ circuit.circuit_store.close
68
+ end
69
+
70
+ def circuit_store_pstore_one_process reporter
71
+ Tempfile.create("test_circuit_store_pstore_one_process") do |dbfile|
72
+ circuit = circuit_with_cache Moneta.new(:PStore, file: dbfile)
73
+
74
+ reporter.report "pstore:" do
75
+ RUNS.times { run_flip_flopping circuit }
76
+ end
77
+
78
+ circuit.circuit_store.close
79
+ end
80
+ end
81
+
82
+ def circuit_store_lmdb_one_process reporter
83
+ Dir.mktmpdir("test_circuit_store_lmdb_one_process") do |dbdir|
84
+ circuit = circuit_with_cache Moneta.new(:LMDB, dir: dbdir, db: "circuitbox_lmdb")
85
+
86
+ reporter.report "lmdb:" do
87
+ RUNS.times { run_flip_flopping circuit }
88
+ end
89
+
90
+ circuit.circuit_store.close
91
+ end
92
+ end
93
+
94
+ def circuit_store_daybreak_one_process reporter
95
+ Tempfile.create("test_circuit_store_daybreak_one_process") do |dbfile|
96
+ circuit = circuit_with_cache Moneta.new(:Daybreak, file: dbfile)
97
+
98
+ reporter.report "daybreak:" do
99
+ RUNS.times { run_flip_flopping circuit }
100
+ end
101
+
102
+ circuit.circuit_store.close
103
+ end
104
+ end
105
+
106
+ Benchmark.bm(8) do |x|
107
+ benchmark_circuitbox_method_with_reporter :circuit_store_memory_one_process, x
108
+ benchmark_circuitbox_method_with_reporter :circuit_store_lmdb_one_process, x
109
+ benchmark_circuitbox_method_with_reporter :circuit_store_pstore_one_process, x
110
+ benchmark_circuitbox_method_with_reporter :circuit_store_daybreak_one_process, x
111
+ end
112
+
113
+
114
+
115
+
116
+
data/circuitbox.gemspec CHANGED
@@ -27,8 +27,12 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "typhoeus"
28
28
  spec.add_development_dependency "timecop"
29
29
  spec.add_development_dependency "faraday"
30
+ spec.add_development_dependency "excon"
30
31
  spec.add_development_dependency "logger"
31
32
  spec.add_development_dependency "bundler-gem_version_tasks"
33
+ spec.add_development_dependency "lmdb"
34
+ spec.add_development_dependency "daybreak"
32
35
 
33
36
  spec.add_dependency "activesupport"
37
+ spec.add_dependency "moneta"
34
38
  end
@@ -118,11 +118,11 @@ class Circuitbox
118
118
  end
119
119
 
120
120
  def failure_count
121
- circuit_store.read(stat_storage_key(:failure)).to_i
121
+ circuit_store.load(stat_storage_key(:failure), raw: true).to_i
122
122
  end
123
123
 
124
124
  def success_count
125
- circuit_store.read(stat_storage_key(:success)).to_i
125
+ circuit_store.load(stat_storage_key(:success), raw: true).to_i
126
126
  end
127
127
 
128
128
  def try_close_next_time
@@ -133,7 +133,7 @@ class Circuitbox
133
133
  def open!
134
134
  log_event :open
135
135
  logger.debug "[CIRCUIT] opening #{service} circuit"
136
- circuit_store.write(storage_key(:asleep), true, expires_in: option_value(:sleep_window).seconds)
136
+ circuit_store.store(storage_key(:asleep), true, expires_in: option_value(:sleep_window).seconds)
137
137
  half_open!
138
138
  was_open!
139
139
  end
@@ -145,24 +145,24 @@ class Circuitbox
145
145
  end
146
146
 
147
147
  def was_open!
148
- circuit_store.write(storage_key(:was_open), true)
148
+ circuit_store.store(storage_key(:was_open), true)
149
149
  end
150
150
 
151
151
  def was_open?
152
- circuit_store.read(storage_key(:was_open)).present?
152
+ circuit_store[storage_key(:was_open)].present?
153
153
  end
154
154
  ### END
155
155
 
156
156
  def half_open!
157
- circuit_store.write(storage_key(:half_open), true)
157
+ circuit_store.store(storage_key(:half_open), true)
158
158
  end
159
159
 
160
160
  def open_flag?
161
- circuit_store.read(storage_key(:asleep)).present?
161
+ circuit_store[storage_key(:asleep)].present?
162
162
  end
163
163
 
164
164
  def half_open?
165
- circuit_store.read(storage_key(:half_open)).present?
165
+ circuit_store[storage_key(:half_open)].present?
166
166
  end
167
167
 
168
168
  def passed_volume_threshold?
@@ -219,16 +219,17 @@ class Circuitbox
219
219
 
220
220
  # When there is a successful response within a stat interval, clear the failures.
221
221
  def clear_failures!
222
- circuit_store.write(stat_storage_key(:failure), 0, raw: true)
222
+ circuit_store.store(stat_storage_key(:failure), 0, raw: true)
223
223
  end
224
224
 
225
225
  # Logs to process memory.
226
226
  def log_event_to_process(event)
227
227
  key = stat_storage_key(event)
228
- if circuit_store.read(key, raw: true)
228
+ if circuit_store.load(key, raw: true)
229
229
  circuit_store.increment(key)
230
230
  else
231
- circuit_store.write(key, 1, raw: true)
231
+ # yes we want a string here, as the underlying stores impement this as a native type.
232
+ circuit_store.store(key, "1", raw: true)
232
233
  end
233
234
  end
234
235
 
@@ -237,7 +238,7 @@ class Circuitbox
237
238
  if stat_store.read(key, raw: true)
238
239
  stat_store.increment(key)
239
240
  else
240
- stat_store.write(key, 1, raw: true)
241
+ stat_store.store(key, 1)
241
242
  end
242
243
  end
243
244
 
@@ -0,0 +1,111 @@
1
+ require 'excon'
2
+ require 'circuitbox'
3
+
4
+ class Circuitbox
5
+ class ExconMiddleware < Excon::Middleware::Base
6
+ class RequestFailed < StandardError; end
7
+
8
+ DEFAULT_EXCEPTIONS = [
9
+ Excon::Errors::Timeout,
10
+ RequestFailed
11
+ ]
12
+
13
+ class NullResponse < Excon::Response
14
+ def initialize(response, exception)
15
+ @original_response = response
16
+ @original_exception = exception
17
+ super(status: 503, response_headers: {})
18
+ end
19
+
20
+ def []=(key, value)
21
+ @data[key] = value
22
+ end
23
+ end
24
+
25
+ attr_reader :opts
26
+
27
+ def initialize(stack, opts = {})
28
+ @stack = stack
29
+ default_options = { open_circuit: lambda { |response| response[:status] >= 400 } }
30
+ @opts = default_options.merge(opts)
31
+ super(stack)
32
+ end
33
+
34
+ def error_call(datum)
35
+ circuit(datum).run!(run_options(datum)) do
36
+ raise RequestFailed
37
+ end
38
+ rescue Circuitbox::Error => exception
39
+ circuit_open_value(datum, datum[:response], exception)
40
+ end
41
+
42
+ def request_call(datum)
43
+ circuit(datum).run!(run_options(datum)) do
44
+ @stack.request_call(datum)
45
+ end
46
+ end
47
+
48
+ def response_call(datum)
49
+ circuit(datum).run!(run_options(datum)) do
50
+ raise RequestFailed if open_circuit?(datum[:response])
51
+ end
52
+ @stack.response_call(datum)
53
+ rescue Circuitbox::Error => exception
54
+ circuit_open_value(datum, datum[:response], exception)
55
+ end
56
+
57
+ def identifier
58
+ @identifier ||= opts.fetch(:identifier, ->(env) { env[:path] })
59
+ end
60
+
61
+ def exceptions
62
+ circuit_breaker_options[:exceptions]
63
+ end
64
+
65
+ private
66
+
67
+ def circuit(datum)
68
+ id = identifier.respond_to?(:call) ? identifier.call(datum) : identifier
69
+ circuitbox.circuit id, circuit_breaker_options
70
+ end
71
+
72
+ def run_options(datum)
73
+ opts.merge(datum)[:circuit_breaker_run_options] || {}
74
+ end
75
+
76
+ def open_circuit?(response)
77
+ opts[:open_circuit].call(response)
78
+ end
79
+
80
+ def circuitbox
81
+ @circuitbox ||= opts.fetch(:circuitbox, Circuitbox)
82
+ end
83
+
84
+ def circuit_open_value(env, response, exception)
85
+ env[:circuit_breaker_default_value] || default_value.call(response, exception)
86
+ end
87
+
88
+ def circuit_breaker_options
89
+ return @circuit_breaker_options if @current_adapter
90
+
91
+ @circuit_breaker_options = opts.fetch(:circuit_breaker_options, {})
92
+ @circuit_breaker_options.merge!(
93
+ exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
94
+ )
95
+ end
96
+
97
+ def default_value
98
+ return @default_value if @default_value
99
+
100
+ default = opts.fetch(:default_value) do
101
+ lambda { |response, exception| NullResponse.new(response, exception) }
102
+ end
103
+
104
+ @default_value = if default.respond_to?(:call)
105
+ default
106
+ else
107
+ lambda { |*| default }
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,3 +1,3 @@
1
1
  class Circuitbox
2
- VERSION='0.10.4'
2
+ VERSION='0.11.0'
3
3
  end
data/lib/circuitbox.rb CHANGED
@@ -1,11 +1,13 @@
1
+ require 'uri'
1
2
  require 'singleton'
2
3
  require 'active_support'
3
4
  require 'logger'
4
5
  require 'timeout'
6
+ require 'moneta'
5
7
 
6
8
  require 'circuitbox/version'
7
9
  require 'circuitbox/memcache_store'
8
- require 'circuitbox/railtie' if defined?(Rails)
10
+ require 'circuitbox/railtie' if defined?(Rails)
9
11
  require 'circuitbox/circuit_breaker'
10
12
  require 'circuitbox/notifier'
11
13
 
@@ -35,7 +37,7 @@ class Circuitbox
35
37
  end
36
38
 
37
39
  def self.circuit_store
38
- self.instance.circuit_store ||= ActiveSupport::Cache::MemoryStore.new
40
+ self.instance.circuit_store ||= Moneta.new(:Memory)
39
41
  end
40
42
 
41
43
  def self.circuit_store=(store)
@@ -0,0 +1,131 @@
1
+ require 'test_helper'
2
+ require 'circuitbox/excon_middleware'
3
+
4
+ class SentialException < StandardError; end
5
+
6
+ class Circuitbox
7
+ class ExconMiddlewareTest < Minitest::Test
8
+
9
+ attr_reader :app
10
+
11
+ def setup
12
+ @app = gimme
13
+ end
14
+
15
+ def test_default_identifier
16
+ env = { path: "sential" }
17
+ assert_equal "sential", ExconMiddleware.new(app).identifier.call(env)
18
+ end
19
+
20
+ def test_overwrite_identifier
21
+ middleware = ExconMiddleware.new(app, identifier: "sential")
22
+ assert_equal middleware.identifier, "sential"
23
+ end
24
+
25
+ def test_overwrite_default_value_generator_lambda
26
+ stub_circuitbox
27
+ env = { path: "path" }
28
+ give(circuitbox).circuit("path", anything) { circuit }
29
+ give(circuit).run!(anything) { raise Circuitbox::Error }
30
+ default_value_generator = lambda { |_, _| :sential }
31
+ middleware = ExconMiddleware.new(app,
32
+ circuitbox: circuitbox,
33
+ default_value: default_value_generator)
34
+ assert_equal :sential, middleware.error_call(env)
35
+ end
36
+
37
+ def test_overwrite_default_value_generator_static_value
38
+ stub_circuitbox
39
+ env = { path: "path" }
40
+ give(circuitbox).circuit("path", anything) { circuit }
41
+ give(circuit).run!(anything) { raise Circuitbox::Error }
42
+ middleware = ExconMiddleware.new(app, circuitbox: circuitbox, default_value: :sential)
43
+ assert_equal :sential, middleware.error_call(env)
44
+ end
45
+
46
+ def test_default_exceptions
47
+ middleware = ExconMiddleware.new(app)
48
+ assert_includes middleware.exceptions, Excon::Errors::Timeout
49
+ assert_includes middleware.exceptions, ExconMiddleware::RequestFailed
50
+ end
51
+
52
+ def test_overridde_success_response
53
+ env = { path: "path", response: { status: 400 } }
54
+ error_response = lambda { |r| r[:status] >= 500 }
55
+ give(app).response_call(anything) { Excon::Response.new(status: 400) }
56
+ mw = ExconMiddleware.new(app, open_circuit: error_response)
57
+ response = mw.response_call(env)
58
+ assert_kind_of Excon::Response, response
59
+ assert_equal response.status, 400
60
+ end
61
+
62
+ def test_default_success_response
63
+ env = { path: "path", response: { status: 400 } }
64
+ app = gimme
65
+ give(app).response_call(anything) { Excon::Response.new(status: 400) }
66
+ response = nil
67
+
68
+ mw = ExconMiddleware.new(app)
69
+ response = mw.response_call(env)
70
+
71
+ assert_kind_of Excon::Response, response
72
+ assert_equal response.status, 503
73
+ end
74
+
75
+ def test_overwrite_exceptions
76
+ middleware = ExconMiddleware.new(app, exceptions: [SentialException])
77
+ assert_includes middleware.exceptions, SentialException
78
+ end
79
+
80
+ def test_pass_circuit_breaker_run_options
81
+ stub_circuitbox
82
+ give(circuit).run!(:sential)
83
+ give(circuitbox).circuit("path", anything) { circuit }
84
+ env = { path: "path", circuit_breaker_run_options: :sential }
85
+ middleware = ExconMiddleware.new(app, circuitbox: circuitbox)
86
+ middleware.request_call(env)
87
+ verify(circuit, 1.times).run!(:sential)
88
+ end
89
+
90
+ def test_pass_circuit_breaker_options
91
+ stub_circuitbox
92
+ env = { path: "path" }
93
+ expected_circuit_breaker_options = {
94
+ sential: :sential,
95
+ exceptions: ExconMiddleware::DEFAULT_EXCEPTIONS
96
+ }
97
+ give(circuitbox).circuit("path", expected_circuit_breaker_options) { circuit }
98
+ options = { circuitbox: circuitbox, circuit_breaker_options: { sential: :sential } }
99
+ middleware = ExconMiddleware.new(app, options)
100
+ middleware.request_call(env)
101
+
102
+ verify(circuitbox, 1.times).circuit("path", expected_circuit_breaker_options)
103
+ end
104
+
105
+ def test_overwrite_circuitbreaker_default_value
106
+ stub_circuitbox
107
+ env = { path: "path", circuit_breaker_default_value: :sential }
108
+ give(circuitbox).circuit("path", anything) { circuit }
109
+ give(circuit).run!(anything) { raise Circuitbox::Error }
110
+ middleware = ExconMiddleware.new(app, circuitbox: circuitbox)
111
+ assert_equal middleware.error_call(env), :sential
112
+ end
113
+
114
+ def test_return_null_response_for_open_circuit
115
+ stub_circuitbox
116
+ env = { path: "path" }
117
+ give(circuit).run!(anything) { raise Circuitbox::Error }
118
+ give(circuitbox).circuit("path", anything) { circuit }
119
+ mw = ExconMiddleware.new(app, circuitbox: circuitbox)
120
+ response = mw.error_call(env)
121
+ assert_kind_of Excon::Response, response
122
+ assert_equal response.status, 503
123
+ end
124
+
125
+ attr_reader :circuitbox, :circuit
126
+ def stub_circuitbox
127
+ @circuitbox = gimme
128
+ @circuit = gimme
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,55 @@
1
+ require "integration_helper"
2
+ require "tempfile"
3
+ require "typhoeus/adapters/faraday"
4
+ require "pstore"
5
+
6
+ class Circuitbox
7
+
8
+ class CrossProcessTest < Minitest::Test
9
+ include IntegrationHelpers
10
+
11
+ attr_reader :connection, :failure_url, :dbfile
12
+
13
+ @@only_once = false
14
+ def setup
15
+ if !@@only_once
16
+ @dbfile = Tempfile.open("circuitbox_test_cross_process")
17
+ end
18
+
19
+ @connection = Faraday.new do |c|
20
+ c.use FaradayMiddleware, identifier: "circuitbox_test_cross_process",
21
+ circuit_breaker_options: { cache: Moneta.new(:PStore, file: dbfile) }
22
+ c.adapter :typhoeus # support in_parallel
23
+ end
24
+ @failure_url = "http://localhost:4713"
25
+
26
+ if !@@only_once
27
+ thread = Thread.new do
28
+ Rack::Handler::WEBrick.run(Proc.new { |env| ["Failure"] },
29
+ Port: 4713,
30
+ AccessLog: [],
31
+ Logger: WEBrick::Log.new(DEV_NULL))
32
+ end
33
+ Minitest.after_run { thread.exit }
34
+ end
35
+ end
36
+
37
+ def teardown
38
+ Circuitbox.reset
39
+ end
40
+
41
+ def test_circuit_opens_cross_process
42
+ # Open the circuit via a different process
43
+ pid = fork do
44
+ con = Faraday.new do |c|
45
+ c.use FaradayMiddleware, identifier: "circuitbox_test_cross_process",
46
+ circuit_breaker_options: { cache: Moneta.new(:PStore, file: dbfile) }
47
+ end
48
+ open_circuit(con)
49
+ end
50
+ Process.wait pid
51
+ response = connection.get(failure_url)
52
+ assert response.original_response.nil?, "opening the circuit from a different process should be respected in the main process"
53
+ end
54
+ end
55
+ end
@@ -40,9 +40,9 @@ class FakeServer
40
40
  end
41
41
 
42
42
  module IntegrationHelpers
43
- def open_circuit
44
- volume_threshold = Circuitbox['test'].option_value(:volume_threshold)
45
- (volume_threshold + 1).times { connection.get(failure_url) }
43
+ def open_circuit(c = connection)
44
+ volume_threshold = Circuitbox::CircuitBreaker::DEFAULTS[:volume_threshold]
45
+ (volume_threshold + 1).times { c.get(failure_url) }
46
46
  end
47
47
  end
48
48
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circuitbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fahim Ferdous
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-03 00:00:00.000000000 Z
11
+ date: 2016-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: excon
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: logger
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +178,34 @@ dependencies:
164
178
  - - ">="
165
179
  - !ruby/object:Gem::Version
166
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: lmdb
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: daybreak
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
167
209
  - !ruby/object:Gem::Dependency
168
210
  name: activesupport
169
211
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +220,20 @@ dependencies:
178
220
  - - ">="
179
221
  - !ruby/object:Gem::Version
180
222
  version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: moneta
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
181
237
  description: A robust circuit breaker that manages failing external services.
182
238
  email:
183
239
  - fahimfmf@gmail.com
@@ -186,17 +242,20 @@ extensions: []
186
242
  extra_rdoc_files: []
187
243
  files:
188
244
  - ".gitignore"
245
+ - ".ruby-version"
189
246
  - ".travis.yml"
190
247
  - Gemfile
191
248
  - LICENSE
192
249
  - README.md
193
250
  - Rakefile
251
+ - benchmark/circuit_store_benchmark.rb
194
252
  - circuitbox.gemspec
195
253
  - lib/circuitbox.rb
196
254
  - lib/circuitbox/circuit_breaker.rb
197
255
  - lib/circuitbox/errors/error.rb
198
256
  - lib/circuitbox/errors/open_circuit_error.rb
199
257
  - lib/circuitbox/errors/service_failure_error.rb
258
+ - lib/circuitbox/excon_middleware.rb
200
259
  - lib/circuitbox/faraday_middleware.rb
201
260
  - lib/circuitbox/memcache_store.rb
202
261
  - lib/circuitbox/notifier.rb
@@ -205,7 +264,9 @@ files:
205
264
  - lib/tasks/circuits.rake
206
265
  - test/circuit_breaker_test.rb
207
266
  - test/circuitbox_test.rb
267
+ - test/excon_middleware_test.rb
208
268
  - test/faraday_middleware_test.rb
269
+ - test/integration/circuitbox_cross_process_open_test.rb
209
270
  - test/integration/faraday_middleware_test.rb
210
271
  - test/integration_helper.rb
211
272
  - test/notifier_test.rb
@@ -238,7 +299,9 @@ summary: A robust circuit breaker that manages failing external services.
238
299
  test_files:
239
300
  - test/circuit_breaker_test.rb
240
301
  - test/circuitbox_test.rb
302
+ - test/excon_middleware_test.rb
241
303
  - test/faraday_middleware_test.rb
304
+ - test/integration/circuitbox_cross_process_open_test.rb
242
305
  - test/integration/faraday_middleware_test.rb
243
306
  - test/integration_helper.rb
244
307
  - test/notifier_test.rb