circuitbox 1.0.3 → 2.0.0.pre3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -122
  3. data/lib/circuitbox.rb +12 -56
  4. data/lib/circuitbox/circuit_breaker.rb +133 -154
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +55 -0
  7. data/lib/circuitbox/errors/error.rb +1 -2
  8. data/lib/circuitbox/errors/open_circuit_error.rb +0 -1
  9. data/lib/circuitbox/errors/service_failure_error.rb +2 -1
  10. data/lib/circuitbox/excon_middleware.rb +15 -24
  11. data/lib/circuitbox/faraday_middleware.rb +38 -61
  12. data/lib/circuitbox/memory_store.rb +84 -0
  13. data/lib/circuitbox/memory_store/container.rb +28 -0
  14. data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
  15. data/lib/circuitbox/notifier/active_support.rb +19 -0
  16. data/lib/circuitbox/notifier/null.rb +11 -0
  17. data/lib/circuitbox/timer/monotonic.rb +17 -0
  18. data/lib/circuitbox/timer/null.rb +9 -0
  19. data/lib/circuitbox/timer/simple.rb +13 -0
  20. data/lib/circuitbox/version.rb +1 -1
  21. metadata +81 -149
  22. data/.gitignore +0 -20
  23. data/.ruby-version +0 -1
  24. data/.travis.yml +0 -8
  25. data/Gemfile +0 -6
  26. data/Rakefile +0 -18
  27. data/benchmark/circuit_store_benchmark.rb +0 -114
  28. data/circuitbox.gemspec +0 -38
  29. data/lib/circuitbox/memcache_store.rb +0 -31
  30. data/lib/circuitbox/notifier.rb +0 -34
  31. data/test/circuit_breaker_test.rb +0 -449
  32. data/test/circuitbox_test.rb +0 -54
  33. data/test/excon_middleware_test.rb +0 -131
  34. data/test/faraday_middleware_test.rb +0 -175
  35. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  36. data/test/integration/faraday_middleware_test.rb +0 -78
  37. data/test/integration_helper.rb +0 -48
  38. data/test/notifier_test.rb +0 -21
  39. data/test/service_failure_error_test.rb +0 -30
  40. 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,55 @@
1
+ require_relative 'memory_store'
2
+ require_relative 'timer/monotonic'
3
+ require_relative 'timer/null'
4
+ require_relative 'timer/simple'
5
+ require_relative 'notifier/active_support'
6
+ require_relative 'notifier/null'
7
+
8
+ class Circuitbox
9
+ module Configuration
10
+ attr_writer :default_circuit_store,
11
+ :default_notifier,
12
+ :default_timer,
13
+ :default_logger
14
+
15
+ def configure
16
+ yield self
17
+ clear_cached_circuits!
18
+ nil
19
+ end
20
+
21
+ def default_circuit_store
22
+ @default_circuit_store ||= MemoryStore.new
23
+ end
24
+
25
+ def default_notifier
26
+ @default_notifier ||= if defined?(ActiveSupport::Notifications)
27
+ Notifier::ActiveSupport.new
28
+ else
29
+ Notifier::Null.new
30
+ end
31
+ end
32
+
33
+ def default_timer
34
+ @default_timer ||= Timer::Simple.new
35
+ end
36
+
37
+ def default_logger
38
+ @default_logger ||= if defined?(Rails)
39
+ Rails.logger
40
+ else
41
+ Logger.new(STDOUT)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def cached_circuits
48
+ @cached_circuits ||= {}
49
+ end
50
+
51
+ def clear_cached_circuits!
52
+ @cached_circuits = {}
53
+ end
54
+ end
55
+ end
@@ -1,4 +1,3 @@
1
1
  class Circuitbox
2
- class Error < StandardError
3
- end
2
+ class Error < StandardError; end
4
3
  end
@@ -5,6 +5,5 @@ class Circuitbox
5
5
  def initialize(service)
6
6
  @service = service
7
7
  end
8
-
9
8
  end
10
9
  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
- set_backtrace(exception.backtrace) unless exception.backtrace.empty?
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!(run_options(datum)) do
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!(run_options(datum)) do
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!(run_options(datum)) do
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[:path] })
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
- 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
- )
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
- return @default_value if @default_value
99
-
100
- default = opts.fetch(:default_value) do
101
- lambda { |response, exception| NullResponse.new(response, exception) }
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,47 @@ class Circuitbox
19
14
  end
20
15
  end
21
16
 
22
- attr_reader :opts
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? || (500 <= response.status && response.status <= 599)
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 before 0.9.0 didn't have Faraday::TimeoutError so we default to Faraday::Error::TimeoutError
35
+ # Faraday >= 0.9.0 defines Faraday::TimeoutError and this can be used for all versions up to 1.0.0 that
36
+ # also define and raise Faraday::Error::TimeoutError as Faraday::TimeoutError is an ancestor
37
+ defined?(Faraday::TimeoutError) ? Faraday::TimeoutError : Faraday::Error::TimeoutError,
38
+ RequestFailed
39
+ ].freeze
40
+
41
+ DEFAULT_CIRCUIT_BREAKER_OPTIONS = {
42
+ exceptions: DEFAULT_EXCEPTIONS
43
+ }.freeze
44
+
45
+ attr_reader :opts
32
46
 
33
47
  def initialize(app, opts = {})
34
48
  @app = app
35
- @opts = DEFAULT_CIRCUITBOX_OPTIONS.merge(opts)
49
+ @opts = DEFAULT_OPTIONS.merge(opts)
50
+
51
+ @opts[:circuit_breaker_options] = DEFAULT_CIRCUIT_BREAKER_OPTIONS.merge(@opts[:circuit_breaker_options])
36
52
  super(app)
37
53
  end
38
54
 
39
55
  def call(request_env)
40
56
  service_response = nil
41
- circuit(request_env).run!(run_options(request_env)) do
57
+ circuit(request_env).run do
42
58
  @app.call(request_env).on_complete do |env|
43
59
  service_response = Faraday::Response.new(env)
44
60
  raise RequestFailed if open_circuit?(service_response)
@@ -48,67 +64,28 @@ class Circuitbox
48
64
  circuit_open_value(request_env, service_response, ex)
49
65
  end
50
66
 
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
67
  private
60
68
 
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
69
+ def call_default_value(response, exception)
70
+ default_value = opts[:default_value]
71
+ default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
86
72
  end
87
73
 
88
74
  def open_circuit?(response)
89
75
  opts[:open_circuit].call(response)
90
76
  end
91
77
 
92
- def circuitbox
93
- @circuitbox ||= opts.fetch(:circuitbox, Circuitbox)
94
- end
95
-
96
78
  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
79
+ env[:circuit_breaker_default_value] || call_default_value(service_response, exception)
106
80
  end
107
81
 
108
82
  def circuit(env)
83
+ identifier = opts[:identifier]
109
84
  id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
110
- circuitbox.circuit id, circuit_breaker_options
111
- end
112
85
 
86
+ Circuitbox.circuit(id, opts[:circuit_breaker_options])
87
+ end
113
88
  end
114
89
  end
90
+
91
+ Faraday::Middleware.register_middleware circuitbox: Circuitbox::FaradayMiddleware
@@ -0,0 +1,84 @@
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
+ container.value
77
+ end
78
+
79
+ def compact(current_time)
80
+ @store.delete_if { |_, value| value.expired_at?(current_time) }
81
+ @compact_after = current_time + @compaction_frequency
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'monotonic_time'
2
+
3
+ class Circuitbox
4
+ class MemoryStore
5
+ class Container
6
+ include MonotonicTime
7
+
8
+ attr_accessor :value
9
+
10
+ def initialize(value:, expiry: 0)
11
+ @value = value
12
+ expires_after(expiry)
13
+ end
14
+
15
+ def expired?
16
+ @expires_after > 0 && @expires_after < current_second
17
+ end
18
+
19
+ def expired_at?(clock_second)
20
+ @expires_after > 0 && @expires_after < clock_second
21
+ end
22
+
23
+ def expires_after(seconds = 0)
24
+ @expires_after = seconds.zero? ? seconds : current_second + seconds
25
+ end
26
+ end
27
+ end
28
+ end