circuitbox 1.1.1 → 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 (37) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +53 -187
  3. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  4. data/lib/circuitbox/circuit_breaker.rb +134 -154
  5. data/lib/circuitbox/configuration.rb +51 -0
  6. data/lib/circuitbox/errors/error.rb +3 -2
  7. data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
  8. data/lib/circuitbox/errors/service_failure_error.rb +5 -1
  9. data/lib/circuitbox/excon_middleware.rb +23 -30
  10. data/lib/circuitbox/faraday_middleware.rb +43 -63
  11. data/lib/circuitbox/memory_store/container.rb +30 -0
  12. data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
  13. data/lib/circuitbox/memory_store.rb +85 -0
  14. data/lib/circuitbox/notifier/active_support.rb +19 -0
  15. data/lib/circuitbox/notifier/null.rb +13 -0
  16. data/lib/circuitbox/timer.rb +51 -0
  17. data/lib/circuitbox/version.rb +3 -1
  18. data/lib/circuitbox.rb +14 -54
  19. metadata +106 -117
  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 -30
  25. data/benchmark/circuit_store_benchmark.rb +0 -114
  26. data/circuitbox.gemspec +0 -48
  27. data/lib/circuitbox/notifier.rb +0 -34
  28. data/test/circuit_breaker_test.rb +0 -436
  29. data/test/circuitbox_test.rb +0 -45
  30. data/test/excon_middleware_test.rb +0 -131
  31. data/test/faraday_middleware_test.rb +0 -175
  32. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  33. data/test/integration/faraday_middleware_test.rb +0 -78
  34. data/test/integration_helper.rb +0 -48
  35. data/test/notifier_test.rb +0 -21
  36. data/test/service_failure_error_test.rb +0 -23
  37. data/test/test_helper.rb +0 -15
@@ -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,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,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,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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Circuitbox
4
+ class Notifier
5
+ class Null
6
+ def notify(_, _); end
7
+
8
+ def notify_warning(_, _); end
9
+
10
+ def metric_gauge(_, _, _); end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Circuitbox
4
+ class Timer
5
+ class Monotonic
6
+ class << self
7
+ def supported?
8
+ defined?(Process::CLOCK_MONOTONIC)
9
+ end
10
+
11
+ def now
12
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
+ end
14
+ end
15
+ end
16
+
17
+ class Default
18
+ class << self
19
+ def supported?
20
+ true
21
+ end
22
+
23
+ def now
24
+ Time.now.to_f
25
+ end
26
+ end
27
+ end
28
+
29
+ class << self
30
+ def measure(service, notifier, metric_name)
31
+ before = now
32
+ result = yield
33
+ total_time = now - before
34
+ notifier.metric_gauge(service, metric_name, total_time)
35
+ result
36
+ end
37
+
38
+ private
39
+
40
+ if Monotonic.supported?
41
+ def now
42
+ Monotonic.now
43
+ end
44
+ else
45
+ def now
46
+ Default.now
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
- VERSION='1.1.1'
4
+ VERSION = '2.0.0.pre4'
3
5
  end
data/lib/circuitbox.rb CHANGED
@@ -1,64 +1,24 @@
1
- require 'uri'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'logger'
3
- require 'timeout'
4
- require 'moneta'
5
- require 'active_support/all'
6
4
 
7
- require 'circuitbox/version'
8
- require 'circuitbox/circuit_breaker'
9
- require 'circuitbox/notifier'
10
- require 'circuitbox/errors/error'
11
- require 'circuitbox/errors/open_circuit_error'
12
- require 'circuitbox/errors/service_failure_error'
5
+ require_relative 'circuitbox/version'
6
+ require_relative 'circuitbox/circuit_breaker'
7
+ require_relative 'circuitbox/errors/error'
8
+ require_relative 'circuitbox/errors/open_circuit_error'
9
+ require_relative 'circuitbox/errors/service_failure_error'
10
+ require_relative 'circuitbox/configuration'
13
11
 
14
12
  class Circuitbox
15
- attr_accessor :circuits, :circuit_store
16
- cattr_accessor :configure
13
+ class << self
14
+ include Configuration
17
15
 
18
- def self.instance
19
- @@instance ||= new
20
- end
16
+ def circuit(service_name, options, &block)
17
+ circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
21
18
 
22
- def initialize
23
- self.instance_eval(&@@configure) if @@configure
24
- end
25
-
26
- def self.configure(&block)
27
- @@configure = block if block
28
- end
29
-
30
- def self.reset
31
- @@instance = nil
32
- @@configure = nil
33
- end
34
-
35
- def self.circuit_store
36
- self.instance.circuit_store ||= Moneta.new(:Memory, expires: true)
37
- end
19
+ return circuit unless block
38
20
 
39
- def self.circuit_store=(store)
40
- self.instance.circuit_store = store
41
- end
42
-
43
- def self.[](service_identifier, options = {})
44
- self.circuit(service_identifier, options)
45
- end
46
-
47
- def self.circuit(service_identifier, options = {})
48
- service_name = self.parameter_to_service_name(service_identifier)
49
-
50
- self.instance.circuits ||= Hash.new
51
- self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
52
-
53
- if block_given?
54
- self.instance.circuits[service_name].run { yield }
55
- else
56
- self.instance.circuits[service_name]
21
+ circuit.run(circuitbox_exceptions: false, &block)
57
22
  end
58
23
  end
59
-
60
- def self.parameter_to_service_name(param)
61
- uri = URI(param.to_s)
62
- uri.host.present? ? uri.host : param.to_s
63
- end
64
24
  end