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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -187
  3. data/lib/circuitbox.rb +14 -57
  4. data/lib/circuitbox/circuit_breaker.rb +137 -161
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +51 -0
  7. data/lib/circuitbox/errors/error.rb +3 -2
  8. data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
  9. data/lib/circuitbox/errors/service_failure_error.rb +5 -1
  10. data/lib/circuitbox/excon_middleware.rb +23 -30
  11. data/lib/circuitbox/faraday_middleware.rb +43 -63
  12. data/lib/circuitbox/memory_store.rb +85 -0
  13. data/lib/circuitbox/memory_store/container.rb +30 -0
  14. data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
  15. data/lib/circuitbox/notifier/active_support.rb +19 -0
  16. data/lib/circuitbox/notifier/null.rb +13 -0
  17. data/lib/circuitbox/timer.rb +51 -0
  18. data/lib/circuitbox/version.rb +3 -1
  19. metadata +106 -118
  20. data/.gitignore +0 -20
  21. data/.ruby-version +0 -1
  22. data/.travis.yml +0 -9
  23. data/Gemfile +0 -6
  24. data/Rakefile +0 -18
  25. data/benchmark/circuit_store_benchmark.rb +0 -114
  26. data/circuitbox.gemspec +0 -48
  27. data/lib/circuitbox/memcache_store.rb +0 -31
  28. data/lib/circuitbox/notifier.rb +0 -34
  29. data/test/circuit_breaker_test.rb +0 -428
  30. data/test/circuitbox_test.rb +0 -45
  31. data/test/excon_middleware_test.rb +0 -131
  32. data/test/faraday_middleware_test.rb +0 -175
  33. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  34. data/test/integration/faraday_middleware_test.rb +0 -78
  35. data/test/integration_helper.rb +0 -48
  36. data/test/notifier_test.rb +0 -21
  37. data/test/service_failure_error_test.rb +0 -23
  38. data/test/test_helper.rb +0 -15
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
- class Error < StandardError
3
- end
4
+ class Error < StandardError; end
4
5
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
4
  class OpenCircuitError < Circuitbox::Error
3
5
  attr_reader :service
4
6
 
5
7
  def initialize(service)
8
+ super()
6
9
  @service = service
7
10
  end
8
-
9
11
  end
10
12
  end
@@ -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
- set_backtrace(exception.backtrace) unless exception.backtrace.empty?
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: lambda { |response| response[:status] >= 400 } }
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!(run_options(datum)) do
37
+ circuit(datum).run do
36
38
  raise RequestFailed
37
39
  end
38
- rescue Circuitbox::Error => exception
39
- circuit_open_value(datum, datum[:response], exception)
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!(run_options(datum)) do
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!(run_options(datum)) do
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 => exception
54
- circuit_open_value(datum, datum[:response], exception)
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[:path] })
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
- return @circuit_breaker_options if @circuit_breaker_options
90
-
91
- @circuit_breaker_options = opts.fetch(:circuit_breaker_options, {})
92
- @circuit_breaker_options.merge!(
93
- exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
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
- return @default_value if @default_value
99
-
100
- default = opts.fetch(:default_value) do
101
- lambda { |response, exception| NullResponse.new(response, exception) }
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
- attr_reader :opts
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? || (500 <= response.status && response.status <= 599)
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 = DEFAULT_CIRCUITBOX_OPTIONS.merge(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!(run_options(request_env)) do
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 => ex
48
- circuit_open_value(request_env, service_response, ex)
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 run_options(env)
62
- env[:circuit_breaker_run_options] || {}
63
- end
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] || call_default_lambda(service_response, exception)
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Circuitbox
4
+ class MemoryStore
5
+ module MonotonicTime
6
+ module_function
7
+
8
+ def current_second
9
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
10
+ end
11
+ end
12
+ end
13
+ 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