circuitbox 1.1.0 → 2.0.0.pre4
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 +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
|