circuitbox 1.1.0 → 2.0.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +56 -187
- data/lib/circuitbox.rb +14 -57
- data/lib/circuitbox/circuit_breaker.rb +137 -161
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +51 -0
- data/lib/circuitbox/errors/error.rb +3 -2
- data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
- data/lib/circuitbox/errors/service_failure_error.rb +5 -1
- data/lib/circuitbox/excon_middleware.rb +23 -30
- data/lib/circuitbox/faraday_middleware.rb +43 -63
- data/lib/circuitbox/memory_store.rb +85 -0
- data/lib/circuitbox/memory_store/container.rb +30 -0
- data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
- data/lib/circuitbox/notifier/active_support.rb +19 -0
- data/lib/circuitbox/notifier/null.rb +13 -0
- data/lib/circuitbox/timer.rb +51 -0
- data/lib/circuitbox/version.rb +3 -1
- metadata +106 -118
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -9
- data/Gemfile +0 -6
- data/Rakefile +0 -18
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -48
- data/lib/circuitbox/memcache_store.rb +0 -31
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -428
- data/test/circuitbox_test.rb +0 -45
- data/test/excon_middleware_test.rb +0 -131
- data/test/faraday_middleware_test.rb +0 -175
- data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
- data/test/integration/faraday_middleware_test.rb +0 -78
- data/test/integration_helper.rb +0 -48
- data/test/notifier_test.rb +0 -21
- data/test/service_failure_error_test.rb +0 -23
- data/test/test_helper.rb +0 -15
@@ -1,12 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Circuitbox
|
2
4
|
class ServiceFailureError < Circuitbox::Error
|
3
5
|
attr_reader :service, :original
|
4
6
|
|
5
7
|
def initialize(service, exception)
|
8
|
+
super()
|
6
9
|
@service = service
|
7
10
|
@original = exception
|
8
11
|
# we copy over the original exceptions backtrace if there is one
|
9
|
-
|
12
|
+
backtrace = exception.backtrace
|
13
|
+
set_backtrace(backtrace) unless backtrace.empty?
|
10
14
|
end
|
11
15
|
|
12
16
|
def to_s
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'excon'
|
2
4
|
require 'circuitbox'
|
3
5
|
|
@@ -8,7 +10,7 @@ class Circuitbox
|
|
8
10
|
DEFAULT_EXCEPTIONS = [
|
9
11
|
Excon::Errors::Timeout,
|
10
12
|
RequestFailed
|
11
|
-
]
|
13
|
+
].freeze
|
12
14
|
|
13
15
|
class NullResponse < Excon::Response
|
14
16
|
def initialize(response, exception)
|
@@ -26,36 +28,36 @@ class Circuitbox
|
|
26
28
|
|
27
29
|
def initialize(stack, opts = {})
|
28
30
|
@stack = stack
|
29
|
-
default_options = { open_circuit:
|
31
|
+
default_options = { open_circuit: ->(response) { response[:status] >= 400 } }
|
30
32
|
@opts = default_options.merge(opts)
|
31
33
|
super(stack)
|
32
34
|
end
|
33
35
|
|
34
36
|
def error_call(datum)
|
35
|
-
circuit(datum).run
|
37
|
+
circuit(datum).run do
|
36
38
|
raise RequestFailed
|
37
39
|
end
|
38
|
-
rescue Circuitbox::Error =>
|
39
|
-
circuit_open_value(datum, datum[:response],
|
40
|
+
rescue Circuitbox::Error => e
|
41
|
+
circuit_open_value(datum, datum[:response], e)
|
40
42
|
end
|
41
43
|
|
42
44
|
def request_call(datum)
|
43
|
-
circuit(datum).run
|
45
|
+
circuit(datum).run do
|
44
46
|
@stack.request_call(datum)
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
50
|
def response_call(datum)
|
49
|
-
circuit(datum).run
|
51
|
+
circuit(datum).run do
|
50
52
|
raise RequestFailed if open_circuit?(datum[:response])
|
51
53
|
end
|
52
54
|
@stack.response_call(datum)
|
53
|
-
rescue Circuitbox::Error =>
|
54
|
-
circuit_open_value(datum, datum[:response],
|
55
|
+
rescue Circuitbox::Error => e
|
56
|
+
circuit_open_value(datum, datum[:response], e)
|
55
57
|
end
|
56
58
|
|
57
59
|
def identifier
|
58
|
-
@identifier ||= opts.fetch(:identifier, ->(env) { env[:
|
60
|
+
@identifier ||= opts.fetch(:identifier, ->(env) { env[:host] })
|
59
61
|
end
|
60
62
|
|
61
63
|
def exceptions
|
@@ -69,10 +71,6 @@ class Circuitbox
|
|
69
71
|
circuitbox.circuit id, circuit_breaker_options
|
70
72
|
end
|
71
73
|
|
72
|
-
def run_options(datum)
|
73
|
-
opts.merge(datum)[:circuit_breaker_run_options] || {}
|
74
|
-
end
|
75
|
-
|
76
74
|
def open_circuit?(response)
|
77
75
|
opts[:open_circuit].call(response)
|
78
76
|
end
|
@@ -86,26 +84,21 @@ class Circuitbox
|
|
86
84
|
end
|
87
85
|
|
88
86
|
def circuit_breaker_options
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
87
|
+
@circuit_breaker_options ||= begin
|
88
|
+
options = opts.fetch(:circuit_breaker_options, {})
|
89
|
+
options.merge!(
|
90
|
+
exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
|
91
|
+
)
|
92
|
+
end
|
95
93
|
end
|
96
94
|
|
97
95
|
def default_value
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
96
|
+
@default_value ||= begin
|
97
|
+
default = opts.fetch(:default_value) do
|
98
|
+
->(response, exception) { NullResponse.new(response, exception) }
|
99
|
+
end
|
100
|
+
default.respond_to?(:call) ? default : ->(*) { default }
|
102
101
|
end
|
103
|
-
|
104
|
-
@default_value = if default.respond_to?(:call)
|
105
|
-
default
|
106
|
-
else
|
107
|
-
lambda { |*| default }
|
108
|
-
end
|
109
102
|
end
|
110
103
|
end
|
111
104
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'circuitbox'
|
3
5
|
|
@@ -5,13 +7,9 @@ class Circuitbox
|
|
5
7
|
class FaradayMiddleware < Faraday::Middleware
|
6
8
|
class RequestFailed < StandardError; end
|
7
9
|
|
8
|
-
DEFAULT_EXCEPTIONS = [
|
9
|
-
Faraday::Error::TimeoutError,
|
10
|
-
RequestFailed,
|
11
|
-
]
|
12
|
-
|
13
10
|
class NullResponse < Faraday::Response
|
14
11
|
attr_reader :original_response, :original_exception
|
12
|
+
|
15
13
|
def initialize(response = nil, exception = nil)
|
16
14
|
@original_response = response
|
17
15
|
@original_exception = exception
|
@@ -19,96 +17,78 @@ class Circuitbox
|
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
DEFAULT_CIRCUITBOX_OPTIONS = {
|
20
|
+
DEFAULT_OPTIONS = {
|
25
21
|
open_circuit: lambda do |response|
|
26
22
|
# response.status:
|
27
23
|
# nil -> connection could not be established, or failed very hard
|
28
24
|
# 5xx -> non recoverable server error, oposed to 4xx which are client errors
|
29
|
-
response.status.nil? || (
|
30
|
-
end
|
31
|
-
|
25
|
+
response.status.nil? || (response.status >= 500 && response.status <= 599)
|
26
|
+
end,
|
27
|
+
default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) },
|
28
|
+
# It's possible for the URL object to not have a host at the time the middleware
|
29
|
+
# is run. To not break circuitbox by creating a circuit with a nil service name
|
30
|
+
# we can get the string representation of the URL object and use that as the service name.
|
31
|
+
identifier: ->(env) { env[:url].host || env[:url].to_s },
|
32
|
+
# default circuit breaker options are merged in during initialization
|
33
|
+
circuit_breaker_options: {}
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
DEFAULT_EXCEPTIONS = [
|
37
|
+
# Faraday before 0.9.0 didn't have Faraday::TimeoutError so we default to Faraday::Error::TimeoutError
|
38
|
+
# Faraday >= 0.9.0 defines Faraday::TimeoutError and this can be used for all versions up to 1.0.0 that
|
39
|
+
# also define and raise Faraday::Error::TimeoutError as Faraday::TimeoutError is an ancestor
|
40
|
+
defined?(Faraday::TimeoutError) ? Faraday::TimeoutError : Faraday::Error::TimeoutError,
|
41
|
+
RequestFailed
|
42
|
+
].freeze
|
43
|
+
|
44
|
+
DEFAULT_CIRCUIT_BREAKER_OPTIONS = {
|
45
|
+
exceptions: DEFAULT_EXCEPTIONS
|
46
|
+
}.freeze
|
47
|
+
|
48
|
+
attr_reader :opts
|
32
49
|
|
33
50
|
def initialize(app, opts = {})
|
34
51
|
@app = app
|
35
|
-
@opts =
|
52
|
+
@opts = DEFAULT_OPTIONS.merge(opts)
|
53
|
+
|
54
|
+
@opts[:circuit_breaker_options] = DEFAULT_CIRCUIT_BREAKER_OPTIONS.merge(@opts[:circuit_breaker_options])
|
36
55
|
super(app)
|
37
56
|
end
|
38
57
|
|
39
58
|
def call(request_env)
|
40
59
|
service_response = nil
|
41
|
-
circuit(request_env).run
|
60
|
+
circuit(request_env).run do
|
42
61
|
@app.call(request_env).on_complete do |env|
|
43
62
|
service_response = Faraday::Response.new(env)
|
44
63
|
raise RequestFailed if open_circuit?(service_response)
|
45
64
|
end
|
46
65
|
end
|
47
|
-
rescue Circuitbox::Error =>
|
48
|
-
circuit_open_value(request_env, service_response,
|
49
|
-
end
|
50
|
-
|
51
|
-
def exceptions
|
52
|
-
circuit_breaker_options[:exceptions]
|
53
|
-
end
|
54
|
-
|
55
|
-
def identifier
|
56
|
-
@identifier ||= opts.fetch(:identifier, ->(env) { env[:url] })
|
66
|
+
rescue Circuitbox::Error => e
|
67
|
+
circuit_open_value(request_env, service_response, e)
|
57
68
|
end
|
58
69
|
|
59
70
|
private
|
60
71
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
def circuit_breaker_options
|
66
|
-
return @circuit_breaker_options if @circuit_breaker_options
|
67
|
-
|
68
|
-
@circuit_breaker_options = opts.fetch(:circuit_breaker_options, {})
|
69
|
-
@circuit_breaker_options.merge!(
|
70
|
-
exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
|
71
|
-
)
|
72
|
-
end
|
73
|
-
|
74
|
-
def default_value
|
75
|
-
return @default_value if @default_value
|
76
|
-
|
77
|
-
default = opts.fetch(:default_value) do
|
78
|
-
lambda { |service_response, exception| NullResponse.new(service_response, exception) }
|
79
|
-
end
|
80
|
-
|
81
|
-
@default_value = if default.respond_to?(:call)
|
82
|
-
default
|
83
|
-
else
|
84
|
-
lambda { |*| default }
|
85
|
-
end
|
72
|
+
def call_default_value(response, exception)
|
73
|
+
default_value = opts[:default_value]
|
74
|
+
default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
|
86
75
|
end
|
87
76
|
|
88
77
|
def open_circuit?(response)
|
89
78
|
opts[:open_circuit].call(response)
|
90
79
|
end
|
91
80
|
|
92
|
-
def circuitbox
|
93
|
-
@circuitbox ||= opts.fetch(:circuitbox, Circuitbox)
|
94
|
-
end
|
95
|
-
|
96
81
|
def circuit_open_value(env, service_response, exception)
|
97
|
-
env[:circuit_breaker_default_value] ||
|
98
|
-
end
|
99
|
-
|
100
|
-
def call_default_lambda(service_response, exception)
|
101
|
-
if default_value.arity == 2
|
102
|
-
default_value.call(service_response, exception)
|
103
|
-
else
|
104
|
-
default_value.call(service_response)
|
105
|
-
end
|
82
|
+
env[:circuit_breaker_default_value] || call_default_value(service_response, exception)
|
106
83
|
end
|
107
84
|
|
108
85
|
def circuit(env)
|
86
|
+
identifier = opts[:identifier]
|
109
87
|
id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
|
110
|
-
circuitbox.circuit id, circuit_breaker_options
|
111
|
-
end
|
112
88
|
|
89
|
+
Circuitbox.circuit(id, opts[:circuit_breaker_options])
|
90
|
+
end
|
113
91
|
end
|
114
92
|
end
|
93
|
+
|
94
|
+
Faraday::Middleware.register_middleware circuitbox: Circuitbox::FaradayMiddleware
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'memory_store/monotonic_time'
|
4
|
+
require_relative 'memory_store/container'
|
5
|
+
|
6
|
+
class Circuitbox
|
7
|
+
class MemoryStore
|
8
|
+
include MonotonicTime
|
9
|
+
|
10
|
+
def initialize(compaction_frequency: 60)
|
11
|
+
@store = {}
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@compaction_frequency = compaction_frequency
|
14
|
+
@compact_after = current_second + compaction_frequency
|
15
|
+
end
|
16
|
+
|
17
|
+
def store(key, value, opts = {})
|
18
|
+
@mutex.synchronize do
|
19
|
+
@store[key] = Container.new(value: value, expiry: opts.fetch(:expires, 0))
|
20
|
+
value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def increment(key, amount = 1, opts = {})
|
25
|
+
seconds_to_expire = opts.fetch(:expires, 0)
|
26
|
+
|
27
|
+
@mutex.synchronize do
|
28
|
+
existing_container = fetch_container(key)
|
29
|
+
|
30
|
+
# reusing the existing container is a small optmization
|
31
|
+
# to reduce the amount of objects created
|
32
|
+
if existing_container
|
33
|
+
existing_container.expires_after(seconds_to_expire)
|
34
|
+
existing_container.value += amount
|
35
|
+
else
|
36
|
+
@store[key] = Container.new(value: amount, expiry: seconds_to_expire)
|
37
|
+
amount
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def load(key, _opts = {})
|
43
|
+
@mutex.synchronize { fetch_value(key) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def key?(key)
|
47
|
+
@mutex.synchronize { !fetch_container(key).nil? }
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(key)
|
51
|
+
@mutex.synchronize { @store.delete(key) }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def fetch_container(key)
|
57
|
+
current_time = current_second
|
58
|
+
|
59
|
+
compact(current_time) if @compact_after < current_time
|
60
|
+
|
61
|
+
container = @store[key]
|
62
|
+
|
63
|
+
return unless container
|
64
|
+
|
65
|
+
if container.expired_at?(current_time)
|
66
|
+
@store.delete(key)
|
67
|
+
nil
|
68
|
+
else
|
69
|
+
container
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_value(key)
|
74
|
+
container = fetch_container(key)
|
75
|
+
return unless container
|
76
|
+
|
77
|
+
container.value
|
78
|
+
end
|
79
|
+
|
80
|
+
def compact(current_time)
|
81
|
+
@store.delete_if { |_, value| value.expired_at?(current_time) }
|
82
|
+
@compact_after = current_time + @compaction_frequency
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'monotonic_time'
|
4
|
+
|
5
|
+
class Circuitbox
|
6
|
+
class MemoryStore
|
7
|
+
class Container
|
8
|
+
include MonotonicTime
|
9
|
+
|
10
|
+
attr_accessor :value
|
11
|
+
|
12
|
+
def initialize(value:, expiry: 0)
|
13
|
+
@value = value
|
14
|
+
expires_after(expiry)
|
15
|
+
end
|
16
|
+
|
17
|
+
def expired?
|
18
|
+
@expires_after.positive? && @expires_after < current_second
|
19
|
+
end
|
20
|
+
|
21
|
+
def expired_at?(clock_second)
|
22
|
+
@expires_after.positive? && @expires_after < clock_second
|
23
|
+
end
|
24
|
+
|
25
|
+
def expires_after(seconds = 0)
|
26
|
+
@expires_after = seconds.zero? ? seconds : current_second + seconds
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Circuitbox
|
4
|
+
class Notifier
|
5
|
+
class ActiveSupport
|
6
|
+
def notify(circuit_name, event)
|
7
|
+
::ActiveSupport::Notifications.instrument("circuit_#{event}", circuit: circuit_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify_warning(circuit_name, message)
|
11
|
+
::ActiveSupport::Notifications.instrument('circuit_warning', circuit: circuit_name, message: message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def metric_gauge(circuit_name, gauge, value)
|
15
|
+
::ActiveSupport::Notifications.instrument('circuit_gauge', circuit: circuit_name, gauge: gauge, value: value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|