circuitbox 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Y2JjODc0MzI1ZWFmMzk3NWExN2VjMTVmYTU5MDg1ZjBlMmI1YTc3OA==
5
+ data.tar.gz: !binary |-
6
+ MTJiMjk1YWFkZThhY2FjZDdmYzA1MjUwYWY3OGNiNzE0N2JjMzM5OQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OTczMWYyMTZiZjQyZWQ0MmZjNTZmNjM4ZDU5MjI1NjA1YjQzYTZmMTY0OGY1
10
+ Y2EyYjIwNGZmYTEzMjY4MjVhYTIyODAwNDE5ZTE1Nzk2MWRiNWMyZDI3Yzgx
11
+ NzM4OWRhMWY1MWU0OTQzMWZiNmU4M2U3NzdiODgwOTE3MDllMmY=
12
+ data.tar.gz: !binary |-
13
+ ODA5NDcxMjk0NDI1Mzc1ODU1OWQyMDQ5ZTA5ZWE4NWRhNDczNzFmOWFkMmY4
14
+ ZDBlNGYzYWEzNDgzMGY0YzM4YzZiZTNjMzI4ZjE2MGFmYzYyMTJiYmViNGEx
15
+ NDE5YWE1N2VmNjFkZWViNDI5MTdhZWM4ODFjMzRjNDNmNDM3Y2U=
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in circuitbox.gemspec
4
+ gemspec
5
+
6
+ gem 'activesupport'
7
+ gem 'logger'
8
+ gem 'faraday'
9
+
10
+ group :test do
11
+ gem 'timecop'
12
+ gem 'mocha'
13
+ gem 'minitest'
14
+ gem 'guard-minitest'
15
+ gem 'gimme'
16
+ gem 'terminal-notifier-guard'
17
+ end
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :minitest do
5
+ # with Minitest::Unit
6
+ watch(%r{^test/(.*)_test\.rb$})
7
+ watch(%r{^lib/circuitbox/([^/]+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
8
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2014. Microsoft Corporation. All Rights Reserved
2
+ All rights reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
12
+ IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR
13
+ PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
14
+
15
+ See the Apache Version 2.0 License for specific language governing
16
+ permissions and limitations under the License.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Circuitbox
2
+
3
+ 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 and reduces the resources your application uses when calling failing services.
4
+
5
+ Resources about the circuit breaker pattern:
6
+ * [http://martinfowler.com/bliki/CircuitBreaker.html](http://martinfowler.com/bliki/CircuitBreaker.html)
7
+ * [https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker](https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker)
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ Circuitbox[:your_service] do
13
+ Net::HTTP.get URI('http://example.com/api/messages')
14
+ end
15
+ ```
16
+
17
+ Circuitbox will return nil for failed requests and open circuits.
18
+ If your HTTP client has it's own conditions for failure, you can pass an `exceptions` option.
19
+
20
+ ```ruby
21
+ class ExampleServiceClient
22
+ def circuit
23
+ Circuitbox.circuit(:yammer, exceptions: [Zephyr::FailedRequest])
24
+ end
25
+
26
+ def http_get
27
+ circuit.run do
28
+ Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
29
+ end
30
+ end
31
+ end
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ ```ruby
37
+ class ExampleServiceClient
38
+ def circuit
39
+ Circuitbox.circuit(:your_service, {
40
+ exceptions: [YourCustomException],
41
+
42
+ # seconds the circuit stays open once it has passed the error threshold
43
+ sleep_window: 300,
44
+
45
+ # number of requests within 1 minute before it calculates error rates
46
+ volume_threshold: 10,
47
+
48
+ # exceeding this rate will open the circuit
49
+ error_threshold: 50,
50
+
51
+ # seconds before the circuit times out
52
+ timeout_seconds: 1
53
+ })
54
+ end
55
+ end
56
+ ```
57
+
58
+ You can also pass a Proc as an option value which will evaluate each time the circuit breaker is used. This lets you configure the circuit breaker without having to restart the processes.
59
+
60
+ ```ruby
61
+ Circuitbox.circuit(:yammer, {
62
+ sleep_window: Proc.new { Configuration.get(:sleep_window) }
63
+ })
64
+ ```
65
+
66
+ ## Monitoring & Statistics
67
+
68
+ You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
69
+ Add `PARTITION={partition_key}` to see the circuit for a particular partition.
70
+ The stats are aggregated into 1 minute intervals.
71
+
72
+ ## Faraday (Caveat: Open circuits return a nil response object)
73
+
74
+ Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
75
+
76
+ ```ruby
77
+ require 'faraday'
78
+ require 'circuitbox/faraday_middleware'
79
+
80
+ conn = Faraday::Connection.new(:url => "http://example.com") do |builder|
81
+ builder.use Circuitbox::FaradayMiddleware
82
+ end
83
+
84
+ if response = conn.get("/api")
85
+ # success
86
+ else
87
+ # failure or open circuit
88
+ end
89
+ ```
90
+
91
+ ## TODO
92
+ * Fix Faraday integration to return a Faraday response object
93
+ * Split stats into it's own repository
94
+ * Circuit Breaker should raise an exception by default instead of returning nil
95
+ * Refactor to use single state variable
96
+ * Fix the partition hack
97
+ * Integrate with Breakerbox/Hystrix
98
+
99
+ ## Installation
100
+
101
+ Add this line to your application's Gemfile:
102
+
103
+ gem 'circuitbox'
104
+
105
+ And then execute:
106
+
107
+ $ bundle
108
+
109
+ Or install it yourself as:
110
+
111
+ $ gem install circuitbox
112
+
113
+ ## Contributing
114
+
115
+ 1. Fork it
116
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
117
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
118
+ 4. Push to the branch (`git push origin my-new-feature`)
119
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+ require 'bundler/yammer_gem_tasks'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'circuitbox/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "circuitbox"
8
+ spec.version = Circuitbox::VERSION
9
+ spec.authors = ["Fahim Ferdous"]
10
+ spec.email = ["fahimfmf@gmail.com"]
11
+ spec.description = %q{A robust circuit breaker that manages failing external services.}
12
+ spec.summary = %q{A robust circuit breaker that manages failing external services.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.4"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_dependency "activesupport"
25
+ end
@@ -0,0 +1,235 @@
1
+ class Circuitbox
2
+ class CircuitBreaker
3
+ attr_accessor :service, :circuit_options, :exceptions, :partition,
4
+ :logger, :stat_store, :circuit_store, :notifier
5
+
6
+ DEFAULTS = {
7
+ sleep_window: 300,
8
+ volume_threshold: 5,
9
+ error_threshold: 50,
10
+ timeout_seconds: 1
11
+ }
12
+
13
+ #
14
+ # Configuration options
15
+ #
16
+ # `sleep_window` - seconds to sleep the circuit
17
+ # `volume_threshold` - number of requests before error rate calculation occurs
18
+ # `error_threshold` - percentage of failed requests needed to trip circuit
19
+ # `timeout_seconds` - seconds until it will timeout the request
20
+ # `exceptions` - exceptions other than Timeout::Error that count as failures
21
+ #
22
+ def initialize(service, options = {})
23
+ @service = service
24
+ @circuit_options = options
25
+ @circuit_store = options.fetch(:cache) { Circuitbox.circuit_store }
26
+ @notifier = Circuitbox::Notifier
27
+
28
+ @exceptions = options.fetch(:exceptions) { [] }
29
+ @exceptions = [Timeout::Error] if @exceptions.blank?
30
+
31
+ @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
32
+ @stat_store = options.fetch(:stat_store) { Circuitbox.stat_store }
33
+ end
34
+
35
+ def option_value(name)
36
+ value = circuit_options.fetch(name) { DEFAULTS.fetch(name) }
37
+ value.is_a?(Proc) ? value.call : value
38
+ end
39
+
40
+ def run(run_options = {}, &block)
41
+ @partition = run_options.delete(:partition) # sorry for this hack.
42
+ cache_key = run_options.delete(:storage_key)
43
+
44
+ if open?
45
+ logger.debug "[CIRCUIT] open: skipping #{service}"
46
+ response = nil
47
+ open! unless open_flag?
48
+ else
49
+ logger.debug "[CIRCUIT] closed: querying #{service}"
50
+
51
+ begin
52
+ response = if exceptions.include? Timeout::Error
53
+ timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
54
+ timeout (timeout_seconds) { yield }
55
+ else
56
+ yield
57
+ end
58
+
59
+ logger.debug "[CIRCUIT] closed: #{service} querie success"
60
+ cache_response(cache_key, response) if cache_key
61
+ success!
62
+ rescue *exceptions => exception
63
+ logger.debug "[CIRCUIT] closed: detected #{service} failure"
64
+ failure!
65
+ response = cache_key ? get_cached_response(cache_key) : nil
66
+ open! if half_open?
67
+ end
68
+ end
69
+
70
+ return response
71
+ end
72
+
73
+ def open?
74
+ if open_flag?
75
+ true
76
+ elsif passed_volume_threshold? && passed_rate_threshold?
77
+ true
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ def stats(partition)
84
+ @partition = partition
85
+ options = { without_partition: @partition.blank? }
86
+
87
+ stats = []
88
+ end_time = Time.now
89
+ hour = 48.hours.ago.change(min: 0, sec: 0)
90
+ while hour <= end_time
91
+ time_object = hour
92
+
93
+ 60.times do |i|
94
+ time = time_object.change(min: i, sec: 0).to_i
95
+ stats << stats_for_time(time, options) unless time > Time.now.to_i
96
+ end
97
+
98
+ hour += 3600
99
+ end
100
+ stats
101
+ end
102
+
103
+ private
104
+ def open!
105
+ log_event :open
106
+ logger.debug "[CIRCUIT] opening #{service} circuit"
107
+ circuit_store.write(storage_key(:asleep), true, expires_in: option_value(:sleep_window).seconds)
108
+ half_open!
109
+ end
110
+
111
+ def half_open!
112
+ circuit_store.write(storage_key(:half_open), true)
113
+ end
114
+
115
+ def open_flag?
116
+ circuit_store.read(storage_key(:asleep)).present?
117
+ end
118
+
119
+ def half_open?
120
+ circuit_store.read(storage_key(:half_open)).present?
121
+ end
122
+
123
+ def passed_volume_threshold?
124
+ success_count + failure_count > option_value(:volume_threshold)
125
+ end
126
+
127
+ def passed_rate_threshold?
128
+ error_rate >= option_value(:error_threshold)
129
+ end
130
+
131
+ def failure_count
132
+ circuit_store.read(stat_storage_key(:failure)).to_i
133
+ end
134
+
135
+ def success_count
136
+ circuit_store.read(stat_storage_key(:success)).to_i
137
+ end
138
+
139
+ def error_rate
140
+ all_count = failure_count + success_count
141
+ return 0.0 unless all_count > 0
142
+ failure_count.to_f / all_count.to_f * 100
143
+ end
144
+
145
+ def success!
146
+ log_event :success
147
+ circuit_store.delete(storage_key(:half_open))
148
+ clear_failures!
149
+ end
150
+
151
+ def failure!
152
+ log_event :failure
153
+ end
154
+
155
+ # Store success/failure/open/close data in memcache
156
+ def log_event(event)
157
+ notifier.notify(event, service, partition)
158
+ log_event_to_process(event)
159
+
160
+ if stat_store.present?
161
+ log_event_to_stat_store(stat_storage_key(event))
162
+ log_event_to_stat_store(stat_storage_key(event, without_partition: true))
163
+ end
164
+ end
165
+
166
+ # When there is a successful response within a stat interval, clear the failures.
167
+ def clear_failures!
168
+ circuit_store.write(stat_storage_key(:failure), 0, raw: true)
169
+ end
170
+
171
+ # Logs to process memory.
172
+ def log_event_to_process(event)
173
+ key = stat_storage_key(event)
174
+ if circuit_store.read(key, raw: true)
175
+ circuit_store.increment(key)
176
+ else
177
+ circuit_store.write(key, 1, raw: true)
178
+ end
179
+ end
180
+
181
+ # Logs to Memcache.
182
+ def log_event_to_stat_store(key)
183
+ if stat_store.read(key, raw: true)
184
+ stat_store.increment(key)
185
+ else
186
+ stat_store.write(key, 1, raw: true)
187
+ end
188
+ end
189
+
190
+ # For returning stale responses when the circuit is open
191
+ def response_key(args)
192
+ Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
193
+ end
194
+
195
+ def cache_response(args, response)
196
+ cache.write(response_key(args), response)
197
+ end
198
+
199
+ def get_cached_response(args)
200
+ cache.read(response_key(args))
201
+ end
202
+
203
+ def stat_storage_key(event, options = {})
204
+ storage_key(:stats, Time.new.change(sec: 0).to_i, event, options)
205
+ end
206
+
207
+ def storage_key(*args)
208
+ options = args.extract_options!
209
+
210
+ key = if options[:without_partition]
211
+ "circuits:#{service}:#{args.join(":")}"
212
+ else
213
+ "circuits:#{service}:#{partition}:#{args.join(":")}"
214
+ end
215
+
216
+ return key
217
+ end
218
+
219
+ def timeout(timeout_seconds, &block)
220
+ Timeout::timeout(timeout_seconds) { block.call }
221
+ end
222
+
223
+ def self.reset
224
+ Circuitbox.reset
225
+ end
226
+
227
+ def stats_for_time(time, options = {})
228
+ stats = { time: time }
229
+ [:success, :failure, :open].each do |event|
230
+ stats[event] = stat_store.read(storage_key(:stats, time, event, options), raw: true) || 0
231
+ end
232
+ stats
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,31 @@
1
+ require 'faraday'
2
+ require 'circuitbox'
3
+
4
+ class Circuitbox
5
+ class RequestError < StandardError; end
6
+
7
+ class FaradayMiddleware < Faraday::Response::Middleware
8
+
9
+ attr_accessor :identifier, :exceptions
10
+
11
+ def initialize(app, opts={})
12
+ @identifier = opts.fetch(:identifier) { ->(env) { env.url }}
13
+ @exceptions = opts.fetch(:exceptions) { [Faraday::Error::TimeoutError] }
14
+ super(app)
15
+ end
16
+
17
+ def call(env)
18
+ id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
19
+ circuit = Circuitbox.circuit id, :exceptions => exceptions
20
+ circuit.run do
21
+ super(env)
22
+ end
23
+ end
24
+
25
+ def on_complete(env)
26
+ if !env.success?
27
+ raise RequestError
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveSupport
2
+ module Cache
3
+ class MemcacheStore
4
+ def initialize(cache)
5
+ @cache = cache
6
+ end
7
+
8
+ def read(key, options = {})
9
+ @cache.get(key, options)
10
+ rescue Memcached::NotFound
11
+ nil
12
+ end
13
+
14
+ def increment(key)
15
+ @cache.incr(key)
16
+ end
17
+
18
+ def write(key, value, options = {})
19
+ if expires_in = options.delete(:expires_in)
20
+ options[:expiry] = expires_in.to_i
21
+ end
22
+
23
+ @cache.set(key, value, options)
24
+ end
25
+
26
+ def delete(key)
27
+ @cache.delete(key)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ class Circuitbox
2
+ class Notifier
3
+ def self.notify(event, service, partition = nil)
4
+ return unless defined? ActiveSupport::Notifications
5
+
6
+ circuit_name = service
7
+ circuit_name += ":#{partition}" if partition
8
+
9
+ ActiveSupport::Notifications.instrument("circuit_#{event}", circuit: circuit_name)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ require 'circuitbox'
2
+ require 'rails'
3
+
4
+ class Circuitbox
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load "tasks/circuits.rake"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ class Circuitbox
2
+ VERSION = '0.5.2'
3
+ end
data/lib/circuitbox.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'singleton'
2
+ require 'active_support'
3
+ require 'logger'
4
+ require 'timeout'
5
+
6
+ require "circuitbox/version"
7
+ require 'circuitbox/memcache_store'
8
+ require 'circuitbox/railtie' if defined?(Rails)
9
+ require 'circuitbox/circuit_breaker'
10
+ require 'circuitbox/notifier'
11
+
12
+ class Circuitbox
13
+ attr_accessor :circuits, :circuit_store, :stat_store
14
+ cattr_accessor :configure
15
+
16
+ def self.instance
17
+ @@instance ||= new
18
+ end
19
+
20
+ def initialize
21
+ self.instance_eval(&@@configure) if @@configure
22
+ end
23
+
24
+ def self.configure(&block)
25
+ @@configure = block if block
26
+ end
27
+
28
+ def self.reset
29
+ @@instance = nil
30
+ @@configure = nil
31
+ end
32
+
33
+ def self.circuit_store
34
+ self.instance.circuit_store ||= ActiveSupport::Cache::MemoryStore.new
35
+ end
36
+
37
+ def self.circuit_store=(store)
38
+ self.instance.circuit_store = store
39
+ end
40
+
41
+ def self.stat_store
42
+ self.instance.stat_store
43
+ end
44
+
45
+ def self.stat_store=(store)
46
+ self.instance.stat_store = store
47
+ end
48
+
49
+ def self.[](service_identifier, options = {})
50
+ self.circuit(service_identifier, options)
51
+ end
52
+
53
+ def self.circuit(service_identifier, options = {})
54
+ service_name = self.parameter_to_service_name(service_identifier)
55
+
56
+ self.instance.circuits ||= Hash.new
57
+ self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
58
+
59
+ if block_given?
60
+ self.instance.circuits[service_name].run { yield }
61
+ else
62
+ self.instance.circuits[service_name]
63
+ end
64
+ end
65
+
66
+ def self.parameter_to_service_name(param)
67
+ uri = URI(param.to_s)
68
+ uri.host.present? ? uri.host : param.to_s
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ namespace :circuits do
2
+ task :stats => :environment do
3
+ service = ENV['SERVICE']
4
+ partition_key = ENV['PARTITION']
5
+
6
+ if service.blank?
7
+ raise "You must specify a SERVICE env variable, eg. `bundle exec rake circuits:stats SERVICE=yammer`"
8
+ else
9
+ pp Circuitbox::CircuitBreaker.new(service).stats(partition_key)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,206 @@
1
+ require 'test_helper'
2
+ require 'circuitbox'
3
+
4
+ class CircuitBreakerTest < Minitest::Test
5
+ SUCCESSFUL_RESPONSE_STRING = "Success!"
6
+ RequestFailureError = Timeout::Error
7
+ class ConnectionError < StandardError; end;
8
+ class SomeOtherError < StandardError; end;
9
+
10
+ def setup
11
+ Circuitbox::CircuitBreaker.reset
12
+ end
13
+
14
+ def test_goes_into_half_open_state_on_sleep
15
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
16
+ circuit.send(:open!)
17
+ assert circuit.send(:half_open?)
18
+ end
19
+
20
+ describe "when in half open state" do
21
+ before do
22
+ Circuitbox::CircuitBreaker.reset
23
+ @circuit = Circuitbox::CircuitBreaker.new(:yammer)
24
+ end
25
+
26
+ it "opens circuit on next failed request" do
27
+ @circuit.stubs(half_open?: true)
28
+ @circuit.expects(:open!)
29
+ @circuit.run { raise RequestFailureError }
30
+ end
31
+
32
+ it "closes circuit on successful request" do
33
+ @circuit.send(:half_open!)
34
+ @circuit.run { 'success' }
35
+ assert !@circuit.send(:half_open?)
36
+ assert !@circuit.send(:open?)
37
+ end
38
+ end
39
+
40
+ def test_should_use_timeout_class_if_exceptions_are_not_defined
41
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 45)
42
+ circuit.expects(:timeout).with(45).once
43
+ emulate_circuit_run(circuit, :success, StandardError)
44
+ end
45
+
46
+ def test_should_not_use_timeout_class_if_custom_exceptions_are_defined
47
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, exceptions: [ConnectionError])
48
+ circuit.expects(:timeout).never
49
+ emulate_circuit_run(circuit, :success, StandardError)
50
+ end
51
+
52
+ def test_should_return_response_if_it_doesnt_timeout
53
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
54
+ response = emulate_circuit_run(circuit, :success, SUCCESSFUL_RESPONSE_STRING)
55
+ assert_equal SUCCESSFUL_RESPONSE_STRING, response
56
+ end
57
+
58
+ def test_timeout_seconds_run_options_overrides_circuit_options
59
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 60)
60
+ circuit.expects(:timeout).with(30).once
61
+ circuit.run(timeout_seconds: 30) { true }
62
+ end
63
+
64
+ def test_catches_connection_error_failures_if_defined
65
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError])
66
+ response = emulate_circuit_run(circuit, :failure, ConnectionError)
67
+ assert_equal nil, response
68
+ end
69
+
70
+ def test_doesnt_catch_out_of_scope_exceptions
71
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError, RequestFailureError])
72
+
73
+ assert_raises SomeOtherError do
74
+ emulate_circuit_run(circuit, :failure, SomeOtherError)
75
+ end
76
+ end
77
+
78
+ def test_records_response_failure
79
+ circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [RequestFailureError])
80
+ circuit.expects(:log_event).with(:failure)
81
+ emulate_circuit_run(circuit, :failure, RequestFailureError)
82
+ end
83
+
84
+ def test_records_response_success
85
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
86
+ circuit.expects(:log_event).with(:success)
87
+ emulate_circuit_run(circuit, :success, SUCCESSFUL_RESPONSE_STRING)
88
+ end
89
+
90
+ def test_does_not_send_request_if_circuit_is_open
91
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
92
+ circuit.stubs(:open? => true)
93
+ circuit.expects(:yield).never
94
+ response = emulate_circuit_run(circuit, :failure, RequestFailureError)
95
+ assert_equal nil, response
96
+ end
97
+
98
+ def test_returns_nil_response_on_failed_request
99
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
100
+ response = emulate_circuit_run(circuit, :failure, RequestFailureError)
101
+ assert_equal nil, response
102
+ end
103
+
104
+ def test_puts_circuit_to_sleep_once_opened
105
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
106
+ circuit.stubs(:open? => true)
107
+
108
+ assert !circuit.send(:open_flag?)
109
+ emulate_circuit_run(circuit, :failure, RequestFailureError)
110
+ assert circuit.send(:open_flag?)
111
+
112
+ circuit.expects(:open!).never
113
+ emulate_circuit_run(circuit, :failure, RequestFailureError)
114
+ end
115
+
116
+ def test_open_is_true_if_open_flag
117
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
118
+ circuit.stubs(:open_flag? => true)
119
+ assert circuit.open?
120
+ end
121
+
122
+ def test_open_checks_if_volume_threshold_has_passed
123
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
124
+ circuit.stubs(:open_flag? => false)
125
+
126
+ circuit.expects(:passed_volume_threshold?).once
127
+ circuit.open?
128
+ end
129
+
130
+ def test_open_checks_error_rate_threshold
131
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
132
+ circuit.stubs(:open_flag? => false,
133
+ :passed_volume_threshold? => true)
134
+
135
+ circuit.expects(:passed_rate_threshold?).once
136
+ circuit.open?
137
+ end
138
+
139
+ def test_open_is_false_if_awake_and_under_rate_threshold
140
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
141
+ circuit.stubs(:open_flag? => false,
142
+ :passed_volume_threshold? => false,
143
+ :passed_rate_threshold => false)
144
+
145
+ assert !circuit.open?
146
+ end
147
+
148
+ def test_error_rate_threshold_calculation
149
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
150
+ circuit.stubs(:failure_count => 3, :success_count => 2)
151
+ assert circuit.send(:passed_rate_threshold?)
152
+
153
+ circuit.stubs(:failure_count => 2, :success_count => 3)
154
+ assert !circuit.send(:passed_rate_threshold?)
155
+ end
156
+
157
+ def test_logs_and_retrieves_success_events
158
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
159
+ 5.times { circuit.send(:log_event, :success) }
160
+ assert_equal 5, circuit.send(:success_count)
161
+ end
162
+
163
+ def test_logs_and_retrieves_failure_events
164
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
165
+ 5.times { circuit.send(:log_event, :failure) }
166
+ assert_equal 5, circuit.send(:failure_count)
167
+ end
168
+
169
+ def test_logs_events_by_minute
170
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
171
+
172
+ Timecop.travel(Time.now.change(sec: 5))
173
+ 4.times { circuit.send(:log_event, :success) }
174
+ assert_equal 4, circuit.send(:success_count)
175
+
176
+ Timecop.travel(1.minute.from_now)
177
+ 7.times { circuit.send(:log_event, :success) }
178
+ assert_equal 7, circuit.send(:success_count)
179
+
180
+ Timecop.travel(30.seconds.from_now)
181
+ circuit.send(:log_event, :success)
182
+ assert_equal 8, circuit.send(:success_count)
183
+
184
+ Timecop.travel(50.seconds.from_now)
185
+ assert_equal 0, circuit.send(:success_count)
186
+ end
187
+
188
+ def test_notifies_on_open_circuit
189
+ circuit = Circuitbox::CircuitBreaker.new(:yammer)
190
+ Circuitbox::Notifier.expects(:notify).with(:open, :yammer, nil)
191
+ circuit.send(:log_event, :open)
192
+ end
193
+
194
+ def emulate_circuit_run(circuit, response_type, response_value)
195
+ circuit.run do
196
+ case response_type
197
+ when :failure
198
+ raise response_value
199
+ when :success
200
+ response_value
201
+ end
202
+ end
203
+ rescue RequestFailureError
204
+ nil
205
+ end
206
+ end
@@ -0,0 +1,73 @@
1
+ require 'test_helper'
2
+ require 'circuitbox'
3
+
4
+ class Circuitbox::ExampleStore < ActiveSupport::Cache::MemoryStore; end
5
+
6
+ describe Circuitbox do
7
+ before { Circuitbox.reset }
8
+ after { Circuitbox.reset }
9
+
10
+ describe "Circuitbox.configure" do
11
+ it "configures instance variables on init" do
12
+ Circuitbox.configure do
13
+ self.stat_store = "hello"
14
+ end
15
+
16
+ assert_equal "hello", Circuitbox.stat_store
17
+ end
18
+ end
19
+
20
+ describe "Circuitbox.circuit_store" do
21
+ it "is configurable" do
22
+ example_store = Circuitbox::ExampleStore.new
23
+ Circuitbox.circuit_store = example_store
24
+ assert_equal example_store, Circuitbox[:yammer].circuit_store
25
+ end
26
+ end
27
+
28
+ describe "Circuitbox.stat_store" do
29
+ it "is configurable" do
30
+ example_store = Circuitbox::ExampleStore.new
31
+ Circuitbox.stat_store = example_store
32
+ assert_equal example_store, Circuitbox[:yammer].stat_store
33
+ end
34
+ end
35
+
36
+ describe "Circuitbox[:service]" do
37
+ it "delegates to #circuit" do
38
+ Circuitbox.expects(:circuit).with(:yammer, {})
39
+ Circuitbox[:yammer]
40
+ end
41
+
42
+ it "creates a CircuitBreaker instance" do
43
+ assert Circuitbox[:yammer].is_a? Circuitbox::CircuitBreaker
44
+ end
45
+ end
46
+
47
+ describe "#circuit" do
48
+ it "returns the same circuit every time" do
49
+ assert_equal Circuitbox.circuit(:yammer).object_id, Circuitbox.circuit(:yammer).object_id
50
+ end
51
+
52
+ it "sets the circuit options the first time" do
53
+ circuit_one = Circuitbox.circuit(:yammer, :sleep_window => 1337)
54
+ circuit_two = Circuitbox.circuit(:yammer, :sleep_window => 2000)
55
+
56
+ assert_equal 1337, circuit_one.option_value(:sleep_window)
57
+ assert_equal 1337, circuit_two.option_value(:sleep_window)
58
+ end
59
+ end
60
+
61
+ describe "#parameter_to_service_name" do
62
+ it "parses out a service name from URI" do
63
+ service = Circuitbox.parameter_to_service_name("http://api.yammer.com/api/v1/messages")
64
+ assert_equal "api.yammer.com", service
65
+ end
66
+
67
+ it "uses the parameter as the service name if the parameter is not an URI" do
68
+ service = Circuitbox.parameter_to_service_name(:yammer)
69
+ assert_equal "yammer", service
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+ require_relative '../lib/circuitbox/faraday_middleware'
3
+
4
+ class Circuitbox
5
+ class FaradayMiddlewareTest < Minitest::Test
6
+
7
+ def setup
8
+ @app = gimme
9
+ @env = gimme
10
+ give(@env).url { 'URL' }
11
+
12
+ @middleware = FaradayMiddleware.new @app,
13
+ :identifier => 'ID',
14
+ :exceptions => [StandardError]
15
+ end
16
+
17
+ def test_should_use_env_url_proc_if_not_provided_as_identifier
18
+ middleware = FaradayMiddleware.new @app, :exceptions => gimme
19
+ assert middleware.identifier.is_a?(Proc)
20
+ assert_equal 'URL', middleware.identifier.call(@env)
21
+ end
22
+
23
+ def test_should_use_request_error_if_not_provided_as_exception
24
+ middleware = FaradayMiddleware.new @app, :identifier => 'ID'
25
+ assert_equal [Faraday::Error::TimeoutError],
26
+ middleware.exceptions
27
+ end
28
+
29
+ def test_successful_call
30
+ @middleware.call(@env)
31
+ end
32
+
33
+ def test_failed_call
34
+ assert_raises Circuitbox::RequestError do
35
+ give(@env).success? { false }
36
+ @middleware.on_complete(@env)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+ require 'circuitbox/notifier'
3
+ require 'active_support/notifications'
4
+
5
+ describe Circuitbox::Notifier do
6
+ it "sends an ActiveSupport::Notification" do
7
+ ActiveSupport::Notifications.expects(:instrument).with("circuit_open", circuit: :yammer)
8
+ Circuitbox::Notifier.notify(:open, :yammer)
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ require 'minitest'
2
+ require 'minitest/autorun'
3
+ require 'minitest/pride'
4
+ require 'mocha/mini_test'
5
+ require 'timecop'
6
+ require 'gimme'
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: circuitbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Fahim Ferdous
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A robust circuit breaker that manages failing external services.
56
+ email:
57
+ - fahimfmf@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - Gemfile
64
+ - Guardfile
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - circuitbox.gemspec
69
+ - lib/circuitbox.rb
70
+ - lib/circuitbox/circuit_breaker.rb
71
+ - lib/circuitbox/faraday_middleware.rb
72
+ - lib/circuitbox/memcache_store.rb
73
+ - lib/circuitbox/notifier.rb
74
+ - lib/circuitbox/railtie.rb
75
+ - lib/circuitbox/version.rb
76
+ - lib/tasks/circuits.rake
77
+ - test/circuit_breaker_test.rb
78
+ - test/circuitbox_test.rb
79
+ - test/faraday_middleware_test.rb
80
+ - test/notifier_test.rb
81
+ - test/test_helper.rb
82
+ homepage: ''
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: A robust circuit breaker that manages failing external services.
106
+ test_files:
107
+ - test/circuit_breaker_test.rb
108
+ - test/circuitbox_test.rb
109
+ - test/faraday_middleware_test.rb
110
+ - test/notifier_test.rb
111
+ - test/test_helper.rb