shift-circuit-breaker 0.2.0
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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +11 -0
- data/.gitignore +13 -0
- data/.reek +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/CONTRIBUTING.md +80 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/Rakefile +9 -0
- data/bin/console +7 -0
- data/bin/setup +7 -0
- data/config/newrelic.yml +28 -0
- data/config/rubocop/.layout_rubocop.yml +2 -0
- data/config/rubocop/.lint_rubocop.yml +47 -0
- data/config/rubocop/.metrics_rubocop.yml +39 -0
- data/config/rubocop/.naming_rubocop.yml +11 -0
- data/config/rubocop/.performance_rubocop.yml +60 -0
- data/config/rubocop/.style_rubocop.yml +154 -0
- data/lib/shift/circuit_breaker.rb +57 -0
- data/lib/shift/circuit_breaker/adapters/base_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/adapters/newrelic_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/adapters/sentry_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/circuit_handler.rb +107 -0
- data/lib/shift/circuit_breaker/circuit_logger.rb +36 -0
- data/lib/shift/circuit_breaker/circuit_monitor.rb +32 -0
- data/lib/shift/circuit_breaker/config.rb +48 -0
- data/lib/shift/circuit_breaker/version.rb +7 -0
- data/shift-circuit-breaker.gemspec +40 -0
- data/spec/shift/adapters/base_adapter_spec.rb +21 -0
- data/spec/shift/adapters/newrelic_adapter_spec.rb +44 -0
- data/spec/shift/adapters/sentry_adapter_spec.rb +42 -0
- data/spec/shift/circuit_breaker/circuit_handler_exception_handling_spec.rb +142 -0
- data/spec/shift/circuit_breaker/circuit_handler_monitoring_spec.rb +72 -0
- data/spec/shift/circuit_breaker/circuit_handler_spec.rb +165 -0
- data/spec/shift/circuit_breaker/circuit_logger_spec.rb +67 -0
- data/spec/shift/circuit_breaker/circuit_monitor_spec.rb +52 -0
- data/spec/shift/circuit_breaker_spec.rb +73 -0
- data/spec/spec_helper.rb +25 -0
- metadata +230 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
Style/Alias:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Style/ArrayJoin:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Style/AsciiComments:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/Attr:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/BlockComments:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/CaseEquality:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Style/CharacterLiteral:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Style/ClassAndModuleChildren:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Style/ClassVars:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Style/CollectionMethods:
|
29
|
+
Enabled: true
|
30
|
+
PreferredMethods:
|
31
|
+
length: "size"
|
32
|
+
|
33
|
+
Style/ColonMethodCall:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Style/CommentAnnotation:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Style/Documentation:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Style/DoubleNegation:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
Style/EachWithObject:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Style/EmptyLiteral:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Style/EvenOdd:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Style/FlipFlop:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Style/FormatString:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Style/GlobalVars:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
Style/GuardClause:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
Style/IfUnlessModifier:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/IfWithSemicolon:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Style/Lambda:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Style/LambdaCall:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Style/LineEndConcatenation:
|
79
|
+
Enabled: false
|
80
|
+
|
81
|
+
Style/ModuleFunction:
|
82
|
+
Enabled: false
|
83
|
+
|
84
|
+
Style/NegatedIf:
|
85
|
+
Enabled: false
|
86
|
+
|
87
|
+
Style/NegatedWhile:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
Style/Next:
|
91
|
+
Enabled: false
|
92
|
+
|
93
|
+
Style/NilComparison:
|
94
|
+
Enabled: false
|
95
|
+
|
96
|
+
Style/Not:
|
97
|
+
Enabled: false
|
98
|
+
|
99
|
+
Style/NumericLiterals:
|
100
|
+
Enabled: false
|
101
|
+
|
102
|
+
Style/OneLineConditional:
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
Style/PercentLiteralDelimiters:
|
106
|
+
Enabled: false
|
107
|
+
|
108
|
+
Style/PerlBackrefs:
|
109
|
+
Enabled: false
|
110
|
+
|
111
|
+
Style/Proc:
|
112
|
+
Enabled: false
|
113
|
+
|
114
|
+
Style/RaiseArgs:
|
115
|
+
Enabled: false
|
116
|
+
|
117
|
+
Style/RegexpLiteral:
|
118
|
+
Enabled: false
|
119
|
+
|
120
|
+
Style/SelfAssignment:
|
121
|
+
Enabled: false
|
122
|
+
|
123
|
+
Style/SignalException:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
Style/SingleLineMethods:
|
127
|
+
Enabled: false
|
128
|
+
|
129
|
+
Style/SpecialGlobalVars:
|
130
|
+
Enabled: false
|
131
|
+
|
132
|
+
Style/StringLiterals:
|
133
|
+
EnforcedStyle: double_quotes
|
134
|
+
|
135
|
+
Style/TrailingCommaInArguments:
|
136
|
+
Enabled: false
|
137
|
+
|
138
|
+
Style/TrailingCommaInLiteral:
|
139
|
+
Enabled: false
|
140
|
+
|
141
|
+
Style/TrivialAccessors:
|
142
|
+
Enabled: false
|
143
|
+
|
144
|
+
Style/VariableInterpolation:
|
145
|
+
Enabled: false
|
146
|
+
|
147
|
+
Style/WhenThen:
|
148
|
+
Enabled: false
|
149
|
+
|
150
|
+
Style/WhileUntilModifier:
|
151
|
+
Enabled: false
|
152
|
+
|
153
|
+
Style/WordArray:
|
154
|
+
Enabled: false
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/string/inflections"
|
6
|
+
require "faraday"
|
7
|
+
require "logger"
|
8
|
+
require "net/protocol"
|
9
|
+
require "sentry-raven"
|
10
|
+
require "singleton"
|
11
|
+
require "timeout"
|
12
|
+
|
13
|
+
require "shift/circuit_breaker/adapters/base_adapter"
|
14
|
+
require "shift/circuit_breaker/adapters/sentry_adapter"
|
15
|
+
require "shift/circuit_breaker/adapters/newrelic_adapter"
|
16
|
+
require "shift/circuit_breaker/config"
|
17
|
+
require "shift/circuit_breaker/circuit_logger"
|
18
|
+
require "shift/circuit_breaker/circuit_monitor"
|
19
|
+
require "shift/circuit_breaker/circuit_handler"
|
20
|
+
require "shift/circuit_breaker/version"
|
21
|
+
|
22
|
+
module Shift
|
23
|
+
#
|
24
|
+
# ==== Example Usage:
|
25
|
+
#
|
26
|
+
# class MyClass
|
27
|
+
# CIRCUIT_BREAKER = Shift::CircuitBreaker.new(:an_identifier_for_the_circuit,
|
28
|
+
# error_threshold: 10,
|
29
|
+
# skip_duration: 60,
|
30
|
+
# additional_exception_classes: [Faraday::ClientError]
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
# def do_something
|
34
|
+
# # Note: operation and fallback should implement the public method #call or wrapped in a Proc/Lambda (as in the example below).
|
35
|
+
# CIRCUIT_BREAKER.call(operation: -> { SomeService.new(name: 'test').perform_task }, fallback: -> { [1, 2, 3, 4, 5].sum })
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
module CircuitBreaker
|
40
|
+
class << self
|
41
|
+
def new(*args)
|
42
|
+
Shift::CircuitBreaker::CircuitHandler.new(*args)
|
43
|
+
end
|
44
|
+
|
45
|
+
def config
|
46
|
+
@config ||= Shift::CircuitBreaker::Config.instance
|
47
|
+
end
|
48
|
+
|
49
|
+
def configure
|
50
|
+
@config = Shift::CircuitBreaker::Config.instance.tap do |config|
|
51
|
+
yield config if block_given?
|
52
|
+
end
|
53
|
+
@config.initialize_dependencies
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shift
|
4
|
+
module CircuitBreaker
|
5
|
+
#
|
6
|
+
# === Overview
|
7
|
+
#
|
8
|
+
# Implements a generic mechanism for detecting external service call timeouts and reduces the
|
9
|
+
# time spent waiting for further requests that will most-likely fail (e.g. timeout) and cause request queueing.
|
10
|
+
#
|
11
|
+
# Similar to a conventional circuit breaker, when a circuit is closed it allows operations
|
12
|
+
# to flow through. When the error_threshold is exceeded (tripped), the circuit is then opened for
|
13
|
+
# the defined skip_duration, ie. no operations are executed and the provided fallback is called.
|
14
|
+
#
|
15
|
+
class CircuitHandler
|
16
|
+
attr_accessor :name, :error_threshold, :skip_duration, :exception_classes, :error_count, :last_error_time, :state, :logger, :monitor
|
17
|
+
|
18
|
+
DEFAULT_EXCEPTION_CLASSES = [Net::OpenTimeout, Net::ReadTimeout, Faraday::TimeoutError, Timeout::Error].freeze
|
19
|
+
|
20
|
+
# Initializer creates an instance of the service
|
21
|
+
#
|
22
|
+
# @param [Symbol] name - the name used to identify the circuit breaker
|
23
|
+
# @param [Integer] error_threshold - The minimum error threshold required for the circuit to be opened/tripped
|
24
|
+
# @param [Integer] skip_duration - The duration in seconds the circuit should be open for before operations are allowed through/executed
|
25
|
+
# @param [Array] additional_exception_classes - Any additional exception classes to rescue along the DEFAULT_EXCEPTION_CLASSES
|
26
|
+
# @param [Object] logger - service to handle error logging
|
27
|
+
# @param [Object] monitor - service to monitor metric
|
28
|
+
def initialize(name,
|
29
|
+
error_threshold:,
|
30
|
+
skip_duration:,
|
31
|
+
additional_exception_classes: [],
|
32
|
+
logger: Shift::CircuitBreaker::CircuitLogger.new,
|
33
|
+
monitor: Shift::CircuitBreaker::CircuitMonitor.new)
|
34
|
+
|
35
|
+
self.name = name
|
36
|
+
self.error_threshold = error_threshold
|
37
|
+
self.skip_duration = skip_duration
|
38
|
+
self.exception_classes = (additional_exception_classes | DEFAULT_EXCEPTION_CLASSES)
|
39
|
+
self.logger = logger
|
40
|
+
self.monitor = monitor
|
41
|
+
self.error_count = 0
|
42
|
+
self.last_error_time = nil
|
43
|
+
self.state = :closed
|
44
|
+
end
|
45
|
+
|
46
|
+
# Performs the given operation within the circuit
|
47
|
+
# @param [Proc] operation - the operation to be performed
|
48
|
+
# @param [Proc] fallback - The result returned if the operation is not performed or raises an exception
|
49
|
+
def call(operation:, fallback:)
|
50
|
+
raise ArgumentError unless operation.respond_to?(:call) && fallback.respond_to?(:call)
|
51
|
+
set_state
|
52
|
+
if state == :open
|
53
|
+
monitor.record_metric(name, state)
|
54
|
+
return fallback.call
|
55
|
+
end
|
56
|
+
perform_operation(operation, fallback)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_state
|
62
|
+
# The curcuit is opened/tripped if the error_threshold is met or exceeded
|
63
|
+
# (error_count >= error_threshold) and the last_error_time is within
|
64
|
+
# the skip_duration (see comments in #skip_duration_expired?).
|
65
|
+
self.state = (error_count >= error_threshold) && !skip_duration_expired? ? :open : :closed
|
66
|
+
end
|
67
|
+
|
68
|
+
def skip_duration_expired?
|
69
|
+
return true unless last_error_time.present?
|
70
|
+
# IF the difference in time between now and the last_error_time
|
71
|
+
# is greater than the skip_duration, then it will have expired.
|
72
|
+
(Time.now - last_error_time) > skip_duration
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset_state
|
76
|
+
# Reset the error attributes to default values.
|
77
|
+
self.error_count = 0
|
78
|
+
self.last_error_time = nil
|
79
|
+
self.state = :closed
|
80
|
+
end
|
81
|
+
|
82
|
+
def record_error
|
83
|
+
# Increment the error_count
|
84
|
+
self.error_count += 1
|
85
|
+
# Set the time the error occured.
|
86
|
+
self.last_error_time = Time.now
|
87
|
+
end
|
88
|
+
|
89
|
+
def perform_operation(operation, fallback)
|
90
|
+
response = operation.call
|
91
|
+
reset_state
|
92
|
+
monitor.record_metric(name, state)
|
93
|
+
response
|
94
|
+
rescue *exception_classes
|
95
|
+
handle_exception($!, fallback)
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_exception(exception, fallback)
|
99
|
+
record_error
|
100
|
+
set_state
|
101
|
+
monitor.record_metric(name, state)
|
102
|
+
logger.error(circuit_name: name, state: state, error_message: exception.message)
|
103
|
+
fallback.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shift
|
4
|
+
module CircuitBreaker
|
5
|
+
class CircuitLogger
|
6
|
+
attr_accessor :logger, :remote_logger
|
7
|
+
|
8
|
+
delegate :debug, :fatal, :info, :warn, :add, :log, to: :logger
|
9
|
+
|
10
|
+
ERROR_MESSAGE = <<~EOF
|
11
|
+
====================================================================================
|
12
|
+
CIRCUIT BREAKER REQUEST FAILURE: %<circuit_name>s
|
13
|
+
|
14
|
+
STATE: %<state>s
|
15
|
+
MESSAGE: %<error_message>s
|
16
|
+
====================================================================================
|
17
|
+
EOF
|
18
|
+
|
19
|
+
# Initializer creates an instance of the service
|
20
|
+
#
|
21
|
+
# @param [Object] logger - service to handle internal logging
|
22
|
+
# @param [Object] remote_logger - external error logging service eg. Sentry
|
23
|
+
def initialize(logger: ::Logger.new(STDOUT), remote_logger: Shift::CircuitBreaker::Adapters::SentryAdapter)
|
24
|
+
self.logger = logger
|
25
|
+
self.remote_logger = remote_logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Object] context - contains :circuit_name, :state, :error_message
|
29
|
+
def error(context)
|
30
|
+
message = (ERROR_MESSAGE % context)
|
31
|
+
logger.error(message)
|
32
|
+
remote_logger.call(message) if remote_logger.respond_to?(:call)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shift
|
4
|
+
module CircuitBreaker
|
5
|
+
class CircuitMonitor
|
6
|
+
attr_accessor :monitor, :logger
|
7
|
+
|
8
|
+
# Initializer creates an instance of the service
|
9
|
+
#
|
10
|
+
# @param [Object] monitor - service to handle monitoring
|
11
|
+
# @param [Object] logger - service to handle logging
|
12
|
+
def initialize(monitor: Shift::CircuitBreaker::Adapters::NewRelicAdapter, logger: Shift::CircuitBreaker::CircuitLogger.new)
|
13
|
+
self.monitor = monitor
|
14
|
+
self.logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [String] name - The circuit name
|
18
|
+
# @param [Symbol] state - The circuit state
|
19
|
+
def record_metric(name, state)
|
20
|
+
metric = formatted_metric(name, state)
|
21
|
+
monitor.call(metric) if monitor.respond_to?(:call)
|
22
|
+
logger.info("* #{metric} *")
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def formatted_metric(name, state)
|
28
|
+
"Custom/#{name.to_s.classify}CircuitBreaker/#{state.to_s.classify}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shift
|
4
|
+
module CircuitBreaker
|
5
|
+
#
|
6
|
+
# Global Configuration Object
|
7
|
+
#
|
8
|
+
# ==== Example Usage:
|
9
|
+
#
|
10
|
+
# Add an initializer in your application (eg. shift_circuit_breaker.rb)
|
11
|
+
# with the following configs:
|
12
|
+
#
|
13
|
+
# Shift::CircuitBreaker.configure do |config|
|
14
|
+
# config.new_relic_license_key = ENV["NEW_RELIC_LICENSE_KEY"]
|
15
|
+
# config.new_relic_app_name = ENV["NEW_RELIC_APP_NAME"]
|
16
|
+
# config.sentry_dsn = ENV["SENTRY_DSN"]
|
17
|
+
# config.sentry_environments = %w[ production ]
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
class Config
|
21
|
+
include Singleton
|
22
|
+
|
23
|
+
attr_accessor :new_relic_license_key, :new_relic_app_name, :sentry_dsn, :sentry_environments
|
24
|
+
|
25
|
+
def initialize_dependencies
|
26
|
+
initialize_sentry
|
27
|
+
initialize_newrelic
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def initialize_sentry
|
33
|
+
if sentry_dsn
|
34
|
+
Raven.configure do |config|
|
35
|
+
config.dsn = sentry_dsn
|
36
|
+
config.environments = sentry_environments if sentry_environments.present?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize_newrelic
|
42
|
+
if new_relic_app_name.present? && new_relic_license_key.present?
|
43
|
+
require "newrelic_rpm"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|