stoplight 5.6.0 → 5.7.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/lib/stoplight/admin/dependencies.rb +1 -1
- data/lib/stoplight/admin/helpers.rb +10 -5
- data/lib/stoplight/admin/lights_repository.rb +18 -15
- data/lib/stoplight/admin.rb +2 -1
- data/lib/stoplight/common/deprecations.rb +11 -0
- data/lib/stoplight/domain/config.rb +5 -1
- data/lib/stoplight/domain/data_store.rb +17 -1
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
- data/lib/stoplight/domain/light.rb +31 -20
- data/lib/stoplight/domain/metrics.rb +6 -27
- data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
- data/lib/stoplight/domain/storage/metrics.rb +42 -0
- data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
- data/lib/stoplight/domain/storage/state.rb +87 -0
- data/lib/stoplight/domain/strategies/run_strategy.rb +0 -5
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +58 -32
- data/lib/stoplight/domain/tracker/base.rb +0 -29
- data/lib/stoplight/domain/tracker/recovery_probe.rb +23 -22
- data/lib/stoplight/domain/tracker/request.rb +23 -19
- data/lib/stoplight/domain/traffic_recovery/base.rb +1 -2
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +2 -8
- data/lib/stoplight/domain/traffic_recovery.rb +0 -1
- data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
- data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
- data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +61 -32
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
- data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
- data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
- data/lib/stoplight/infrastructure/data_store/redis.rb +133 -162
- data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
- data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
- data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/data_store/base.rb +11 -0
- data/lib/stoplight/wiring/data_store/memory.rb +10 -0
- data/lib/stoplight/wiring/data_store/redis.rb +25 -0
- data/lib/stoplight/wiring/default.rb +1 -1
- data/lib/stoplight/wiring/default_configuration.rb +1 -1
- data/lib/stoplight/wiring/default_factory_builder.rb +1 -1
- data/lib/stoplight/wiring/light_builder.rb +185 -0
- data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
- data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
- data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
- data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
- data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
- data/lib/stoplight/wiring/light_factory.rb +45 -132
- data/lib/stoplight/wiring/notifier_factory.rb +26 -0
- data/lib/stoplight/wiring/public_api.rb +3 -2
- data/lib/stoplight.rb +18 -3
- metadata +50 -15
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
- data/lib/stoplight/wiring/container.rb +0 -80
- data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -147
- data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
- data/lib/stoplight/wiring/system_container.rb +0 -9
- data/lib/stoplight/wiring/system_light_factory.rb +0 -17
- /data/lib/stoplight/infrastructure/data_store/redis/{get_metrics.lua → lua_scripts/get_metrics.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{record_failure.lua → lua_scripts/record_failure.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{record_success.lua → lua_scripts/record_success.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_green.lua → lua_scripts/transition_to_green.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_red.lua → lua_scripts/transition_to_red.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_yellow.lua → lua_scripts/transition_to_yellow.lua} +0 -0
|
@@ -74,22 +74,46 @@ module Stoplight
|
|
|
74
74
|
KEY_SEPARATOR = ":"
|
|
75
75
|
KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
|
|
76
76
|
|
|
77
|
+
# @!attribute recovery_lock_store
|
|
78
|
+
# @return [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockStore]
|
|
79
|
+
protected attr_reader :recovery_lock_store
|
|
80
|
+
|
|
81
|
+
# @!attribute scripting
|
|
82
|
+
# @return [Stoplight::Infrastructure::DataStore::Redis::Scripting]
|
|
83
|
+
protected attr_reader :scripting
|
|
84
|
+
|
|
85
|
+
# @!attribute redis
|
|
86
|
+
# @return [::Redis | ConnectionPool<::Redis>]
|
|
87
|
+
protected attr_reader :redis
|
|
88
|
+
|
|
89
|
+
# @!attribute warn_on_clock_skew
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
protected attr_reader :warn_on_clock_skew
|
|
92
|
+
|
|
77
93
|
# @param redis [::Redis, ConnectionPool<::Redis>]
|
|
94
|
+
# @param recovery_lock_store [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockStore]
|
|
78
95
|
# @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
|
|
96
|
+
# @param scripting [Stoplight::Infrastructure::DataStore::Redis::Scripting]
|
|
79
97
|
# the application server
|
|
80
|
-
def initialize(redis
|
|
98
|
+
def initialize(redis:, recovery_lock_store:, scripting:, warn_on_clock_skew: true)
|
|
81
99
|
@warn_on_clock_skew = warn_on_clock_skew
|
|
82
100
|
@redis = redis
|
|
101
|
+
@recovery_lock_store = recovery_lock_store
|
|
102
|
+
@scripting = scripting
|
|
83
103
|
end
|
|
84
104
|
|
|
85
105
|
def names
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
metadata, recovery_metrics = @redis.then do |client|
|
|
107
|
+
[
|
|
108
|
+
[key("metadata", "*"), /^#{key("metadata", "")}/],
|
|
109
|
+
[key("recovery_metrics", "*"), /^#{key("recovery_metrics", "")}/]
|
|
110
|
+
].map do |(pattern, prefix_regex)|
|
|
111
|
+
client.scan_each(match: pattern).to_a.map do |key|
|
|
112
|
+
key.sub(prefix_regex, "")
|
|
113
|
+
end
|
|
91
114
|
end
|
|
92
115
|
end
|
|
116
|
+
metadata + recovery_metrics
|
|
93
117
|
end
|
|
94
118
|
|
|
95
119
|
# @param config [Stoplight::Domain::Config]
|
|
@@ -108,28 +132,28 @@ module Stoplight
|
|
|
108
132
|
success_keys = []
|
|
109
133
|
end
|
|
110
134
|
|
|
111
|
-
successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = scripting.call(
|
|
136
|
+
:get_metrics,
|
|
137
|
+
args: [
|
|
138
|
+
failure_keys.count,
|
|
139
|
+
window_start_ts,
|
|
140
|
+
window_end_ts,
|
|
141
|
+
"last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
|
|
142
|
+
],
|
|
143
|
+
keys: [
|
|
144
|
+
metadata_key(config),
|
|
145
|
+
*success_keys,
|
|
146
|
+
*failure_keys
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
consecutive_errors = config.window_size ? [consecutive_errors.to_i, errors].min : consecutive_errors.to_i
|
|
150
|
+
consecutive_successes = config.window_size ? [consecutive_successes.to_i, successes].min : consecutive_successes.to_i
|
|
127
151
|
|
|
128
152
|
Domain::Metrics.new(
|
|
129
153
|
successes: (successes if config.window_size),
|
|
130
154
|
errors: (errors if config.window_size),
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
consecutive_errors:,
|
|
156
|
+
consecutive_successes:,
|
|
133
157
|
last_error: deserialize_failure(last_error_json),
|
|
134
158
|
last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
|
|
135
159
|
)
|
|
@@ -138,36 +162,17 @@ module Stoplight
|
|
|
138
162
|
# @param config [Stoplight::Domain::Config]
|
|
139
163
|
# @return [Stoplight::Domain::Metrics]
|
|
140
164
|
def get_recovery_metrics(config)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
|
|
147
|
-
recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
|
|
148
|
-
|
|
149
|
-
successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
|
|
150
|
-
client.evalsha(
|
|
151
|
-
get_metrics_sha,
|
|
152
|
-
argv: [
|
|
153
|
-
recovery_probe_failure_keys.count,
|
|
154
|
-
window_start_ts,
|
|
155
|
-
window_end_ts,
|
|
156
|
-
"last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
|
|
157
|
-
],
|
|
158
|
-
keys: [
|
|
159
|
-
metadata_key(config),
|
|
160
|
-
*recovery_probe_success_keys,
|
|
161
|
-
*recovery_probe_failure_keys
|
|
162
|
-
]
|
|
165
|
+
last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
|
|
166
|
+
client.hmget(
|
|
167
|
+
recovery_metrics_key(config),
|
|
168
|
+
"last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
|
|
163
169
|
)
|
|
164
170
|
end
|
|
165
171
|
|
|
166
172
|
Domain::Metrics.new(
|
|
167
|
-
successes
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
total_consecutive_successes: consecutive_successes.to_i,
|
|
173
|
+
successes: nil, errors: nil,
|
|
174
|
+
consecutive_errors: consecutive_errors.to_i,
|
|
175
|
+
consecutive_successes: consecutive_successes.to_i,
|
|
171
176
|
last_error: deserialize_failure(last_error_json),
|
|
172
177
|
last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
|
|
173
178
|
)
|
|
@@ -193,18 +198,31 @@ module Stoplight
|
|
|
193
198
|
)
|
|
194
199
|
end
|
|
195
200
|
|
|
196
|
-
def
|
|
201
|
+
def clear_metrics(config)
|
|
197
202
|
if config.window_size
|
|
198
203
|
window_end_ts = current_time.to_i
|
|
199
204
|
@redis.with do |client|
|
|
200
|
-
client.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
client.multi do |tx|
|
|
206
|
+
tx.unlink(
|
|
207
|
+
*failure_bucket_keys(config, window_end: window_end_ts),
|
|
208
|
+
*success_bucket_keys(config, window_end: window_end_ts)
|
|
209
|
+
)
|
|
210
|
+
tx.hdel(metadata_key(config), "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
else
|
|
214
|
+
@redis.with do |client|
|
|
215
|
+
client.hdel(metadata_key(config), "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes")
|
|
204
216
|
end
|
|
205
217
|
end
|
|
206
218
|
end
|
|
207
219
|
|
|
220
|
+
def clear_recovery_metrics(config)
|
|
221
|
+
@redis.with do |client|
|
|
222
|
+
client.del(recovery_metrics_key(config))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
208
226
|
private def state_snapshot_from_hash(data, time: current_time)
|
|
209
227
|
breached_at = data[:breached_at]&.to_f
|
|
210
228
|
recovery_scheduled_after = data[:recovery_scheduled_after]&.to_f
|
|
@@ -227,31 +245,27 @@ module Stoplight
|
|
|
227
245
|
current_ts = current_time.to_f
|
|
228
246
|
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
229
247
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
end
|
|
248
|
+
scripting.call(
|
|
249
|
+
:record_failure,
|
|
250
|
+
args: [current_ts, SecureRandom.hex(12), serialize_failure(failure), metrics_ttl, metadata_ttl],
|
|
251
|
+
keys: [
|
|
252
|
+
metadata_key(config),
|
|
253
|
+
config.window_size && errors_key(config, time: current_ts)
|
|
254
|
+
].compact
|
|
255
|
+
)
|
|
240
256
|
end
|
|
241
257
|
|
|
242
258
|
def record_success(config, request_id: SecureRandom.hex(12))
|
|
243
259
|
current_ts = current_time.to_f
|
|
244
260
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
254
|
-
end
|
|
261
|
+
scripting.call(
|
|
262
|
+
:record_success,
|
|
263
|
+
args: [current_ts, request_id, metrics_ttl, metadata_ttl],
|
|
264
|
+
keys: [
|
|
265
|
+
metadata_key(config),
|
|
266
|
+
config.window_size && successes_key(config, time: current_ts)
|
|
267
|
+
].compact
|
|
268
|
+
)
|
|
255
269
|
end
|
|
256
270
|
|
|
257
271
|
# Records a failed recovery probe for a specific light configuration.
|
|
@@ -264,36 +278,25 @@ module Stoplight
|
|
|
264
278
|
current_ts = current_time.to_f
|
|
265
279
|
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
266
280
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
metadata_key(config),
|
|
273
|
-
recovery_probe_errors_key(config, time: current_ts)
|
|
274
|
-
].compact
|
|
275
|
-
)
|
|
276
|
-
end
|
|
281
|
+
scripting.call(
|
|
282
|
+
:record_recovery_probe_failure,
|
|
283
|
+
args: [current_ts, serialize_failure(failure)],
|
|
284
|
+
keys: [recovery_metrics_key(config)]
|
|
285
|
+
)
|
|
277
286
|
end
|
|
278
287
|
|
|
279
288
|
# Records a successful recovery probe for a specific light configuration.
|
|
280
289
|
#
|
|
281
290
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
282
|
-
# @param request_id [String] The unique identifier for the request
|
|
283
291
|
# @return [void]
|
|
284
|
-
def record_recovery_probe_success(config
|
|
292
|
+
def record_recovery_probe_success(config)
|
|
285
293
|
current_ts = current_time.to_f
|
|
286
294
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
metadata_key(config),
|
|
293
|
-
recovery_probe_successes_key(config, time: current_ts)
|
|
294
|
-
].compact
|
|
295
|
-
)
|
|
296
|
-
end
|
|
295
|
+
scripting.call(
|
|
296
|
+
:record_recovery_probe_success,
|
|
297
|
+
args: [current_ts],
|
|
298
|
+
keys: [recovery_metrics_key(config)]
|
|
299
|
+
)
|
|
297
300
|
end
|
|
298
301
|
|
|
299
302
|
def set_state(config, state)
|
|
@@ -325,6 +328,18 @@ module Stoplight
|
|
|
325
328
|
end
|
|
326
329
|
end
|
|
327
330
|
|
|
331
|
+
# @param config [Stoplight::Domain::Config]
|
|
332
|
+
# @return [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockToken, nil]
|
|
333
|
+
def acquire_recovery_lock(config)
|
|
334
|
+
recovery_lock_store.acquire_lock(config.name)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @param lock [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockToken]
|
|
338
|
+
# @return [void]
|
|
339
|
+
def release_recovery_lock(lock)
|
|
340
|
+
recovery_lock_store.release_lock(lock)
|
|
341
|
+
end
|
|
342
|
+
|
|
328
343
|
# Transitions to GREEN state and ensures only one notification
|
|
329
344
|
#
|
|
330
345
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
@@ -333,13 +348,11 @@ module Stoplight
|
|
|
333
348
|
current_ts = current_time.to_f
|
|
334
349
|
meta_key = metadata_key(config)
|
|
335
350
|
|
|
336
|
-
became_green =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
)
|
|
342
|
-
end
|
|
351
|
+
became_green = scripting.call(
|
|
352
|
+
:transition_to_green,
|
|
353
|
+
args: [current_ts],
|
|
354
|
+
keys: [meta_key]
|
|
355
|
+
)
|
|
343
356
|
became_green == 1
|
|
344
357
|
end
|
|
345
358
|
|
|
@@ -351,13 +364,11 @@ module Stoplight
|
|
|
351
364
|
current_ts = current_time.to_f
|
|
352
365
|
meta_key = metadata_key(config)
|
|
353
366
|
|
|
354
|
-
became_yellow =
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
360
|
-
end
|
|
367
|
+
became_yellow = scripting.call(
|
|
368
|
+
:transition_to_yellow,
|
|
369
|
+
args: [current_ts],
|
|
370
|
+
keys: [meta_key]
|
|
371
|
+
)
|
|
361
372
|
became_yellow == 1
|
|
362
373
|
end
|
|
363
374
|
|
|
@@ -370,13 +381,11 @@ module Stoplight
|
|
|
370
381
|
meta_key = metadata_key(config)
|
|
371
382
|
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
372
383
|
|
|
373
|
-
became_red =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
)
|
|
379
|
-
end
|
|
384
|
+
became_red = scripting.call(
|
|
385
|
+
:transition_to_red,
|
|
386
|
+
args: [current_ts, recovery_scheduled_after_ts],
|
|
387
|
+
keys: [meta_key]
|
|
388
|
+
)
|
|
380
389
|
|
|
381
390
|
became_red == 1
|
|
382
391
|
end
|
|
@@ -384,9 +393,11 @@ module Stoplight
|
|
|
384
393
|
# Removes all traces of a light from Redis metadata (metrics will expire by TTL).
|
|
385
394
|
#
|
|
386
395
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
387
|
-
# @return [
|
|
396
|
+
# @return [void]
|
|
388
397
|
def delete_light(config)
|
|
389
|
-
@redis.then
|
|
398
|
+
@redis.then do |client|
|
|
399
|
+
client.del(metadata_key(config), recovery_metrics_key(config))
|
|
400
|
+
end
|
|
390
401
|
end
|
|
391
402
|
|
|
392
403
|
# @param failure_json [String, nil]
|
|
@@ -464,12 +475,8 @@ module Stoplight
|
|
|
464
475
|
self.class.bucket_key(config.name, metric: "failure", time:)
|
|
465
476
|
end
|
|
466
477
|
|
|
467
|
-
private def
|
|
468
|
-
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
private def recovery_probe_errors_key(config, time:)
|
|
472
|
-
self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
|
|
478
|
+
private def recovery_metrics_key(config)
|
|
479
|
+
key("recovery_metrics", config.name)
|
|
473
480
|
end
|
|
474
481
|
|
|
475
482
|
private def metadata_key(config)
|
|
@@ -508,42 +515,6 @@ module Stoplight
|
|
|
508
515
|
rand <= probability
|
|
509
516
|
end
|
|
510
517
|
|
|
511
|
-
private def record_success_sha
|
|
512
|
-
@record_success_sha ||= @redis.then do |client|
|
|
513
|
-
client.script("load", Lua::RECORD_SUCCESS)
|
|
514
|
-
end
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
private def get_metrics_sha
|
|
518
|
-
@get_metrics_sha ||= @redis.then do |client|
|
|
519
|
-
client.script("load", Lua::GET_METRICS)
|
|
520
|
-
end
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
private def transition_to_yellow_sha
|
|
524
|
-
@transition_to_yellow_sha ||= @redis.then do |client|
|
|
525
|
-
client.script("load", Lua::TRANSITION_TO_YELLOW)
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
private def transition_to_red_sha
|
|
530
|
-
@transition_to_red_sha ||= @redis.then do |client|
|
|
531
|
-
client.script("load", Lua::TRANSITION_TO_RED)
|
|
532
|
-
end
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
private def transition_to_green_sha
|
|
536
|
-
@transition_to_green_sha ||= @redis.then do |client|
|
|
537
|
-
client.script("load", Lua::TRANSITION_TO_GREEN)
|
|
538
|
-
end
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
private def record_failure_sha
|
|
542
|
-
@record_failure_sha ||= @redis.then do |client|
|
|
543
|
-
client.script("load", Lua::RECORD_FAILURE)
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
|
|
547
518
|
private def current_time
|
|
548
519
|
Time.now
|
|
549
520
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Notifier
|
|
6
|
+
# A wrapper around a notifier that provides fail-safe mechanisms using a
|
|
7
|
+
# circuit breaker. It ensures that a notification can gracefully
|
|
8
|
+
# handle failures.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
class FailSafe < Domain::StateTransitionNotifier
|
|
12
|
+
# @!attribute [r] notifier
|
|
13
|
+
# @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
|
|
14
|
+
attr_reader :notifier
|
|
15
|
+
|
|
16
|
+
# @!attribute [r] error_notifier
|
|
17
|
+
# @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
|
|
18
|
+
attr_reader :error_notifier
|
|
19
|
+
|
|
20
|
+
# Initializes a new instance of the +FailSafe+ class.
|
|
21
|
+
#
|
|
22
|
+
# @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
|
|
23
|
+
# @param error_notifier [Proc] called when wrapped data store fails
|
|
24
|
+
def initialize(notifier:, error_notifier:)
|
|
25
|
+
@notifier = notifier
|
|
26
|
+
@error_notifier = error_notifier
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sends a notification using the wrapped notifier with fail-safe mechanisms.
|
|
30
|
+
#
|
|
31
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
32
|
+
# @param from_color [String] The initial color of the light.
|
|
33
|
+
# @param to_color [String] The target color of the light.
|
|
34
|
+
# @param error [Exception, nil] An optional error to include in the notification.
|
|
35
|
+
# @return [void]
|
|
36
|
+
def notify(config, from_color, to_color, error = nil)
|
|
37
|
+
fallback = proc do |exception|
|
|
38
|
+
error_notifier.call(exception) if exception
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
circuit_breaker.run(fallback) do
|
|
43
|
+
notifier.notify(config, from_color, to_color, error)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def ==(other)
|
|
49
|
+
other.is_a?(self.class) && notifier == other.notifier
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Stoplight::Light] The circuit breaker used to handle failures.
|
|
53
|
+
private def circuit_breaker
|
|
54
|
+
@circuit_breaker ||= Stoplight.system_light(
|
|
55
|
+
"stoplight:notifier:fail_safe:#{notifier.class.name}",
|
|
56
|
+
notifiers: []
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Storage
|
|
6
|
+
# Temporary adapter that bridges Domain::Storage::Metrics to existing DataStore.
|
|
7
|
+
#
|
|
8
|
+
# This compatibility layer allows the metrics abstraction to be introduced
|
|
9
|
+
# without breaking existing data store implementations. It delegates all
|
|
10
|
+
# operations to the data store's original methods.
|
|
11
|
+
#
|
|
12
|
+
# This class will be removed in a future versions once all data stores
|
|
13
|
+
# have native metrics implementations.
|
|
14
|
+
#
|
|
15
|
+
# @example Creating metrics for a circuit
|
|
16
|
+
# metrics = CompatibilityMetrics.new(
|
|
17
|
+
# data_store: redis_store,
|
|
18
|
+
# config: config
|
|
19
|
+
# )
|
|
20
|
+
# metrics.record_success
|
|
21
|
+
#
|
|
22
|
+
# @see Stoplight::Domain::Storage::Metrics
|
|
23
|
+
class CompatibilityMetrics < Domain::Storage::Metrics
|
|
24
|
+
private attr_reader :data_store
|
|
25
|
+
private attr_reader :config
|
|
26
|
+
|
|
27
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
28
|
+
# @param config [Stoplight::Domain::Config]
|
|
29
|
+
def initialize(data_store:, config:)
|
|
30
|
+
@data_store = data_store
|
|
31
|
+
@config = config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def metrics_snapshot = data_store.get_metrics(config)
|
|
35
|
+
|
|
36
|
+
# @return [void]
|
|
37
|
+
def record_success = data_store.record_success(config)
|
|
38
|
+
|
|
39
|
+
# @param error [StandardError]
|
|
40
|
+
# @return [void]
|
|
41
|
+
def record_failure(error) = data_store.record_failure(config, error)
|
|
42
|
+
|
|
43
|
+
# @return [void]
|
|
44
|
+
def clear = data_store.clear_metrics(config)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Storage
|
|
6
|
+
# Temporary adapter that bridges +Domain::Storage::RecoveryLock+ to existing DataStore.
|
|
7
|
+
#
|
|
8
|
+
# This compatibility layer allows the recovery lock abstraction to be
|
|
9
|
+
# introduced without breaking existing data store implementations. It
|
|
10
|
+
# delegates all lock operations to the data store's original methods.
|
|
11
|
+
#
|
|
12
|
+
# This adapter will be removed in a future versions once all
|
|
13
|
+
# data stores have native recovery lock implementations.
|
|
14
|
+
#
|
|
15
|
+
# @see Stoplight::Domain::Storage::RecoveryLock
|
|
16
|
+
class CompatibilityRecoveryLock < Domain::Storage::RecoveryLock
|
|
17
|
+
private attr_reader :data_store
|
|
18
|
+
private attr_reader :config
|
|
19
|
+
|
|
20
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
21
|
+
# @param config [Stoplight::Domain::Config]
|
|
22
|
+
def initialize(data_store:, config:)
|
|
23
|
+
@data_store = data_store
|
|
24
|
+
@config = config
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Stoplight::Domain::RecoveryLockToken, nil]
|
|
28
|
+
def acquire_lock = data_store.acquire_recovery_lock(config)
|
|
29
|
+
|
|
30
|
+
# @param lock [Stoplight::Domain::LockToken]
|
|
31
|
+
# @return [void]
|
|
32
|
+
def release_lock(lock) = data_store.release_recovery_lock(lock)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Storage
|
|
6
|
+
# When a circuit is RED (open), Stoplight periodically sends "recovery probes"
|
|
7
|
+
# to test whether the protected service has recovered. These test requests have
|
|
8
|
+
# different semantics than normal requests and their metrics are tracked separately.
|
|
9
|
+
#
|
|
10
|
+
# Like +CompatibilityMetrics+, this adapter will be replaced with purpose-built
|
|
11
|
+
# recovery metrics implementations (e.g., +ConsecutiveSuccessMetrics+) once the
|
|
12
|
+
# metrics extraction is complete.
|
|
13
|
+
#
|
|
14
|
+
# @example Recovery probe flow
|
|
15
|
+
# # Circuit is RED, start probing
|
|
16
|
+
# recovery_metrics = CompatibilityRecoveryMetrics.new(
|
|
17
|
+
# data_store: redis_store,
|
|
18
|
+
# config: circuit_config
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# recovery_metrics.record_success
|
|
22
|
+
# recovery_metrics.metrics_snapshot # => 1 success, 0 failures
|
|
23
|
+
#
|
|
24
|
+
# @see Stoplight::Domain::Storage::Metrics
|
|
25
|
+
class CompatibilityRecoveryMetrics < Domain::Storage::Metrics
|
|
26
|
+
private attr_reader :data_store
|
|
27
|
+
private attr_reader :config
|
|
28
|
+
|
|
29
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
30
|
+
# @param config [Stoplight::Domain::Config]
|
|
31
|
+
def initialize(data_store:, config:)
|
|
32
|
+
@data_store = data_store
|
|
33
|
+
@config = config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def metrics_snapshot = data_store.get_recovery_metrics(config)
|
|
37
|
+
|
|
38
|
+
# Tracks successful circuit breaker execution
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def record_success = data_store.record_recovery_probe_success(config)
|
|
42
|
+
|
|
43
|
+
# Tracks failed circuit breaker execution
|
|
44
|
+
#
|
|
45
|
+
# @param error [StandardError]
|
|
46
|
+
# @return [void]
|
|
47
|
+
def record_failure(error) = data_store.record_recovery_probe_failure(config, error)
|
|
48
|
+
|
|
49
|
+
# Clears metrics
|
|
50
|
+
# @return [void]
|
|
51
|
+
def clear = data_store.clear_recovery_metrics(config)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Storage
|
|
6
|
+
# Temporary adapter that bridges Domain::Storage::State to existing DataStore.
|
|
7
|
+
#
|
|
8
|
+
# This compatibility layer allows the state abstraction to be introduced
|
|
9
|
+
# without breaking existing data store implementations. It delegates all
|
|
10
|
+
# state operations to the data store's original methods.
|
|
11
|
+
#
|
|
12
|
+
# This adapter will be removed in a future versions once all
|
|
13
|
+
# data stores have native state storage implementations.
|
|
14
|
+
#
|
|
15
|
+
# @example Creating state storage for a circuit
|
|
16
|
+
# state = CompatibilityState.new(
|
|
17
|
+
# data_store: redis_store,
|
|
18
|
+
# config: circuit_config
|
|
19
|
+
# )
|
|
20
|
+
# state.set_state(State::LOCKED_RED)
|
|
21
|
+
# snapshot = state.state_snapshot
|
|
22
|
+
#
|
|
23
|
+
class CompatibilityState < Domain::Storage::State
|
|
24
|
+
# @!attribute data_store
|
|
25
|
+
# @return [Stoplight::Domain::DataStore]
|
|
26
|
+
private attr_reader :data_store
|
|
27
|
+
|
|
28
|
+
# @!attribute config
|
|
29
|
+
# @return [Stoplight::Domain::Config]
|
|
30
|
+
private attr_reader :config
|
|
31
|
+
|
|
32
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
33
|
+
# @param config [Stoplight::Domain::Config]
|
|
34
|
+
def initialize(data_store:, config:)
|
|
35
|
+
@data_store = data_store
|
|
36
|
+
@config = config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Stoplight::Domain::StateSnapshot]
|
|
40
|
+
def state_snapshot = data_store.get_state_snapshot(config)
|
|
41
|
+
|
|
42
|
+
# @param state [String]
|
|
43
|
+
# @return [String]
|
|
44
|
+
def set_state(state) = data_store.set_state(config, state)
|
|
45
|
+
|
|
46
|
+
# @param color [String]
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def transition_to_color(color) = data_store.transition_to_color(config, color)
|
|
49
|
+
|
|
50
|
+
# @return [void]
|
|
51
|
+
def clear = data_store.delete_light(config)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|