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