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.
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