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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +60 -122
  3. data/lib/circuitbox.rb +15 -52
  4. data/lib/circuitbox/circuit_breaker.rb +131 -141
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +53 -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 +35 -61
  12. data/lib/circuitbox/memory_store.rb +76 -0
  13. data/lib/circuitbox/memory_store/compactor.rb +35 -0
  14. data/lib/circuitbox/memory_store/container.rb +28 -0
  15. data/lib/circuitbox/memory_store/monotonic_time.rb +11 -0
  16. data/lib/circuitbox/notifier/active_support.rb +19 -0
  17. data/lib/circuitbox/notifier/null.rb +11 -0
  18. data/lib/circuitbox/timer/monotonic.rb +17 -0
  19. data/lib/circuitbox/timer/null.rb +9 -0
  20. data/lib/circuitbox/timer/simple.rb +13 -0
  21. data/lib/circuitbox/version.rb +1 -1
  22. metadata +78 -150
  23. data/.gitignore +0 -20
  24. data/.ruby-version +0 -1
  25. data/.travis.yml +0 -9
  26. data/Gemfile +0 -6
  27. data/Rakefile +0 -30
  28. data/benchmark/circuit_store_benchmark.rb +0 -114
  29. data/circuitbox.gemspec +0 -48
  30. data/lib/circuitbox/notifier.rb +0 -34
  31. data/test/circuit_breaker_test.rb +0 -436
  32. data/test/circuitbox_test.rb +0 -45
  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 -23
  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,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
@@ -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,44 @@ 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::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 = DEFAULT_CIRCUITBOX_OPTIONS.merge(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!(run_options(request_env)) do
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 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
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] || 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
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