stoplight 3.0.0 → 4.0.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 +4 -4
- data/README.md +176 -198
- data/lib/stoplight/builder.rb +68 -0
- data/lib/stoplight/circuit_breaker.rb +92 -0
- data/lib/stoplight/configurable.rb +95 -0
- data/lib/stoplight/configuration.rb +126 -0
- data/lib/stoplight/data_store/base.rb +9 -0
- data/lib/stoplight/data_store/memory.rb +46 -5
- data/lib/stoplight/data_store/redis.rb +75 -6
- data/lib/stoplight/default.rb +2 -0
- data/lib/stoplight/error.rb +1 -0
- data/lib/stoplight/light/deprecated.rb +44 -0
- data/lib/stoplight/light/lockable.rb +45 -0
- data/lib/stoplight/light/runnable.rb +34 -16
- data/lib/stoplight/light.rb +69 -63
- data/lib/stoplight/rspec/generic_notifier.rb +42 -0
- data/lib/stoplight/rspec.rb +3 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +33 -10
- data/spec/spec_helper.rb +7 -0
- data/spec/stoplight/builder_spec.rb +165 -0
- data/spec/stoplight/circuit_breaker_spec.rb +35 -0
- data/spec/stoplight/configurable_spec.rb +25 -0
- data/spec/stoplight/data_store/base_spec.rb +7 -0
- data/spec/stoplight/data_store/memory_spec.rb +12 -123
- data/spec/stoplight/data_store/redis_spec.rb +28 -129
- data/spec/stoplight/error_spec.rb +10 -0
- data/spec/stoplight/light/lockable_spec.rb +93 -0
- data/spec/stoplight/light/runnable_spec.rb +12 -233
- data/spec/stoplight/light_spec.rb +4 -28
- data/spec/stoplight/notifier/generic_spec.rb +35 -35
- data/spec/stoplight/notifier/io_spec.rb +1 -0
- data/spec/stoplight/notifier/logger_spec.rb +3 -0
- data/spec/stoplight_spec.rb +17 -6
- data/spec/support/configurable.rb +69 -0
- data/spec/support/data_store/base/clear_failures.rb +18 -0
- data/spec/support/data_store/base/clear_state.rb +20 -0
- data/spec/support/data_store/base/get_all.rb +44 -0
- data/spec/support/data_store/base/get_failures.rb +30 -0
- data/spec/support/data_store/base/get_state.rb +7 -0
- data/spec/support/data_store/base/names.rb +29 -0
- data/spec/support/data_store/base/record_failures.rb +70 -0
- data/spec/support/data_store/base/set_state.rb +15 -0
- data/spec/support/data_store/base/with_notification_lock.rb +27 -0
- data/spec/support/data_store/base.rb +21 -0
- data/spec/support/database_cleaner.rb +26 -0
- data/spec/support/exception_helpers.rb +9 -0
- data/spec/support/light/runnable/color.rb +79 -0
- data/spec/support/light/runnable/run.rb +247 -0
- data/spec/support/light/runnable.rb +4 -0
- metadata +56 -225
- data/lib/stoplight/notifier/bugsnag.rb +0 -37
- data/lib/stoplight/notifier/hip_chat.rb +0 -43
- data/lib/stoplight/notifier/honeybadger.rb +0 -44
- data/lib/stoplight/notifier/pagerduty.rb +0 -21
- data/lib/stoplight/notifier/raven.rb +0 -40
- data/lib/stoplight/notifier/rollbar.rb +0 -39
- data/lib/stoplight/notifier/slack.rb +0 -21
- data/spec/stoplight/notifier/bugsnag_spec.rb +0 -90
- data/spec/stoplight/notifier/hip_chat_spec.rb +0 -91
- data/spec/stoplight/notifier/honeybadger_spec.rb +0 -88
- data/spec/stoplight/notifier/pagerduty_spec.rb +0 -40
- data/spec/stoplight/notifier/raven_spec.rb +0 -90
- data/spec/stoplight/notifier/rollbar_spec.rb +0 -90
- data/spec/stoplight/notifier/slack_spec.rb +0 -46
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Stoplight
|
6
|
+
# An interface to build Stoplight configuration. The builder is
|
7
|
+
# immutable, so it's safe to pass an instance of this builder
|
8
|
+
# across the code.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# circuit_breaker = Stoplight('http_api')
|
12
|
+
# .with_data_store(data_store)
|
13
|
+
# .with_cool_off_time(60)
|
14
|
+
# .with_threshold(5)
|
15
|
+
# .with_window_size(3600)
|
16
|
+
# .with_notifiers(notifiers)
|
17
|
+
# .with_error_notifier(error_notifier) #=> <#Stoplight::Builder ..>
|
18
|
+
#
|
19
|
+
# It's safe to pass this +circuit_breaker+ around your code like this:
|
20
|
+
#
|
21
|
+
# def call(circuit_breaker)
|
22
|
+
# circuit_breaker.run { call_api }
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @api private use +Stoplight()+ method instead
|
26
|
+
class Builder
|
27
|
+
include CircuitBreaker
|
28
|
+
extend Forwardable
|
29
|
+
|
30
|
+
def_delegator :build, :with_error_handler
|
31
|
+
def_delegator :build, :with_fallback
|
32
|
+
def_delegator :build, :color
|
33
|
+
def_delegator :build, :run
|
34
|
+
def_delegator :build, :lock
|
35
|
+
def_delegator :build, :unlock
|
36
|
+
|
37
|
+
class << self
|
38
|
+
# @param settings [Hash]
|
39
|
+
# @see +Stoplight::Configuration#initialize+
|
40
|
+
# @return [Stoplight::Builder]
|
41
|
+
def with(**settings)
|
42
|
+
new Configuration.new(**settings)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [Stoplight::Configuration]
|
47
|
+
def initialize(configuration)
|
48
|
+
@configuration = configuration
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Stoplight::Light]
|
52
|
+
def build(&code)
|
53
|
+
Light.new(configuration.name, configuration, &code)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param other [any]
|
57
|
+
# @return [Boolean]
|
58
|
+
def ==(other)
|
59
|
+
other.is_a?(self.class) && configuration == other.configuration
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def reconfigure(configuration)
|
65
|
+
self.class.new(configuration)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
# @abstract
|
5
|
+
module CircuitBreaker
|
6
|
+
include Configurable
|
7
|
+
|
8
|
+
# Configures a custom proc that allows you to not to handle an error
|
9
|
+
# with Stoplight.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# light = Stoplight('example')
|
13
|
+
# .with_error_handler do |error, handler|
|
14
|
+
# raise error if error.is_a?(ActiveRecord::RecordNotFound)
|
15
|
+
# handle.call(error)
|
16
|
+
# end
|
17
|
+
# light.run { User.find(123) }
|
18
|
+
#
|
19
|
+
# In the example above, the +ActiveRecord::RecordNotFound+ doesn't
|
20
|
+
# move the circuit breaker into the red state.
|
21
|
+
#
|
22
|
+
# @yieldparam error [Exception]
|
23
|
+
# @yieldparam handle [Proc]
|
24
|
+
# @return [Stoplight::CircuitBreaker]
|
25
|
+
def with_error_handler(&error_handler)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
# Configures light with the given fallback block
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# light = Stoplight('example')
|
33
|
+
# light.with_fallback { |error| e.is_a?()ZeroDivisionError) ? 0 : nil }
|
34
|
+
# light.run { 1 / 0} #=> 0
|
35
|
+
#
|
36
|
+
# @yieldparam error [Exception, nil]
|
37
|
+
# @return [Stoplight::CircuitBreaker]
|
38
|
+
def with_fallback(&fallback)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns current color:
|
43
|
+
# * +Stoplight::Color::GREEN+ -- circuit breaker is closed
|
44
|
+
# * +Stoplight::Color::RED+ -- circuit breaker is open
|
45
|
+
# * +Stoplight::Color::YELLOW+ -- circuit breaker is half-open
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# light = Stoplight('example')
|
49
|
+
# light.color #=> Color::GREEN
|
50
|
+
#
|
51
|
+
# @return [String] returns current light color
|
52
|
+
def color
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
# Runs the given block of code with this circuit breaker
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# light = Stoplight('example')
|
60
|
+
# light.run { 2/0 }
|
61
|
+
#
|
62
|
+
# @raise [Stoplight::Error::RedLight]
|
63
|
+
# @return [any]
|
64
|
+
def run(&code)
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# light = Stoplight('example-locked')
|
72
|
+
# light.lock(Stoplight::Color::RED)
|
73
|
+
#
|
74
|
+
# @param color [String] should be either +Color::RED+ or +Color::GREEN+
|
75
|
+
# @return [Stoplight::CircuitBreaker] returns locked circuit breaker
|
76
|
+
def lock(color)
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
80
|
+
# Unlocks light and sets it's state to State::UNLOCKED
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# light = Stoplight('example-locked')
|
84
|
+
# light.lock(Stoplight::Color::RED)
|
85
|
+
# light.unlock
|
86
|
+
#
|
87
|
+
# @return [Stoplight::CircuitBreaker] returns unlocked circuit breaker
|
88
|
+
def unlock
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
# @api private
|
5
|
+
# @abstract include the module and define +#reconfigure+ method
|
6
|
+
module Configurable
|
7
|
+
# @!attribute [r] configuration
|
8
|
+
# @return [Stoplight::Configuration]
|
9
|
+
# @api private
|
10
|
+
attr_reader :configuration
|
11
|
+
|
12
|
+
# Configures data store to be used with this circuit breaker
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# Stoplight('example')
|
16
|
+
# .with_data_store(Stoplight::DataStore::Memory.new)
|
17
|
+
#
|
18
|
+
# @param data_store [DataStore::Base]
|
19
|
+
# @return [Stoplight::CircuitBreaker]
|
20
|
+
def with_data_store(data_store)
|
21
|
+
reconfigure(configuration.with(data_store: data_store))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Configures cool off time. Stoplight automatically tries to recover
|
25
|
+
# from the red state after the cool off time.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Stoplight('example')
|
29
|
+
# .cool_off_time(60)
|
30
|
+
#
|
31
|
+
# @param cool_off_time [Numeric] number of seconds
|
32
|
+
# @return [Stoplight::CircuitBreaker]
|
33
|
+
def with_cool_off_time(cool_off_time)
|
34
|
+
reconfigure(configuration.with(cool_off_time: cool_off_time))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Configures custom threshold. After this number of failures Stoplight
|
38
|
+
# switches to the red state:
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Stoplight('example')
|
42
|
+
# .with_threshold(5)
|
43
|
+
#
|
44
|
+
# @param threshold [Numeric]
|
45
|
+
# @return [Stoplight::CircuitBreaker]
|
46
|
+
def with_threshold(threshold)
|
47
|
+
reconfigure(configuration.with(threshold: threshold))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Configures custom window size which Stoplight uses to count failures. For example,
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# Stoplight('example')
|
54
|
+
# .with_threshold(5)
|
55
|
+
# .with_window_size(60)
|
56
|
+
#
|
57
|
+
# The above example will turn to red light only when 5 errors happen
|
58
|
+
# within 60 seconds period.
|
59
|
+
#
|
60
|
+
# @param window_size [Numeric] number of seconds
|
61
|
+
# @return [Stoplight::CircuitBreaker]
|
62
|
+
def with_window_size(window_size)
|
63
|
+
reconfigure(configuration.with(window_size: window_size))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Configures custom notifier
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# io = StringIO.new
|
70
|
+
# notifier = Stoplight::Notifier::IO.new(io)
|
71
|
+
# Stoplight('example')
|
72
|
+
# .with_notifiers([notifier])
|
73
|
+
#
|
74
|
+
# @param notifiers [Array<Notifier::Base>]
|
75
|
+
# @return [Stoplight::CircuitBreaker]
|
76
|
+
def with_notifiers(notifiers)
|
77
|
+
reconfigure(configuration.with(notifiers: notifiers))
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param error_notifier [Proc]
|
81
|
+
# @return [Stoplight::CircuitBreaker]
|
82
|
+
# @api private
|
83
|
+
def with_error_notifier(&error_notifier)
|
84
|
+
reconfigure(configuration.with(error_notifier: error_notifier))
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# @param [Stoplight::Configuration]
|
90
|
+
# @return [Stoplight::CircuitBreaker]
|
91
|
+
def reconfigure(_configuration)
|
92
|
+
raise NotImplementedError, "#{self.class.name}#reconfigure is not implemented"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
# A +Stoplight::Light+ configuration object.
|
5
|
+
class Configuration
|
6
|
+
class << self
|
7
|
+
alias __new_without_defaults__ new
|
8
|
+
|
9
|
+
# It overrides the +Configuration.new+ to inject default settings
|
10
|
+
# @see +Stoplight::Configuration#initialize+
|
11
|
+
def new(**settings)
|
12
|
+
__new_without_defaults__(
|
13
|
+
**default_settings.merge(settings)
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @return [Hash]
|
20
|
+
def default_settings
|
21
|
+
{
|
22
|
+
cool_off_time: Default::COOL_OFF_TIME,
|
23
|
+
data_store: Stoplight.default_data_store,
|
24
|
+
error_notifier: Stoplight.default_error_notifier,
|
25
|
+
notifiers: Stoplight.default_notifiers,
|
26
|
+
threshold: Default::THRESHOLD,
|
27
|
+
window_size: Default::WINDOW_SIZE
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @!attribute [r] name
|
33
|
+
# @return [String]
|
34
|
+
attr_reader :name
|
35
|
+
|
36
|
+
# @!attribute [r] cool_off_time
|
37
|
+
# @return [Numeric]
|
38
|
+
attr_reader :cool_off_time
|
39
|
+
|
40
|
+
# @!attribute [r] data_store
|
41
|
+
# @return [Stoplight::DataStore::Base]
|
42
|
+
attr_reader :data_store
|
43
|
+
|
44
|
+
# @!attribute [r] error_notifier
|
45
|
+
# # @return [StandardError => void]
|
46
|
+
attr_reader :error_notifier
|
47
|
+
|
48
|
+
# @!attribute [r] notifiers
|
49
|
+
# # @return [Array<Notifier::Base>]
|
50
|
+
attr_reader :notifiers
|
51
|
+
|
52
|
+
# @!attribute [r] threshold
|
53
|
+
# @return [Numeric]
|
54
|
+
attr_reader :threshold
|
55
|
+
|
56
|
+
# @!attribute [r] window_size
|
57
|
+
# @return [Numeric]
|
58
|
+
attr_reader :window_size
|
59
|
+
|
60
|
+
# @param name [String]
|
61
|
+
# @param cool_off_time [Numeric]
|
62
|
+
# @param data_store [Stoplight::DataStore::Base]
|
63
|
+
# @param error_notifier [Proc]
|
64
|
+
# @param notifiers [Stoplight::Notifier::Base]
|
65
|
+
# @param threshold [Numeric]
|
66
|
+
# @param window_size [Numeric]
|
67
|
+
def initialize(name:, cool_off_time:, data_store:, error_notifier:, notifiers:, threshold:, window_size:)
|
68
|
+
@name = name
|
69
|
+
@cool_off_time = cool_off_time
|
70
|
+
@data_store = data_store
|
71
|
+
@error_notifier = error_notifier
|
72
|
+
@notifiers = notifiers
|
73
|
+
@threshold = threshold
|
74
|
+
@window_size = window_size
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param other [any]
|
78
|
+
# @return [Boolean]
|
79
|
+
def ==(other)
|
80
|
+
other.is_a?(self.class) && settings == other.settings
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param cool_off_time [Numeric]
|
84
|
+
# @param data_store [Stoplight::DataStore::Base]
|
85
|
+
# @param error_notifier [Proc]
|
86
|
+
# @param name [String]
|
87
|
+
# @param notifiers [Stoplight::Notifier::Base]
|
88
|
+
# @param threshold [Numeric]
|
89
|
+
# @param window_size [Numeric]
|
90
|
+
# @return [Stoplight::Configuration]
|
91
|
+
def with(
|
92
|
+
cool_off_time: self.cool_off_time,
|
93
|
+
data_store: self.data_store,
|
94
|
+
error_notifier: self.error_notifier,
|
95
|
+
name: self.name,
|
96
|
+
notifiers: self.notifiers,
|
97
|
+
threshold: self.threshold,
|
98
|
+
window_size: self.window_size
|
99
|
+
)
|
100
|
+
Configuration.new(
|
101
|
+
cool_off_time: cool_off_time,
|
102
|
+
data_store: data_store,
|
103
|
+
error_notifier: error_notifier,
|
104
|
+
name: name,
|
105
|
+
notifiers: notifiers,
|
106
|
+
threshold: threshold,
|
107
|
+
window_size: window_size
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
# @return [Hash]
|
114
|
+
def settings
|
115
|
+
{
|
116
|
+
cool_off_time: cool_off_time,
|
117
|
+
data_store: data_store,
|
118
|
+
error_notifier: error_notifier,
|
119
|
+
name: name,
|
120
|
+
notifiers: notifiers,
|
121
|
+
threshold: threshold,
|
122
|
+
window_size: window_size
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -52,6 +52,15 @@ module Stoplight
|
|
52
52
|
def clear_state(_light)
|
53
53
|
raise NotImplementedError
|
54
54
|
end
|
55
|
+
|
56
|
+
# @param _light [Light]
|
57
|
+
# @param _from_color [String]
|
58
|
+
# @param _to_color [String]
|
59
|
+
# @yield _block
|
60
|
+
# @return [Void]
|
61
|
+
def with_notification_lock(_light, _from_color, _to_color, &_block)
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
55
64
|
end
|
56
65
|
end
|
57
66
|
end
|
@@ -7,10 +7,12 @@ module Stoplight
|
|
7
7
|
# @see Base
|
8
8
|
class Memory < Base
|
9
9
|
include MonitorMixin
|
10
|
+
KEY_SEPARATOR = ':'
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@failures = Hash.new { |h, k| h[k] = [] }
|
13
14
|
@states = Hash.new { |h, k| h[k] = State::UNLOCKED }
|
15
|
+
@last_notifications = {}
|
14
16
|
super() # MonitorMixin
|
15
17
|
end
|
16
18
|
|
@@ -19,18 +21,23 @@ module Stoplight
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def get_all(light)
|
22
|
-
synchronize { [
|
24
|
+
synchronize { [query_failures(light), @states[light.name]] }
|
23
25
|
end
|
24
26
|
|
25
27
|
def get_failures(light)
|
26
|
-
synchronize {
|
28
|
+
synchronize { query_failures(light) }
|
27
29
|
end
|
28
30
|
|
29
31
|
def record_failure(light, failure)
|
30
32
|
synchronize do
|
31
|
-
|
32
|
-
|
33
|
-
|
33
|
+
light_name = light.name
|
34
|
+
|
35
|
+
# Keep at most +light.threshold+ number of errors
|
36
|
+
@failures[light_name] = @failures[light_name].first(light.threshold - 1)
|
37
|
+
@failures[light_name].unshift(failure)
|
38
|
+
# Remove all errors happened before the window start
|
39
|
+
@failures[light_name] = query_failures(light, failure.time)
|
40
|
+
@failures[light_name].size
|
34
41
|
end
|
35
42
|
end
|
36
43
|
|
@@ -49,6 +56,40 @@ module Stoplight
|
|
49
56
|
def clear_state(light)
|
50
57
|
synchronize { @states.delete(light.name) }
|
51
58
|
end
|
59
|
+
|
60
|
+
def with_notification_lock(light, from_color, to_color)
|
61
|
+
synchronize do
|
62
|
+
if last_notification(light) != [from_color, to_color]
|
63
|
+
set_last_notification(light, from_color, to_color)
|
64
|
+
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# @param light [Stoplight::Light]
|
73
|
+
# @return [Array, nil]
|
74
|
+
def last_notification(light)
|
75
|
+
@last_notifications[light.name]
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param light [Stoplight::Light]
|
79
|
+
# @param from_color [String]
|
80
|
+
# @param to_color [String]
|
81
|
+
# @return [void]
|
82
|
+
def set_last_notification(light, from_color, to_color)
|
83
|
+
@last_notifications[light.name] = [from_color, to_color]
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param light [Stoplight::Light]
|
87
|
+
# @return [<Stoplight::Failure>]
|
88
|
+
def query_failures(light, time = Time.now)
|
89
|
+
@failures[light.name].select do |failure|
|
90
|
+
failure.time.to_i > time.to_i - light.window_size
|
91
|
+
end
|
92
|
+
end
|
52
93
|
end
|
53
94
|
end
|
54
95
|
end
|
@@ -1,15 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'redlock'
|
4
|
+
|
3
5
|
module Stoplight
|
4
6
|
module DataStore
|
7
|
+
# == Errors
|
8
|
+
# All errors are stored in the sorted set where keys are serialized errors and
|
9
|
+
# values (Redis uses "score" term) contain integer representations of the time
|
10
|
+
# when an error happened.
|
11
|
+
#
|
12
|
+
# This data structure enables us to query errors that happened within a specific
|
13
|
+
# period. We use this feature to support +window_size+ option.
|
14
|
+
#
|
15
|
+
# To avoid uncontrolled memory consumption, we keep at most +light.threshold+ number
|
16
|
+
# of errors happened within last +light.window_size+ seconds (by default infinity).
|
17
|
+
#
|
5
18
|
# @see Base
|
6
19
|
class Redis < Base
|
7
|
-
KEY_PREFIX = 'stoplight'
|
8
20
|
KEY_SEPARATOR = ':'
|
21
|
+
KEY_PREFIX = %w[stoplight v4].join(KEY_SEPARATOR)
|
9
22
|
|
10
23
|
# @param redis [::Redis]
|
11
|
-
def initialize(redis)
|
24
|
+
def initialize(redis, redlock: Redlock::Client.new([redis]))
|
12
25
|
@redis = redis
|
26
|
+
@redlock = redlock
|
13
27
|
end
|
14
28
|
|
15
29
|
def names
|
@@ -40,10 +54,14 @@ module Stoplight
|
|
40
54
|
normalize_failures(query_failures(light), light.error_notifier)
|
41
55
|
end
|
42
56
|
|
57
|
+
# Saves a new failure to the errors HSet and cleans up outdated errors.
|
43
58
|
def record_failure(light, failure)
|
44
|
-
size
|
45
|
-
|
46
|
-
|
59
|
+
*, size = @redis.multi do |transaction|
|
60
|
+
failures_key = failures_key(light)
|
61
|
+
|
62
|
+
transaction.zadd(failures_key, failure.time.to_i, failure.to_json)
|
63
|
+
remove_outdated_failures(light, failure.time, transaction: transaction)
|
64
|
+
transaction.zcard(failures_key)
|
47
65
|
end
|
48
66
|
|
49
67
|
size
|
@@ -76,10 +94,49 @@ module Stoplight
|
|
76
94
|
normalize_state(state)
|
77
95
|
end
|
78
96
|
|
97
|
+
LOCK_TTL = 2_000 # milliseconds
|
98
|
+
|
99
|
+
def with_notification_lock(light, from_color, to_color)
|
100
|
+
@redlock.lock(notification_lock_key(light), LOCK_TTL) do
|
101
|
+
if last_notification(light) != [from_color, to_color]
|
102
|
+
set_last_notification(light, from_color, to_color)
|
103
|
+
|
104
|
+
yield
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
79
109
|
private
|
80
110
|
|
111
|
+
# @param light [Stoplight::Light]
|
112
|
+
# @param time [Time]
|
113
|
+
def remove_outdated_failures(light, time, transaction: @redis)
|
114
|
+
failures_key = failures_key(light)
|
115
|
+
|
116
|
+
# Remove all errors happened before the window start
|
117
|
+
transaction.zremrangebyscore(failures_key, 0, time.to_i - light.window_size)
|
118
|
+
# Keep at most +light.threshold+ number of errors
|
119
|
+
transaction.zremrangebyrank(failures_key, 0, -light.threshold - 1)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param light [Stoplight::Light]
|
123
|
+
# @return [Array, nil]
|
124
|
+
def last_notification(light)
|
125
|
+
@redis.get(last_notification_key(light))&.split('->')
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param light [Stoplight::Light]
|
129
|
+
# @param from_color [String]
|
130
|
+
# @param to_color [String]
|
131
|
+
# @return [void]
|
132
|
+
def set_last_notification(light, from_color, to_color)
|
133
|
+
@redis.set(last_notification_key(light), [from_color, to_color].join('->'))
|
134
|
+
end
|
135
|
+
|
81
136
|
def query_failures(light, transaction: @redis)
|
82
|
-
|
137
|
+
window_start = Time.now.to_i - light.window_size
|
138
|
+
|
139
|
+
transaction.zrange(failures_key(light), Float::INFINITY, window_start, rev: true, by_score: true)
|
83
140
|
end
|
84
141
|
|
85
142
|
def normalize_failures(failures, error_notifier)
|
@@ -99,10 +156,22 @@ module Stoplight
|
|
99
156
|
state || State::UNLOCKED
|
100
157
|
end
|
101
158
|
|
159
|
+
# We store a list of failures happened in the +light+ in this key
|
160
|
+
#
|
161
|
+
# @param light [Stoplight::Light]
|
162
|
+
# @return [String]
|
102
163
|
def failures_key(light)
|
103
164
|
key('failures', light.name)
|
104
165
|
end
|
105
166
|
|
167
|
+
def notification_lock_key(light)
|
168
|
+
key('notification_lock', light.name)
|
169
|
+
end
|
170
|
+
|
171
|
+
def last_notification_key(light)
|
172
|
+
key('last_notification', light.name)
|
173
|
+
end
|
174
|
+
|
106
175
|
def states_key
|
107
176
|
key('states')
|
108
177
|
end
|
data/lib/stoplight/default.rb
CHANGED
data/lib/stoplight/error.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Light
|
5
|
+
# @api private
|
6
|
+
module Deprecated
|
7
|
+
def default_data_store
|
8
|
+
warn '[DEPRECATED] `Stoplight::Light.default_data_store` is deprecated. ' \
|
9
|
+
'Please use `Stoplight.default_data_store` instead.'
|
10
|
+
Stoplight.default_data_store
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_data_store=(value)
|
14
|
+
warn '[DEPRECATED] `Stoplight::Light.default_data_store=` is deprecated. ' \
|
15
|
+
'Please use `Stoplight.default_data_store=` instead.'
|
16
|
+
Stoplight.default_data_store = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_notifiers
|
20
|
+
warn '[DEPRECATED] `Stoplight::Light.default_notifiers` is deprecated. ' \
|
21
|
+
'Please use `Stoplight.default_notifiers` instead.'
|
22
|
+
Stoplight.default_notifiers
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_notifiers=(value)
|
26
|
+
warn '[DEPRECATED] `Stoplight::Light.default_notifiers=` is deprecated. ' \
|
27
|
+
'Please use `Stoplight.default_notifiers=` instead.'
|
28
|
+
Stoplight.default_notifiers = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_error_notifier
|
32
|
+
warn '[DEPRECATED] `Stoplight::Light.default_error_notifier` is deprecated. ' \
|
33
|
+
'Please use `Stoplight.default_error_notifier` instead.'
|
34
|
+
Stoplight.default_error_notifier
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_error_notifier=(value)
|
38
|
+
warn '[DEPRECATED] `Stoplight::Light.default_error_notifier=` is deprecated. ' \
|
39
|
+
'Please use `Stoplight.default_error_notifier=` instead.'
|
40
|
+
Stoplight.default_error_notifier = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|