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