circuitbox 1.1.1 → 2.0.0.pre1
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 +60 -122
- data/lib/circuitbox.rb +15 -52
- data/lib/circuitbox/circuit_breaker.rb +131 -141
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +53 -0
- data/lib/circuitbox/errors/error.rb +1 -2
- data/lib/circuitbox/errors/open_circuit_error.rb +0 -1
- data/lib/circuitbox/errors/service_failure_error.rb +2 -1
- data/lib/circuitbox/excon_middleware.rb +15 -24
- data/lib/circuitbox/faraday_middleware.rb +35 -61
- data/lib/circuitbox/memory_store.rb +76 -0
- data/lib/circuitbox/memory_store/compactor.rb +35 -0
- data/lib/circuitbox/memory_store/container.rb +28 -0
- data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
- data/lib/circuitbox/notifier/active_support.rb +19 -0
- data/lib/circuitbox/notifier/null.rb +11 -0
- data/lib/circuitbox/timer/monotonic.rb +17 -0
- data/lib/circuitbox/timer/null.rb +9 -0
- data/lib/circuitbox/timer/simple.rb +13 -0
- data/lib/circuitbox/version.rb +1 -1
- metadata +78 -150
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -9
- data/Gemfile +0 -6
- data/Rakefile +0 -30
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -48
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -436
- 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
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Circuitbox
|
4
|
+
class CircuitBreaker
|
5
|
+
module LoggerMessages
|
6
|
+
def circuit_skipped_message
|
7
|
+
@circuit_skipped_message ||= "[CIRCUIT] #{service}: skipped"
|
8
|
+
end
|
9
|
+
|
10
|
+
def circuit_running_message
|
11
|
+
@circuit_running_message ||= "[CIRCUIT] #{service}: running"
|
12
|
+
end
|
13
|
+
|
14
|
+
def circuit_success_message
|
15
|
+
@circuit_success_message ||= "[CIRCUIT] #{service}: success"
|
16
|
+
end
|
17
|
+
|
18
|
+
def circuit_failure_message
|
19
|
+
@circuit_failure_message ||= "[CIRCUIT] #{service}: failure"
|
20
|
+
end
|
21
|
+
|
22
|
+
def circuit_opened_message
|
23
|
+
@circuit_opened_message ||= "[CIRCUIT] #{service}: opened"
|
24
|
+
end
|
25
|
+
|
26
|
+
def circuit_closed_message
|
27
|
+
@circuit_closed_message ||= "[CIRCUIT] #{service}: closed"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'memory_store'
|
2
|
+
require_relative 'timer/simple'
|
3
|
+
require_relative 'notifier/active_support'
|
4
|
+
require_relative 'notifier/null'
|
5
|
+
|
6
|
+
class Circuitbox
|
7
|
+
module Configuration
|
8
|
+
attr_writer :default_circuit_store,
|
9
|
+
:default_notifier,
|
10
|
+
:default_timer,
|
11
|
+
:default_logger
|
12
|
+
|
13
|
+
def configure
|
14
|
+
yield self
|
15
|
+
clear_cached_circuits!
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_circuit_store
|
20
|
+
@default_circuit_store ||= MemoryStore.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_notifier
|
24
|
+
@default_notifier ||= if defined?(ActiveSupport::Notifications)
|
25
|
+
Notifier::ActiveSupport.new
|
26
|
+
else
|
27
|
+
Notifier::Null.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_timer
|
32
|
+
@default_timer ||= Timer::Simple.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_logger
|
36
|
+
@default_logger ||= if defined?(Rails)
|
37
|
+
Rails.logger
|
38
|
+
else
|
39
|
+
Logger.new(STDOUT)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def cached_circuits
|
46
|
+
@cached_circuits ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear_cached_circuits!
|
50
|
+
@cached_circuits = {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -6,7 +6,8 @@ class Circuitbox
|
|
6
6
|
@service = service
|
7
7
|
@original = exception
|
8
8
|
# we copy over the original exceptions backtrace if there is one
|
9
|
-
|
9
|
+
backtrace = exception.backtrace
|
10
|
+
set_backtrace(backtrace) unless backtrace.empty?
|
10
11
|
end
|
11
12
|
|
12
13
|
def to_s
|
@@ -32,7 +32,7 @@ class Circuitbox
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def error_call(datum)
|
35
|
-
circuit(datum).run
|
35
|
+
circuit(datum).run do
|
36
36
|
raise RequestFailed
|
37
37
|
end
|
38
38
|
rescue Circuitbox::Error => exception
|
@@ -40,13 +40,13 @@ class Circuitbox
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def request_call(datum)
|
43
|
-
circuit(datum).run
|
43
|
+
circuit(datum).run do
|
44
44
|
@stack.request_call(datum)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
48
|
def response_call(datum)
|
49
|
-
circuit(datum).run
|
49
|
+
circuit(datum).run do
|
50
50
|
raise RequestFailed if open_circuit?(datum[:response])
|
51
51
|
end
|
52
52
|
@stack.response_call(datum)
|
@@ -55,7 +55,7 @@ class Circuitbox
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def identifier
|
58
|
-
@identifier ||= opts.fetch(:identifier, ->(env) { env[:
|
58
|
+
@identifier ||= opts.fetch(:identifier, ->(env) { env[:host] })
|
59
59
|
end
|
60
60
|
|
61
61
|
def exceptions
|
@@ -69,10 +69,6 @@ class Circuitbox
|
|
69
69
|
circuitbox.circuit id, circuit_breaker_options
|
70
70
|
end
|
71
71
|
|
72
|
-
def run_options(datum)
|
73
|
-
opts.merge(datum)[:circuit_breaker_run_options] || {}
|
74
|
-
end
|
75
|
-
|
76
72
|
def open_circuit?(response)
|
77
73
|
opts[:open_circuit].call(response)
|
78
74
|
end
|
@@ -86,26 +82,21 @@ class Circuitbox
|
|
86
82
|
end
|
87
83
|
|
88
84
|
def circuit_breaker_options
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
85
|
+
@circuit_breaker_options ||= begin
|
86
|
+
options = opts.fetch(:circuit_breaker_options, {})
|
87
|
+
options.merge!(
|
88
|
+
exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
|
89
|
+
)
|
90
|
+
end
|
95
91
|
end
|
96
92
|
|
97
93
|
def default_value
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
94
|
+
@default_value ||= begin
|
95
|
+
default = opts.fetch(:default_value) do
|
96
|
+
lambda { |response, exception| NullResponse.new(response, exception) }
|
97
|
+
end
|
98
|
+
default.respond_to?(:call) ? default : lambda { |*| default }
|
102
99
|
end
|
103
|
-
|
104
|
-
@default_value = if default.respond_to?(:call)
|
105
|
-
default
|
106
|
-
else
|
107
|
-
lambda { |*| default }
|
108
|
-
end
|
109
100
|
end
|
110
101
|
end
|
111
102
|
end
|
@@ -5,11 +5,6 @@ class Circuitbox
|
|
5
5
|
class FaradayMiddleware < Faraday::Middleware
|
6
6
|
class RequestFailed < StandardError; end
|
7
7
|
|
8
|
-
DEFAULT_EXCEPTIONS = [
|
9
|
-
Faraday::Error::TimeoutError,
|
10
|
-
RequestFailed,
|
11
|
-
]
|
12
|
-
|
13
8
|
class NullResponse < Faraday::Response
|
14
9
|
attr_reader :original_response, :original_exception
|
15
10
|
def initialize(response = nil, exception = nil)
|
@@ -19,26 +14,44 @@ class Circuitbox
|
|
19
14
|
end
|
20
15
|
end
|
21
16
|
|
22
|
-
|
23
|
-
|
24
|
-
DEFAULT_CIRCUITBOX_OPTIONS = {
|
17
|
+
DEFAULT_OPTIONS = {
|
25
18
|
open_circuit: lambda do |response|
|
26
19
|
# response.status:
|
27
20
|
# nil -> connection could not be established, or failed very hard
|
28
21
|
# 5xx -> non recoverable server error, oposed to 4xx which are client errors
|
29
|
-
response.status.nil? || (
|
30
|
-
end
|
31
|
-
|
22
|
+
response.status.nil? || (response.status >= 500 && response.status <= 599)
|
23
|
+
end,
|
24
|
+
default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) },
|
25
|
+
# It's possible for the URL object to not have a host at the time the middleware
|
26
|
+
# is run. To not break circuitbox by creating a circuit with a nil service name
|
27
|
+
# we can get the string representation of the URL object and use that as the service name.
|
28
|
+
identifier: ->(env) { env[:url].host || env[:url].to_s },
|
29
|
+
# default circuit breaker options are merged in during initialization
|
30
|
+
circuit_breaker_options: {}
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
DEFAULT_EXCEPTIONS = [
|
34
|
+
Faraday::Error::TimeoutError,
|
35
|
+
RequestFailed
|
36
|
+
].freeze
|
37
|
+
|
38
|
+
DEFAULT_CIRCUIT_BREAKER_OPTIONS = {
|
39
|
+
exceptions: DEFAULT_EXCEPTIONS
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
attr_reader :opts
|
32
43
|
|
33
44
|
def initialize(app, opts = {})
|
34
45
|
@app = app
|
35
|
-
@opts =
|
46
|
+
@opts = DEFAULT_OPTIONS.merge(opts)
|
47
|
+
|
48
|
+
@opts[:circuit_breaker_options] = DEFAULT_CIRCUIT_BREAKER_OPTIONS.merge(@opts[:circuit_breaker_options])
|
36
49
|
super(app)
|
37
50
|
end
|
38
51
|
|
39
52
|
def call(request_env)
|
40
53
|
service_response = nil
|
41
|
-
circuit(request_env).run
|
54
|
+
circuit(request_env).run do
|
42
55
|
@app.call(request_env).on_complete do |env|
|
43
56
|
service_response = Faraday::Response.new(env)
|
44
57
|
raise RequestFailed if open_circuit?(service_response)
|
@@ -48,67 +61,28 @@ class Circuitbox
|
|
48
61
|
circuit_open_value(request_env, service_response, ex)
|
49
62
|
end
|
50
63
|
|
51
|
-
def exceptions
|
52
|
-
circuit_breaker_options[:exceptions]
|
53
|
-
end
|
54
|
-
|
55
|
-
def identifier
|
56
|
-
@identifier ||= opts.fetch(:identifier, ->(env) { env[:url] })
|
57
|
-
end
|
58
|
-
|
59
64
|
private
|
60
65
|
|
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
|
66
|
+
def call_default_value(response, exception)
|
67
|
+
default_value = opts[:default_value]
|
68
|
+
default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
|
86
69
|
end
|
87
70
|
|
88
71
|
def open_circuit?(response)
|
89
72
|
opts[:open_circuit].call(response)
|
90
73
|
end
|
91
74
|
|
92
|
-
def circuitbox
|
93
|
-
@circuitbox ||= opts.fetch(:circuitbox, Circuitbox)
|
94
|
-
end
|
95
|
-
|
96
75
|
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
|
76
|
+
env[:circuit_breaker_default_value] || call_default_value(service_response, exception)
|
106
77
|
end
|
107
78
|
|
108
79
|
def circuit(env)
|
80
|
+
identifier = opts[:identifier]
|
109
81
|
id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
|
110
|
-
circuitbox.circuit id, circuit_breaker_options
|
111
|
-
end
|
112
82
|
|
83
|
+
Circuitbox.circuit(id, opts[:circuit_breaker_options])
|
84
|
+
end
|
113
85
|
end
|
114
86
|
end
|
87
|
+
|
88
|
+
Faraday::Middleware.register_middleware circuitbox: Circuitbox::FaradayMiddleware
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require_relative 'memory_store/compactor'
|
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
|
+
@compactor = Compactor.new(store: @store, frequency: compaction_frequency)
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(key, value, opts = {})
|
17
|
+
@mutex.synchronize do
|
18
|
+
@store[key] = Container.new(value: value, expiry: opts.fetch(:expires, 0))
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def increment(key, amount = 1, opts = {})
|
24
|
+
seconds_to_expire = opts.fetch(:expires, 0)
|
25
|
+
|
26
|
+
@mutex.synchronize do
|
27
|
+
existing_container = fetch_container(key)
|
28
|
+
|
29
|
+
# reusing the existing container is a small optmization
|
30
|
+
# to reduce the amount of objects created
|
31
|
+
if existing_container
|
32
|
+
existing_container.expires_after(seconds_to_expire)
|
33
|
+
existing_container.value += amount
|
34
|
+
else
|
35
|
+
@store[key] = Container.new(value: amount, expiry: seconds_to_expire)
|
36
|
+
amount
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def load(key, _opts = {})
|
42
|
+
@mutex.synchronize { fetch_value(key) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def key?(key)
|
46
|
+
@mutex.synchronize { !fetch_container(key).nil? }
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete(key)
|
50
|
+
@mutex.synchronize { @store.delete(key) }
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def fetch_container(key)
|
56
|
+
@compactor.run
|
57
|
+
|
58
|
+
container = @store[key]
|
59
|
+
|
60
|
+
return unless container
|
61
|
+
|
62
|
+
if container.expired?
|
63
|
+
@store.delete(key)
|
64
|
+
nil
|
65
|
+
else
|
66
|
+
container
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_value(key)
|
71
|
+
container = fetch_container(key)
|
72
|
+
return unless container
|
73
|
+
container.value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require_relative 'monotonic_time'
|
4
|
+
|
5
|
+
class Circuitbox
|
6
|
+
class MemoryStore
|
7
|
+
class Compactor
|
8
|
+
include MonotonicTime
|
9
|
+
|
10
|
+
attr_reader :store, :frequency, :compact_after
|
11
|
+
|
12
|
+
def initialize(store:, frequency:)
|
13
|
+
@store = store
|
14
|
+
@frequency = frequency
|
15
|
+
set_next_compaction_time
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
compaction_attempted_at = current_second
|
20
|
+
|
21
|
+
return unless compact_after < compaction_attempted_at
|
22
|
+
|
23
|
+
@store.delete_if { |_, value| value.expired_at?(compaction_attempted_at) }
|
24
|
+
|
25
|
+
set_next_compaction_time
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def set_next_compaction_time
|
31
|
+
@compact_after = current_second + frequency
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|