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,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module DataStore
|
|
8
|
+
# @see +Domain::DataStore+
|
|
9
|
+
class Memory < Domain::DataStore
|
|
10
|
+
include MonitorMixin
|
|
11
|
+
|
|
12
|
+
KEY_SEPARATOR = ":"
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@errors = Hash.new { |errors, light_name| errors[light_name] = SlidingWindow.new }
|
|
16
|
+
@successes = Hash.new { |successes, light_name| successes[light_name] = SlidingWindow.new }
|
|
17
|
+
|
|
18
|
+
@recovery_probe_errors = Hash.new { |recovery_probe_errors, light_name| recovery_probe_errors[light_name] = SlidingWindow.new }
|
|
19
|
+
@recovery_probe_successes = Hash.new { |recovery_probe_successes, light_name| recovery_probe_successes[light_name] = SlidingWindow.new }
|
|
20
|
+
|
|
21
|
+
@metadata = Hash.new do |metadata, light_name|
|
|
22
|
+
metadata[light_name] = Domain::Metadata.new(
|
|
23
|
+
current_time: Time.now,
|
|
24
|
+
successes: 0,
|
|
25
|
+
errors: 0,
|
|
26
|
+
recovery_probe_successes: 0,
|
|
27
|
+
recovery_probe_errors: 0,
|
|
28
|
+
last_error: nil,
|
|
29
|
+
last_error_at: nil,
|
|
30
|
+
last_success_at: nil,
|
|
31
|
+
consecutive_errors: 0,
|
|
32
|
+
consecutive_successes: 0,
|
|
33
|
+
breached_at: nil,
|
|
34
|
+
locked_state: Domain::State::UNLOCKED,
|
|
35
|
+
recovery_scheduled_after: nil,
|
|
36
|
+
recovery_started_at: nil,
|
|
37
|
+
recovered_at: nil
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
super # MonitorMixin
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<String>]
|
|
44
|
+
def names
|
|
45
|
+
synchronize { @metadata.keys }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param config [Stoplight::Domain::Config]
|
|
49
|
+
# @return [Stoplight::Domain::Metadata]
|
|
50
|
+
def get_metadata(config)
|
|
51
|
+
light_name = config.name
|
|
52
|
+
|
|
53
|
+
synchronize do
|
|
54
|
+
current_time = self.current_time
|
|
55
|
+
recovery_window_start = (current_time - config.cool_off_time)
|
|
56
|
+
recovered_at = @metadata[light_name].recovered_at
|
|
57
|
+
window_start = if config.window_size
|
|
58
|
+
[recovered_at, (current_time - config.window_size)].compact.max
|
|
59
|
+
else
|
|
60
|
+
current_time
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@metadata[light_name].with(
|
|
64
|
+
current_time:,
|
|
65
|
+
errors: @errors[config.name].sum_in_window(window_start),
|
|
66
|
+
successes: @successes[config.name].sum_in_window(window_start),
|
|
67
|
+
recovery_probe_errors: @recovery_probe_errors[config.name].sum_in_window(recovery_window_start),
|
|
68
|
+
recovery_probe_successes: @recovery_probe_successes[config.name].sum_in_window(recovery_window_start)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param config [Stoplight::Domain::Config]
|
|
74
|
+
# @param exception [Exception]
|
|
75
|
+
# @return [Stoplight::Domain::Metadata]
|
|
76
|
+
def record_failure(config, exception)
|
|
77
|
+
current_time = self.current_time
|
|
78
|
+
light_name = config.name
|
|
79
|
+
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
80
|
+
|
|
81
|
+
synchronize do
|
|
82
|
+
@errors[light_name].increment if config.window_size
|
|
83
|
+
|
|
84
|
+
metadata = @metadata[light_name]
|
|
85
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
|
|
86
|
+
metadata.with(
|
|
87
|
+
last_error_at: current_time,
|
|
88
|
+
last_error: failure,
|
|
89
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
|
90
|
+
consecutive_successes: 0
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
metadata.with(
|
|
94
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
|
95
|
+
consecutive_successes: 0
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
get_metadata(config)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param config [Stoplight::Domain::Config]
|
|
103
|
+
# @return [void]
|
|
104
|
+
def record_success(config)
|
|
105
|
+
light_name = config.name
|
|
106
|
+
current_time = self.current_time
|
|
107
|
+
|
|
108
|
+
synchronize do
|
|
109
|
+
@successes[light_name].increment if config.window_size
|
|
110
|
+
|
|
111
|
+
metadata = @metadata[light_name]
|
|
112
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
|
|
113
|
+
metadata.with(
|
|
114
|
+
last_success_at: current_time,
|
|
115
|
+
consecutive_errors: 0,
|
|
116
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
|
117
|
+
)
|
|
118
|
+
else
|
|
119
|
+
metadata.with(
|
|
120
|
+
consecutive_errors: 0,
|
|
121
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @param config [Stoplight::Domain::Config]
|
|
128
|
+
# @param exception [Exception]
|
|
129
|
+
# @return [Stoplight::Domain::Metadata]
|
|
130
|
+
def record_recovery_probe_failure(config, exception)
|
|
131
|
+
light_name = config.name
|
|
132
|
+
current_time = self.current_time
|
|
133
|
+
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
134
|
+
|
|
135
|
+
synchronize do
|
|
136
|
+
@recovery_probe_errors[light_name].increment
|
|
137
|
+
|
|
138
|
+
metadata = @metadata[light_name]
|
|
139
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
|
|
140
|
+
metadata.with(
|
|
141
|
+
last_error_at: current_time,
|
|
142
|
+
last_error: failure,
|
|
143
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
|
144
|
+
consecutive_successes: 0
|
|
145
|
+
)
|
|
146
|
+
else
|
|
147
|
+
metadata.with(
|
|
148
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
|
149
|
+
consecutive_successes: 0
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
get_metadata(config)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @param config [Stoplight::Domain::Config]
|
|
157
|
+
# @return [Stoplight::Domain::Metadata]
|
|
158
|
+
def record_recovery_probe_success(config)
|
|
159
|
+
light_name = config.name
|
|
160
|
+
current_time = self.current_time
|
|
161
|
+
|
|
162
|
+
synchronize do
|
|
163
|
+
@recovery_probe_successes[light_name].increment
|
|
164
|
+
|
|
165
|
+
metadata = @metadata[light_name]
|
|
166
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
|
|
167
|
+
metadata.with(
|
|
168
|
+
last_success_at: current_time,
|
|
169
|
+
consecutive_errors: 0,
|
|
170
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
|
171
|
+
)
|
|
172
|
+
else
|
|
173
|
+
metadata.with(
|
|
174
|
+
consecutive_errors: 0,
|
|
175
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
get_metadata(config)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @param config [Stoplight::Domain::Config]
|
|
183
|
+
# @param state [String]
|
|
184
|
+
# @return [String]
|
|
185
|
+
def set_state(config, state)
|
|
186
|
+
light_name = config.name
|
|
187
|
+
|
|
188
|
+
synchronize do
|
|
189
|
+
metadata = @metadata[light_name]
|
|
190
|
+
@metadata[light_name] = metadata.with(locked_state: state)
|
|
191
|
+
end
|
|
192
|
+
state
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [String]
|
|
196
|
+
def inspect
|
|
197
|
+
"#<#{self.class.name}>"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Combined method that performs the state transition based on color
|
|
201
|
+
#
|
|
202
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
203
|
+
# @param color [String] The color to transition to ("GREEN", "YELLOW", or "RED")
|
|
204
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
205
|
+
def transition_to_color(config, color)
|
|
206
|
+
case color
|
|
207
|
+
when Domain::Color::GREEN
|
|
208
|
+
transition_to_green(config)
|
|
209
|
+
when Domain::Color::YELLOW
|
|
210
|
+
transition_to_yellow(config)
|
|
211
|
+
when Domain::Color::RED
|
|
212
|
+
transition_to_red(config)
|
|
213
|
+
else
|
|
214
|
+
raise ArgumentError, "Invalid color: #{color}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Transitions to GREEN state and ensures only one notification
|
|
219
|
+
#
|
|
220
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
221
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
222
|
+
private def transition_to_green(config)
|
|
223
|
+
light_name = config.name
|
|
224
|
+
current_time = self.current_time
|
|
225
|
+
|
|
226
|
+
synchronize do
|
|
227
|
+
metadata = @metadata[light_name]
|
|
228
|
+
if metadata.recovered_at
|
|
229
|
+
false
|
|
230
|
+
else
|
|
231
|
+
@metadata[light_name] = metadata.with(
|
|
232
|
+
recovered_at: current_time,
|
|
233
|
+
recovery_started_at: nil,
|
|
234
|
+
breached_at: nil,
|
|
235
|
+
recovery_scheduled_after: nil
|
|
236
|
+
)
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
243
|
+
#
|
|
244
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
245
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
246
|
+
private def transition_to_yellow(config)
|
|
247
|
+
light_name = config.name
|
|
248
|
+
current_time = self.current_time
|
|
249
|
+
|
|
250
|
+
synchronize do
|
|
251
|
+
metadata = @metadata[light_name]
|
|
252
|
+
if metadata.recovery_started_at.nil?
|
|
253
|
+
@metadata[light_name] = metadata.with(
|
|
254
|
+
recovery_started_at: current_time,
|
|
255
|
+
recovery_scheduled_after: nil,
|
|
256
|
+
recovered_at: nil,
|
|
257
|
+
breached_at: nil
|
|
258
|
+
)
|
|
259
|
+
true
|
|
260
|
+
else
|
|
261
|
+
@metadata[light_name] = metadata.with(
|
|
262
|
+
recovery_scheduled_after: nil,
|
|
263
|
+
recovered_at: nil,
|
|
264
|
+
breached_at: nil
|
|
265
|
+
)
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Transitions to RED state and ensures only one notification
|
|
272
|
+
#
|
|
273
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
274
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
275
|
+
private def transition_to_red(config)
|
|
276
|
+
light_name = config.name
|
|
277
|
+
current_time = self.current_time
|
|
278
|
+
recovery_scheduled_after = current_time + config.cool_off_time
|
|
279
|
+
|
|
280
|
+
synchronize do
|
|
281
|
+
metadata = @metadata[light_name]
|
|
282
|
+
if metadata.breached_at
|
|
283
|
+
@metadata[light_name] = metadata.with(
|
|
284
|
+
recovery_scheduled_after: recovery_scheduled_after,
|
|
285
|
+
recovery_started_at: nil,
|
|
286
|
+
recovered_at: nil
|
|
287
|
+
)
|
|
288
|
+
false
|
|
289
|
+
else
|
|
290
|
+
@metadata[light_name] = metadata.with(
|
|
291
|
+
breached_at: current_time,
|
|
292
|
+
recovery_scheduled_after: recovery_scheduled_after,
|
|
293
|
+
recovery_started_at: nil,
|
|
294
|
+
recovered_at: nil
|
|
295
|
+
)
|
|
296
|
+
true
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private def current_time
|
|
302
|
+
Time.now
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module DataStore
|
|
6
|
+
class Redis
|
|
7
|
+
# @api private
|
|
8
|
+
module Lua
|
|
9
|
+
class << self
|
|
10
|
+
def read_lua_file(name_without_extension)
|
|
11
|
+
File.read(File.join(__dir__, "#{name_without_extension}.lua"))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RECORD_FAILURE = read_lua_file("record_failure")
|
|
16
|
+
RECORD_SUCCESS = read_lua_file("record_success")
|
|
17
|
+
GET_METADATA = read_lua_file("get_metadata")
|
|
18
|
+
TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
|
|
19
|
+
TRANSITION_TO_RED = read_lua_file("transition_to_red")
|
|
20
|
+
TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|