circuitbox 1.1.1 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|