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.
- checksums.yaml +4 -4
- data/README.md +16 -1
- data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
- data/lib/stoplight/admin/views/layout.erb +3 -3
- data/lib/stoplight/admin.rb +4 -4
- data/lib/stoplight/domain/color.rb +11 -0
- data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
- data/lib/stoplight/domain/config.rb +55 -0
- data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
- data/lib/stoplight/domain/error.rb +42 -0
- data/lib/stoplight/domain/failure.rb +42 -0
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
- data/lib/stoplight/domain/light.rb +198 -0
- data/lib/stoplight/domain/light_factory.rb +75 -0
- data/lib/stoplight/domain/metadata.rb +65 -0
- data/lib/stoplight/domain/state.rb +11 -0
- data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
- data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
- data/lib/stoplight/domain/tracker/base.rb +41 -0
- data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
- data/lib/stoplight/domain/tracker/request.rb +67 -0
- data/lib/stoplight/domain/traffic_control/base.rb +74 -0
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
- data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
- data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
- data/lib/stoplight/domain/traffic_recovery.rb +13 -0
- data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
- data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
- data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
- data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
- data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
- data/lib/stoplight/rspec/generic_notifier.rb +1 -1
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/container.rb +80 -0
- data/lib/stoplight/wiring/default.rb +28 -0
- data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
- data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
- data/lib/stoplight/wiring/fail_safe_data_store.rb +123 -0
- data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
- data/lib/stoplight/wiring/light/default_config.rb +18 -0
- data/lib/stoplight/wiring/light/system_config.rb +11 -0
- data/lib/stoplight/wiring/light_factory.rb +188 -0
- data/lib/stoplight/wiring/public_api.rb +28 -0
- data/lib/stoplight/wiring/system_container.rb +9 -0
- data/lib/stoplight/wiring/system_light_factory.rb +17 -0
- data/lib/stoplight.rb +38 -28
- metadata +53 -42
- data/lib/stoplight/color.rb +0 -9
- data/lib/stoplight/config/dsl.rb +0 -97
- data/lib/stoplight/config/library_default_config.rb +0 -21
- data/lib/stoplight/config/system_config.rb +0 -7
- data/lib/stoplight/data_store/fail_safe.rb +0 -113
- data/lib/stoplight/data_store/memory.rb +0 -311
- data/lib/stoplight/data_store/redis/lua.rb +0 -23
- data/lib/stoplight/data_store/redis.rb +0 -449
- data/lib/stoplight/data_store.rb +0 -6
- data/lib/stoplight/default.rb +0 -30
- data/lib/stoplight/error.rb +0 -10
- data/lib/stoplight/failure.rb +0 -71
- data/lib/stoplight/light/config.rb +0 -111
- data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
- data/lib/stoplight/light/green_run_strategy.rb +0 -54
- data/lib/stoplight/light/red_run_strategy.rb +0 -27
- data/lib/stoplight/light/run_strategy.rb +0 -32
- data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
- data/lib/stoplight/light.rb +0 -191
- data/lib/stoplight/metadata.rb +0 -99
- data/lib/stoplight/notifier/generic.rb +0 -79
- data/lib/stoplight/notifier/io.rb +0 -21
- data/lib/stoplight/notifier/logger.rb +0 -19
- data/lib/stoplight/state.rb +0 -9
- data/lib/stoplight/traffic_control/base.rb +0 -70
- data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
- data/lib/stoplight/traffic_control/error_rate.rb +0 -49
- data/lib/stoplight/traffic_recovery/base.rb +0 -75
- data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
- data/lib/stoplight/traffic_recovery.rb +0 -11
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
- /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
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stoplight
|
|
4
|
-
module
|
|
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
|
-
#
|
|
10
|
-
class
|
|
9
|
+
# :nocov:
|
|
10
|
+
class StateTransitionNotifier # ColorTransition?????
|
|
11
11
|
# Sends a notification when a Stoplight changes state.
|
|
12
12
|
#
|
|
13
|
-
# @param config [Stoplight::
|
|
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
|