stoplight 3.0.1 → 4.1.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 +174 -181
- data/lib/stoplight/builder.rb +70 -0
- data/lib/stoplight/circuit_breaker.rb +102 -0
- data/lib/stoplight/configurable.rb +95 -0
- data/lib/stoplight/configuration.rb +126 -0
- data/lib/stoplight/data_store/memory.rb +20 -5
- data/lib/stoplight/data_store/redis.rb +37 -5
- 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 +31 -13
- 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 +32 -8
- data/spec/spec_helper.rb +7 -0
- data/spec/stoplight/builder_spec.rb +165 -0
- data/spec/stoplight/circuit_breaker_spec.rb +43 -0
- data/spec/stoplight/configurable_spec.rb +25 -0
- data/spec/stoplight/data_store/memory_spec.rb +12 -149
- data/spec/stoplight/data_store/redis_spec.rb +26 -158
- data/spec/stoplight/error_spec.rb +10 -0
- data/spec/stoplight/light/lockable_spec.rb +93 -0
- data/spec/stoplight/light/runnable_spec.rb +14 -265
- 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/state.rb +31 -0
- data/spec/support/light/runnable.rb +5 -0
- metadata +53 -231
- data/lib/stoplight/notifier/bugsnag.rb +0 -37
- 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/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,70 @@
|
|
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, :name
|
34
|
+
def_delegator :build, :state
|
35
|
+
def_delegator :build, :run
|
36
|
+
def_delegator :build, :lock
|
37
|
+
def_delegator :build, :unlock
|
38
|
+
|
39
|
+
class << self
|
40
|
+
# @param settings [Hash]
|
41
|
+
# @see +Stoplight::Configuration#initialize+
|
42
|
+
# @return [Stoplight::Builder]
|
43
|
+
def with(**settings)
|
44
|
+
new Configuration.new(**settings)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [Stoplight::Configuration]
|
49
|
+
def initialize(configuration)
|
50
|
+
@configuration = configuration
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Stoplight::Light]
|
54
|
+
def build(&code)
|
55
|
+
Light.new(configuration.name, configuration, &code)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param other [any]
|
59
|
+
# @return [Boolean]
|
60
|
+
def ==(other)
|
61
|
+
other.is_a?(self.class) && configuration == other.configuration
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def reconfigure(configuration)
|
67
|
+
self.class.new(configuration)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,102 @@
|
|
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
|
+
# @return [String] one of +locked_green+, +locked_red+, and +unlocked+
|
43
|
+
def state
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String] the light's name
|
48
|
+
def name
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns current color:
|
53
|
+
# * +Stoplight::Color::GREEN+ -- circuit breaker is closed
|
54
|
+
# * +Stoplight::Color::RED+ -- circuit breaker is open
|
55
|
+
# * +Stoplight::Color::YELLOW+ -- circuit breaker is half-open
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# light = Stoplight('example')
|
59
|
+
# light.color #=> Color::GREEN
|
60
|
+
#
|
61
|
+
# @return [String] returns current light color
|
62
|
+
def color
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
# Runs the given block of code with this circuit breaker
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# light = Stoplight('example')
|
70
|
+
# light.run { 2/0 }
|
71
|
+
#
|
72
|
+
# @raise [Stoplight::Error::RedLight]
|
73
|
+
# @return [any]
|
74
|
+
def run(&code)
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# light = Stoplight('example-locked')
|
82
|
+
# light.lock(Stoplight::Color::RED)
|
83
|
+
#
|
84
|
+
# @param color [String] should be either +Color::RED+ or +Color::GREEN+
|
85
|
+
# @return [Stoplight::CircuitBreaker] returns locked circuit breaker
|
86
|
+
def lock(color)
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
90
|
+
# Unlocks light and sets it's state to State::UNLOCKED
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# light = Stoplight('example-locked')
|
94
|
+
# light.lock(Stoplight::Color::RED)
|
95
|
+
# light.unlock
|
96
|
+
#
|
97
|
+
# @return [Stoplight::CircuitBreaker] returns unlocked circuit breaker
|
98
|
+
def unlock
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
end
|
102
|
+
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
|
@@ -21,18 +21,23 @@ module Stoplight
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def get_all(light)
|
24
|
-
synchronize { [
|
24
|
+
synchronize { [query_failures(light), @states[light.name]] }
|
25
25
|
end
|
26
26
|
|
27
27
|
def get_failures(light)
|
28
|
-
synchronize {
|
28
|
+
synchronize { query_failures(light) }
|
29
29
|
end
|
30
30
|
|
31
31
|
def record_failure(light, failure)
|
32
32
|
synchronize do
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
36
41
|
end
|
37
42
|
end
|
38
43
|
|
@@ -62,6 +67,8 @@ module Stoplight
|
|
62
67
|
end
|
63
68
|
end
|
64
69
|
|
70
|
+
private
|
71
|
+
|
65
72
|
# @param light [Stoplight::Light]
|
66
73
|
# @return [Array, nil]
|
67
74
|
def last_notification(light)
|
@@ -75,6 +82,14 @@ module Stoplight
|
|
75
82
|
def set_last_notification(light, from_color, to_color)
|
76
83
|
@last_notifications[light.name] = [from_color, to_color]
|
77
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
|
78
93
|
end
|
79
94
|
end
|
80
95
|
end
|
@@ -4,10 +4,21 @@ require 'redlock'
|
|
4
4
|
|
5
5
|
module Stoplight
|
6
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
|
+
#
|
7
18
|
# @see Base
|
8
19
|
class Redis < Base
|
9
|
-
KEY_PREFIX = 'stoplight'
|
10
20
|
KEY_SEPARATOR = ':'
|
21
|
+
KEY_PREFIX = %w[stoplight v4].join(KEY_SEPARATOR)
|
11
22
|
|
12
23
|
# @param redis [::Redis]
|
13
24
|
def initialize(redis, redlock: Redlock::Client.new([redis]))
|
@@ -43,10 +54,14 @@ module Stoplight
|
|
43
54
|
normalize_failures(query_failures(light), light.error_notifier)
|
44
55
|
end
|
45
56
|
|
57
|
+
# Saves a new failure to the errors HSet and cleans up outdated errors.
|
46
58
|
def record_failure(light, failure)
|
47
|
-
size
|
48
|
-
|
49
|
-
|
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)
|
50
65
|
end
|
51
66
|
|
52
67
|
size
|
@@ -93,6 +108,17 @@ module Stoplight
|
|
93
108
|
|
94
109
|
private
|
95
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
|
+
|
96
122
|
# @param light [Stoplight::Light]
|
97
123
|
# @return [Array, nil]
|
98
124
|
def last_notification(light)
|
@@ -108,7 +134,9 @@ module Stoplight
|
|
108
134
|
end
|
109
135
|
|
110
136
|
def query_failures(light, transaction: @redis)
|
111
|
-
|
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)
|
112
140
|
end
|
113
141
|
|
114
142
|
def normalize_failures(failures, error_notifier)
|
@@ -128,6 +156,10 @@ module Stoplight
|
|
128
156
|
state || State::UNLOCKED
|
129
157
|
end
|
130
158
|
|
159
|
+
# We store a list of failures happened in the +light+ in this key
|
160
|
+
#
|
161
|
+
# @param light [Stoplight::Light]
|
162
|
+
# @return [String]
|
131
163
|
def failures_key(light)
|
132
164
|
key('failures', light.name)
|
133
165
|
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
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Light
|
5
|
+
# The Lockable module implements the behavior of locking and unlocking the light.
|
6
|
+
# Light can be locked in either a State::LOCKED_RED or State::LOCKED_GREEN state.
|
7
|
+
# By locking the light, you force it always to run code with the chosen light color.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# light = Stoplight('example-locked') { true }
|
11
|
+
# # => #<Stoplight::Light:..>
|
12
|
+
# light.run
|
13
|
+
# # => true
|
14
|
+
# light.lock(Stoplight::Color::RED)
|
15
|
+
# # => #<Stoplight::Light:..>
|
16
|
+
# light.run
|
17
|
+
# # => Stoplight::Error::RedLight: example-locked
|
18
|
+
# light.unlock
|
19
|
+
# # => #<Stoplight::Light:..>
|
20
|
+
# light.run
|
21
|
+
# # => true
|
22
|
+
module Lockable
|
23
|
+
# @param color [String] should be either Color::RED or Color::GREEN
|
24
|
+
# @return [Stoplight::Light] returns locked light
|
25
|
+
def lock(color)
|
26
|
+
state = case color
|
27
|
+
when Color::RED then State::LOCKED_RED
|
28
|
+
when Color::GREEN then State::LOCKED_GREEN
|
29
|
+
else raise Error::IncorrectColor
|
30
|
+
end
|
31
|
+
|
32
|
+
safely { data_store.set_state(self, state) }
|
33
|
+
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Stoplight::Light] returns unlocked light
|
38
|
+
def unlock
|
39
|
+
safely { data_store.set_state(self, Stoplight::State::UNLOCKED) }
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|