stoplight 5.3.8 → 5.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/views/layout.erb +3 -3
  5. data/lib/stoplight/admin.rb +4 -4
  6. data/lib/stoplight/domain/color.rb +11 -0
  7. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  8. data/lib/stoplight/domain/config.rb +55 -0
  9. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  10. data/lib/stoplight/domain/error.rb +42 -0
  11. data/lib/stoplight/domain/failure.rb +42 -0
  12. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  13. data/lib/stoplight/domain/light.rb +198 -0
  14. data/lib/stoplight/domain/light_factory.rb +75 -0
  15. data/lib/stoplight/domain/metadata.rb +65 -0
  16. data/lib/stoplight/domain/state.rb +11 -0
  17. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  18. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  19. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  20. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  21. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  22. data/lib/stoplight/domain/tracker/base.rb +41 -0
  23. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  24. data/lib/stoplight/domain/tracker/request.rb +67 -0
  25. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  26. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  27. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  28. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  29. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  30. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  31. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  32. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  33. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  34. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  36. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  37. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  38. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  39. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  40. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  41. data/lib/stoplight/version.rb +1 -1
  42. data/lib/stoplight/wiring/container.rb +80 -0
  43. data/lib/stoplight/wiring/default.rb +28 -0
  44. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  45. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  46. data/lib/stoplight/wiring/fail_safe_data_store.rb +123 -0
  47. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  48. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  49. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  50. data/lib/stoplight/wiring/light_factory.rb +188 -0
  51. data/lib/stoplight/wiring/public_api.rb +28 -0
  52. data/lib/stoplight/wiring/system_container.rb +9 -0
  53. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  54. data/lib/stoplight.rb +38 -28
  55. metadata +53 -42
  56. data/lib/stoplight/color.rb +0 -9
  57. data/lib/stoplight/config/dsl.rb +0 -97
  58. data/lib/stoplight/config/library_default_config.rb +0 -21
  59. data/lib/stoplight/config/system_config.rb +0 -7
  60. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  61. data/lib/stoplight/data_store/memory.rb +0 -311
  62. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  63. data/lib/stoplight/data_store/redis.rb +0 -449
  64. data/lib/stoplight/data_store.rb +0 -6
  65. data/lib/stoplight/default.rb +0 -30
  66. data/lib/stoplight/error.rb +0 -10
  67. data/lib/stoplight/failure.rb +0 -71
  68. data/lib/stoplight/light/config.rb +0 -111
  69. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  70. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  71. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  72. data/lib/stoplight/light/run_strategy.rb +0 -32
  73. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  74. data/lib/stoplight/light.rb +0 -191
  75. data/lib/stoplight/metadata.rb +0 -99
  76. data/lib/stoplight/notifier/generic.rb +0 -79
  77. data/lib/stoplight/notifier/io.rb +0 -21
  78. data/lib/stoplight/notifier/logger.rb +0 -19
  79. data/lib/stoplight/state.rb +0 -9
  80. data/lib/stoplight/traffic_control/base.rb +0 -70
  81. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  82. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  83. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  84. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  85. data/lib/stoplight/traffic_recovery.rb +0 -11
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  90. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  91. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Stoplight
6
+ module Domain
7
+ #
8
+ # @api private use +Stoplight()+ method instead
9
+ class Light
10
+ extend Forwardable
11
+ include ConfigurationBuilderInterface
12
+
13
+ # @!attribute [r] config
14
+ # @return [Stoplight::Domain::Config]
15
+ # @api private
16
+ attr_reader :config
17
+
18
+ # @!attribute [r] name
19
+ # The name of the light.
20
+ # @return [String]
21
+ def_delegator :config, :name
22
+
23
+ # @!attribute [r] green_run_strategy
24
+ # @return [Stoplight::Domain::Strategies::GreenRunStrategy]
25
+ protected attr_reader :green_run_strategy
26
+
27
+ # @!attribute [r] yellow_run_strategy
28
+ # @return [Stoplight::Domain::Strategies::YellowRunStrategy]
29
+ protected attr_reader :yellow_run_strategy
30
+
31
+ # @!attribute [r] red_run_strategy
32
+ # @return [Stoplight::Domain::Strategies::RedRunStrategy]
33
+ protected attr_reader :red_run_strategy
34
+
35
+ # @!attribute [r] data_store
36
+ # @return [Stoplight::Light::Base]
37
+ protected attr_reader :data_store
38
+
39
+ # @!attribute [r] factory
40
+ # @return [Stoplight::Domain::LightFactory]
41
+ protected attr_reader :factory
42
+
43
+ # @param config [Stoplight::Domain::Config]
44
+ def initialize(config, green_run_strategy:, yellow_run_strategy:, red_run_strategy:, data_store:, factory:)
45
+ @config = config
46
+ @data_store = data_store
47
+ @green_run_strategy = green_run_strategy
48
+ @yellow_run_strategy = yellow_run_strategy
49
+ @red_run_strategy = red_run_strategy
50
+ @factory = factory
51
+ end
52
+
53
+ # Returns the current state of the light:
54
+ # * +Stoplight::State::LOCKED_GREEN+ -- light is locked green and allows all traffic
55
+ # * +Stoplight::State::LOCKED_RED+ -- light is locked red and blocks all traffic
56
+ # * +Stoplight::State::UNLOCKED+ -- light is not locked and follow the configured rules
57
+ #
58
+ # @return [String]
59
+ def state
60
+ metadata.locked_state
61
+ end
62
+
63
+ # Returns current color:
64
+ # * +Stoplight::Color::GREEN+ -- circuit breaker is closed
65
+ # * +Stoplight::Color::RED+ -- circuit breaker is open
66
+ # * +Stoplight::Color::YELLOW+ -- circuit breaker is half-open
67
+ #
68
+ # @example
69
+ # light = Stoplight('example')
70
+ # light.color #=> Color::GREEN
71
+ #
72
+ # @return [String] returns current light color
73
+ def color
74
+ metadata.color
75
+ end
76
+
77
+ # Runs the given block of code with this circuit breaker
78
+ #
79
+ # @example
80
+ # light = Stoplight('example')
81
+ # light.run { 2/0 }
82
+ #
83
+ # @example Running with fallback
84
+ # light = Stoplight('example')
85
+ # light.run(->(error) { 0 }) { 1 / 0 } #=> 0
86
+ #
87
+ # @param fallback [Proc, nil] (nil) fallback code to run if the circuit breaker is open
88
+ # @raise [Stoplight::Error::RedLight]
89
+ # @return [any]
90
+ # @raise [Stoplight::Error::RedLight]
91
+ def run(fallback = nil, &code)
92
+ raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
93
+
94
+ metadata.then do |metadata|
95
+ strategy = state_strategy_factory(metadata.color)
96
+ strategy.execute(fallback, metadata:, &code)
97
+ end
98
+ end
99
+
100
+ # Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
101
+ #
102
+ # @example
103
+ # light = Stoplight('example-locked')
104
+ # light.lock(Stoplight::Color::RED)
105
+ #
106
+ # @param color [String] should be either +Color::RED+ or +Color::GREEN+
107
+ # @return [Stoplight::Light] returns locked light (circuit breaker)
108
+ def lock(color)
109
+ state = case color
110
+ when Color::RED then State::LOCKED_RED
111
+ when Color::GREEN then State::LOCKED_GREEN
112
+ else raise Error::IncorrectColor
113
+ end
114
+
115
+ data_store.set_state(config, state)
116
+
117
+ self
118
+ end
119
+
120
+ # Unlocks light and sets its state to State::UNLOCKED
121
+ #
122
+ # @example
123
+ # light = Stoplight('example-locked')
124
+ # light.lock(Stoplight::Color::RED)
125
+ # light.unlock
126
+ #
127
+ # @return [Stoplight::Light] returns unlocked light (circuit breaker)
128
+ def unlock
129
+ data_store.set_state(config, State::UNLOCKED)
130
+
131
+ self
132
+ end
133
+
134
+ # Two lights considered equal if they have the same configuration.
135
+ #
136
+ # @param other [any]
137
+ # @return [Boolean]
138
+ def ==(other)
139
+ other.is_a?(self.class) && config == other.config && data_store == other.data_store &&
140
+ green_run_strategy == other.green_run_strategy && yellow_run_strategy == other.yellow_run_strategy &&
141
+ red_run_strategy == other.red_run_strategy && factory == other.factory
142
+ end
143
+
144
+ # Reconfigures the light with updated settings and returns a new instance.
145
+ #
146
+ # This method allows you to modify the configuration of a +Stoplight::Light+ object
147
+ # by providing a hash of settings. The original light remains unchanged, and a new
148
+ # light instance with the updated configuration is returned.
149
+ #
150
+ # @param settings [Hash] A hash of configuration options to update.
151
+ # @option settings [String] :name The name of the light.
152
+ # @option settings [Numeric] :cool_off_time The cool-off time in seconds before the light attempts recovery.
153
+ # @option settings [Numeric] :threshold The failure threshold to trigger the red state.
154
+ # @option settings [Numeric] :window_size The time window in seconds for counting failures.
155
+ # @option settings [Stoplight::DataStore::Base] :data_store The data store to use for persisting light state.
156
+ # @option settings [Array<Stoplight::Domain::AbstractStateTransitionNotifier>] :notifiers A list of notifiers to handle light events.
157
+ # @option settings [Proc] :error_notifier A custom error notifier to handle exceptions.
158
+ # @option settings [Array<StandardError>] :tracked_errors A list of errors to track for failure counting.
159
+ # @option settings [Array<StandardError>] :skipped_errors A list of errors to skip from failure counting.
160
+ # @return [Stoplight::Light] A new `Stoplight::Light` instance with the updated configuration.
161
+ #
162
+ # @example Reconfiguring a light with custom settings
163
+ # light = Stoplight('payment-api')
164
+ #
165
+ # # Create a light for invoices with a higher threshold
166
+ # invoices_light = light.with(tracked_errors: [TimeoutError], threshold: 10)
167
+ #
168
+ # # Create a light for payments with a lower threshold
169
+ # payment_light = light.with(threshold: 5)
170
+ #
171
+ # # Run the lights with their respective configurations
172
+ # invoices_light.run(->(error) { [] }) { call_invoices_api }
173
+ # payment_light.run(->(error) { nil }) { call_payment_api }
174
+ # @see +Stoplight()+
175
+ def with(**settings)
176
+ factory.build_with(**settings)
177
+ end
178
+
179
+ private
180
+
181
+ def state_strategy_factory(color)
182
+ case color
183
+ when Color::GREEN
184
+ green_run_strategy
185
+ when Color::YELLOW
186
+ yellow_run_strategy
187
+ else
188
+ red_run_strategy
189
+ end
190
+ end
191
+
192
+ # @return [Stoplight::Domain::Metadata]
193
+ def metadata
194
+ data_store.get_metadata(config)
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ # Abstract factory protocol for building +Stoplight::Light+ instances.
6
+ #
7
+ # This defines the interface that any Light factory must implement,
8
+ # regardless of the underlying implementation details.
9
+ #
10
+ # This uses the Abstract Factory pattern to decouple the domain (Light)
11
+ # from the application layer (concrete factory implementation). Light
12
+ # doesn't know HOW it's built, only that it needs something that can
13
+ # build variants of itself.
14
+ #
15
+ # By defining this interface in the domain layer:
16
+ # - Light can request reconfiguration without knowing about containers
17
+ # - Different factory implementations can be swapped (code, database-configured, etc.)
18
+ # - Dependency direction is preserved (Application → Domain, not Domain → Application)
19
+ #
20
+ # @abstract Subclasses must implement +#with+ and +#build+
21
+ # @api private
22
+ class LightFactory
23
+ # Creates a new factory with modified settings.
24
+ #
25
+ # This method must return a NEW factory instance - it should not
26
+ # modify the current factory. The new factory should inherit all
27
+ # configuration from the current factory, with the provided
28
+ # settings overriding specific values.
29
+ #
30
+ # @param settings [Hash] Configuration and dependency overrides
31
+ # @return [Stoplight::Domain::LightFactory] New factory with updated settings
32
+ # @raise [NotImplementedError] Must be implemented by subclass
33
+ # @abstract
34
+ # :nocov:
35
+ def with(**settings)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ # Builds a +Stoplight::Light+ instance with the current configuration.
40
+ #
41
+ # This method must construct a fully-wired Light with all required
42
+ # dependencies (strategies, data store, notifiers, etc.). The Light
43
+ # should be ready to use immediately after construction.
44
+ #
45
+ # @return [Stoplight::Light] Configured circuit breaker instance
46
+ # @raise [NotImplementedError] Must be implemented by subclass
47
+ # @raise [Stoplight::Error::ConfigurationError] If configuration is invalid
48
+ # @abstract
49
+ def build
50
+ raise NotImplementedError
51
+ end
52
+ # :nocov:
53
+
54
+ # Convenience method to configure and build in one operation.
55
+ #
56
+ # This combines +#with+ and +#build+ into a single call, which is
57
+ # useful when you want to create a customized Light without keeping
58
+ # a reference to the intermediate factory.
59
+ #
60
+ # @param settings [Hash] Settings to override before building
61
+ # @return [Stoplight::Light] Configured circuit breaker instance
62
+ #
63
+ # @example Usage
64
+ # # Instead of:
65
+ # new_factory = factory.with(threshold: 10)
66
+ # light = new_factory.build
67
+ #
68
+ # # You can do:
69
+ # light = factory.build_with(threshold: 10)
70
+ def build_with(**settings)
71
+ with(**settings).build
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ # @api private
6
+ Metadata = Data.define(
7
+ :successes,
8
+ :errors,
9
+ :recovery_probe_successes,
10
+ :recovery_probe_errors,
11
+ :last_error_at,
12
+ :last_success_at,
13
+ :consecutive_errors,
14
+ :consecutive_successes,
15
+ :last_error,
16
+ :breached_at,
17
+ :locked_state,
18
+ :recovery_scheduled_after,
19
+ :recovery_started_at,
20
+ :recovered_at,
21
+ :current_time
22
+ ) do
23
+ # YELLOW color could be entered implicitly through a timeout
24
+ # and explicitly through a transition.
25
+ #
26
+ # This method indicates whether the recovery has already started explicitly
27
+ #
28
+ # @return [Boolean]
29
+ def recovery_started?
30
+ recovery_started_at && recovery_started_at <= current_time
31
+ end
32
+
33
+ # @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
34
+ def color
35
+ if locked_state == State::LOCKED_GREEN
36
+ Color::GREEN
37
+ elsif locked_state == State::LOCKED_RED
38
+ Color::RED
39
+ elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
40
+ Color::YELLOW
41
+ elsif breached_at
42
+ Color::RED
43
+ else
44
+ Color::GREEN
45
+ end
46
+ end
47
+
48
+ # Calculates the error rate based on the number of successes and errors.
49
+ #
50
+ # @return [Float]
51
+ def error_rate
52
+ if (successes + errors).zero?
53
+ 0.0
54
+ else
55
+ errors.fdiv(successes + errors)
56
+ end
57
+ end
58
+
59
+ # @return [Integer]
60
+ def requests
61
+ successes + errors
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module State
6
+ UNLOCKED = "unlocked"
7
+ LOCKED_GREEN = "locked_green"
8
+ LOCKED_RED = "locked_red"
9
+ end
10
+ end
11
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- module Notifier
4
+ module Domain
5
5
  # Base class for creating custom notifiers in Stoplight.
6
6
  # This is an abstract class that defines the interface for notifiers.
7
7
  #
8
8
  # @abstract Subclasses must implement the `notify` method to define custom notification logic.
9
- # @see +Stoplight::Notifier::Generic+
10
- class Base
9
+ # :nocov:
10
+ class StateTransitionNotifier # ColorTransition?????
11
11
  # Sends a notification when a Stoplight changes state.
12
12
  #
13
- # @param config [Stoplight::Light::Config] The Stoplight instance triggering the notification.
13
+ # @param config [Stoplight::Domain::Config] The Stoplight instance triggering the notification.
14
14
  # @param from_color [String] The previous state color of the Stoplight.
15
15
  # @param to_color [String] The new state color of the Stoplight.
16
16
  # @param error [Exception, nil] The error (if any) that caused the state change.
@@ -20,5 +20,6 @@ module Stoplight
20
20
  raise NotImplementedError
21
21
  end
22
22
  end
23
+ # :nocov:
23
24
  end
24
25
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is green.
7
+ #
8
+ # This strategy clears failures after successful execution and handles errors
9
+ # by either raising them or invoking a fallback if provided.
10
+ #
11
+ # @api private
12
+ class GreenRunStrategy < RunStrategy
13
+ # @!attribute [r] request_tracker
14
+ # @return [Stoplight::Domain::Tracker::Request]
15
+ protected attr_reader :request_tracker
16
+
17
+ # @!attribute [r] config
18
+ # @return [Stoplight::Domain::Config] The configuration for the light.
19
+ protected attr_reader :config
20
+
21
+ # @param config [Stoplight::Domain::Config]
22
+ # @param request_tracker [Stoplight::Domain::Tracker::Request
23
+ def initialize(config:, request_tracker:)
24
+ @config = config
25
+ @request_tracker = request_tracker
26
+ end
27
+
28
+ # Executes the provided code block when the light is in the green state.
29
+ #
30
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
31
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
32
+ # @yield The code block to execute.
33
+ # @return [Object] The result of the code block if successful.
34
+ # @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
35
+ def execute(fallback, metadata:, &code)
36
+ # TODO: Consider implementing sampling rate to limit the memory footprint
37
+ code.call.tap { record_success }
38
+ rescue => error
39
+ if config.track_error?(error)
40
+ record_error(error)
41
+
42
+ if fallback
43
+ fallback.call(error)
44
+ else
45
+ raise
46
+ end
47
+ else
48
+ # User chose to not track the error, so we record it as a success
49
+ record_success
50
+ raise
51
+ end
52
+ end
53
+
54
+ private def record_error(error)
55
+ request_tracker.record_failure(error)
56
+ end
57
+
58
+ private def record_success
59
+ request_tracker.record_success
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def ==(other)
64
+ super && config == other.config && request_tracker == other.request_tracker
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is red.
7
+ #
8
+ # This strategy prevents execution of the code block and either raises an error
9
+ # or invokes a fallback if provided.
10
+ #
11
+ # @api private
12
+ class RedRunStrategy < RunStrategy
13
+ # @!attribute [r] config
14
+ # @return [Stoplight::Domain::Config] The configuration for the light.
15
+ protected attr_reader :config
16
+
17
+ def initialize(config:)
18
+ @config = config
19
+ end
20
+
21
+ # Executes the fallback proc when the light is in the red state.
22
+ #
23
+ # @param fallback [Proc, nil] A fallback proc to execute instead of the code block.
24
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
25
+ # @return [Object, nil] The result of the fallback proc if provided.
26
+ # @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
27
+ def execute(fallback, metadata:)
28
+ if fallback
29
+ fallback.call(nil)
30
+ else
31
+ raise Error::RedLight.new(
32
+ config.name,
33
+ cool_off_time: config.cool_off_time,
34
+ retry_after: metadata.recovery_scheduled_after
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Represents an abstract strategy for running a light's operations.
7
+ # Every new strategy should be a child of this class.
8
+ #
9
+ # @api private
10
+ # @abstract
11
+ class RunStrategy
12
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
13
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
14
+ # :nocov:
15
+ def execute(fallback, metadata:, &code)
16
+ raise NotImplementedError, "Subclasses must implement the execute method"
17
+ end
18
+ # :nocov:
19
+
20
+ # @return [Boolean]
21
+ def ==(other)
22
+ other.is_a?(self.class)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is yellow.
7
+ #
8
+ # This strategy clears failures after successful execution and notifies
9
+ # about color switch from Red to Green. It also handles errors by either
10
+ # raising them or invoking a fallback if provided.
11
+ #
12
+ # @api private
13
+ class YellowRunStrategy < RunStrategy
14
+ # @!attribute [r] config
15
+ # @return [Stoplight::Domain::Config] The configuration for the light.
16
+ protected attr_reader :config
17
+
18
+ # @!attribute [r] data_store
19
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
20
+ protected attr_reader :data_store
21
+
22
+ # @!attribute [r] notifiers
23
+ # @return [Stoplight::Domain::StateTransitionNotifier]
24
+ protected attr_reader :notifiers
25
+
26
+ # @!attribute [r] request_tracker
27
+ # @return [Stoplight::Domain::RecoveryProbeRequestRecorder]
28
+ protected attr_reader :request_tracker
29
+
30
+ # @param config [Stoplight::Domain::Config]
31
+ # @param data_store [Stoplight::DataStore::Base]
32
+ # @param notifiers [Array<Stoplight::Domain::StateTransitionNotifier>]
33
+ # @param request_tracker [Stoplight::Domain::Tracker::RecoveryProbe]
34
+ def initialize(config:, data_store:, notifiers:, request_tracker:)
35
+ @config = config
36
+ @data_store = data_store
37
+ @notifiers = notifiers
38
+ @request_tracker = request_tracker
39
+ end
40
+
41
+ # Executes the provided code block when the light is in the yellow state.
42
+ #
43
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
44
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
45
+ # @yield The code block to execute.
46
+ # @return [Object] The result of the code block if successful.
47
+ # @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
48
+ def execute(fallback, metadata:, &code)
49
+ enter_recovery(metadata)
50
+ # TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
51
+ code.call.tap { record_recovery_probe_success }
52
+ rescue => error
53
+ if config.track_error?(error)
54
+ record_recovery_probe_failure(error)
55
+
56
+ if fallback
57
+ fallback.call(error)
58
+ else
59
+ raise
60
+ end
61
+ else
62
+ record_recovery_probe_success
63
+ raise
64
+ end
65
+ end
66
+
67
+ private def record_recovery_probe_success
68
+ request_tracker.record_success
69
+ end
70
+
71
+ private def record_recovery_probe_failure(error)
72
+ request_tracker.record_failure(error)
73
+ end
74
+
75
+ # @param metadata [Stoplight::Domain::Metadata]
76
+ # @return [void]
77
+ private def enter_recovery(metadata)
78
+ return if metadata.recovery_started?
79
+
80
+ if data_store.transition_to_color(config, Color::YELLOW)
81
+ notifiers.each do |notifier|
82
+ notifier.notify(config, Color::RED, Color::YELLOW, nil)
83
+ end
84
+ end
85
+ end
86
+
87
+ # @return [Boolean]
88
+ def ==(other)
89
+ super &&
90
+ config == other.config &&
91
+ notifiers == other.notifiers &&
92
+ data_store == other.data_store &&
93
+ request_tracker == other.request_tracker
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Tracker
6
+ # @api private
7
+ # @abstract
8
+ class Base
9
+ # @!attribute [r] data_store
10
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
11
+ protected attr_reader :data_store
12
+
13
+ # @!attribute [r] traffic_control
14
+ # @return [Stoplight::Domain::TrafficControl::Base]
15
+ protected attr_reader :notifiers
16
+
17
+ # @!attribute [r] config
18
+ # @return [Stoplight::Domain::Config] The configuration for the light.
19
+ protected attr_reader :config
20
+
21
+ def ==(other)
22
+ other.is_a?(self.class) &&
23
+ config == other.config &&
24
+ data_store == other.data_store &&
25
+ notifiers == other.notifiers
26
+ end
27
+
28
+ def transition_and_notify(from_color, to_color, error = nil)
29
+ if data_store.transition_to_color(config, to_color)
30
+ notifiers.each do |notifier|
31
+ notifier.notify(config, from_color, to_color, error)
32
+ end
33
+ true
34
+ else
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end