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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +176 -198
  3. data/lib/stoplight/builder.rb +68 -0
  4. data/lib/stoplight/circuit_breaker.rb +92 -0
  5. data/lib/stoplight/configurable.rb +95 -0
  6. data/lib/stoplight/configuration.rb +126 -0
  7. data/lib/stoplight/data_store/base.rb +9 -0
  8. data/lib/stoplight/data_store/memory.rb +46 -5
  9. data/lib/stoplight/data_store/redis.rb +75 -6
  10. data/lib/stoplight/default.rb +2 -0
  11. data/lib/stoplight/error.rb +1 -0
  12. data/lib/stoplight/light/deprecated.rb +44 -0
  13. data/lib/stoplight/light/lockable.rb +45 -0
  14. data/lib/stoplight/light/runnable.rb +34 -16
  15. data/lib/stoplight/light.rb +69 -63
  16. data/lib/stoplight/rspec/generic_notifier.rb +42 -0
  17. data/lib/stoplight/rspec.rb +3 -0
  18. data/lib/stoplight/version.rb +1 -1
  19. data/lib/stoplight.rb +33 -10
  20. data/spec/spec_helper.rb +7 -0
  21. data/spec/stoplight/builder_spec.rb +165 -0
  22. data/spec/stoplight/circuit_breaker_spec.rb +35 -0
  23. data/spec/stoplight/configurable_spec.rb +25 -0
  24. data/spec/stoplight/data_store/base_spec.rb +7 -0
  25. data/spec/stoplight/data_store/memory_spec.rb +12 -123
  26. data/spec/stoplight/data_store/redis_spec.rb +28 -129
  27. data/spec/stoplight/error_spec.rb +10 -0
  28. data/spec/stoplight/light/lockable_spec.rb +93 -0
  29. data/spec/stoplight/light/runnable_spec.rb +12 -233
  30. data/spec/stoplight/light_spec.rb +4 -28
  31. data/spec/stoplight/notifier/generic_spec.rb +35 -35
  32. data/spec/stoplight/notifier/io_spec.rb +1 -0
  33. data/spec/stoplight/notifier/logger_spec.rb +3 -0
  34. data/spec/stoplight_spec.rb +17 -6
  35. data/spec/support/configurable.rb +69 -0
  36. data/spec/support/data_store/base/clear_failures.rb +18 -0
  37. data/spec/support/data_store/base/clear_state.rb +20 -0
  38. data/spec/support/data_store/base/get_all.rb +44 -0
  39. data/spec/support/data_store/base/get_failures.rb +30 -0
  40. data/spec/support/data_store/base/get_state.rb +7 -0
  41. data/spec/support/data_store/base/names.rb +29 -0
  42. data/spec/support/data_store/base/record_failures.rb +70 -0
  43. data/spec/support/data_store/base/set_state.rb +15 -0
  44. data/spec/support/data_store/base/with_notification_lock.rb +27 -0
  45. data/spec/support/data_store/base.rb +21 -0
  46. data/spec/support/database_cleaner.rb +26 -0
  47. data/spec/support/exception_helpers.rb +9 -0
  48. data/spec/support/light/runnable/color.rb +79 -0
  49. data/spec/support/light/runnable/run.rb +247 -0
  50. data/spec/support/light/runnable.rb +4 -0
  51. metadata +56 -225
  52. data/lib/stoplight/notifier/bugsnag.rb +0 -37
  53. data/lib/stoplight/notifier/hip_chat.rb +0 -43
  54. data/lib/stoplight/notifier/honeybadger.rb +0 -44
  55. data/lib/stoplight/notifier/pagerduty.rb +0 -21
  56. data/lib/stoplight/notifier/raven.rb +0 -40
  57. data/lib/stoplight/notifier/rollbar.rb +0 -39
  58. data/lib/stoplight/notifier/slack.rb +0 -21
  59. data/spec/stoplight/notifier/bugsnag_spec.rb +0 -90
  60. data/spec/stoplight/notifier/hip_chat_spec.rb +0 -91
  61. data/spec/stoplight/notifier/honeybadger_spec.rb +0 -88
  62. data/spec/stoplight/notifier/pagerduty_spec.rb +0 -40
  63. data/spec/stoplight/notifier/raven_spec.rb +0 -90
  64. data/spec/stoplight/notifier/rollbar_spec.rb +0 -90
  65. 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 { [@failures[light.name], @states[light.name]] }
24
+ synchronize { [query_failures(light), @states[light.name]] }
23
25
  end
24
26
 
25
27
  def get_failures(light)
26
- synchronize { @failures[light.name] }
28
+ synchronize { query_failures(light) }
27
29
  end
28
30
 
29
31
  def record_failure(light, failure)
30
32
  synchronize do
31
- n = light.threshold - 1
32
- @failures[light.name] = @failures[light.name].first(n)
33
- @failures[light.name].unshift(failure).size
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, = @redis.multi do |transaction|
45
- transaction.lpush(failures_key(light), failure.to_json)
46
- transaction.ltrim(failures_key(light), 0, light.threshold - 1)
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
- transaction.lrange(failures_key(light), 0, -1)
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
@@ -23,5 +23,7 @@ module Stoplight
23
23
  ].freeze
24
24
 
25
25
  THRESHOLD = 3
26
+
27
+ WINDOW_SIZE = Float::INFINITY
26
28
  end
27
29
  end
@@ -16,6 +16,7 @@ module Stoplight
16
16
  ].freeze
17
17
 
18
18
  Base = Class.new(StandardError)
19
+ IncorrectColor = Class.new(Base)
19
20
  RedLight = Class.new(Base)
20
21
  end
21
22
  end
@@ -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