stoplight 5.5.0 → 5.6.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 +1 -1
- data/lib/stoplight/admin/actions/remove.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +5 -0
- data/lib/stoplight/admin/lights_repository.rb +12 -3
- data/lib/stoplight/admin/views/_card.erb +13 -1
- data/lib/stoplight/admin.rb +8 -0
- data/lib/stoplight/domain/data_store.rb +42 -6
- data/lib/stoplight/domain/failure.rb +2 -0
- data/lib/stoplight/domain/light.rb +7 -8
- data/lib/stoplight/domain/metrics.rb +85 -0
- data/lib/stoplight/domain/{metadata.rb → state_snapshot.rb} +29 -37
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +2 -2
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +3 -3
- data/lib/stoplight/domain/strategies/run_strategy.rb +2 -2
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +7 -6
- data/lib/stoplight/domain/tracker/recovery_probe.rb +9 -6
- data/lib/stoplight/domain/tracker/request.rb +5 -4
- data/lib/stoplight/domain/traffic_control/base.rb +5 -5
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +3 -7
- data/lib/stoplight/domain/traffic_control/error_rate.rb +3 -3
- data/lib/stoplight/domain/traffic_recovery/base.rb +6 -5
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +8 -6
- data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
- data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +125 -123
- data/lib/stoplight/infrastructure/data_store/redis/get_metrics.lua +26 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +1 -1
- data/lib/stoplight/infrastructure/data_store/redis.rb +115 -40
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/fail_safe_data_store.rb +27 -3
- metadata +7 -3
- data/lib/stoplight/infrastructure/data_store/redis/get_metadata.lua +0 -38
|
@@ -92,12 +92,13 @@ module Stoplight
|
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
# @param config [Stoplight::Domain::Config]
|
|
96
|
+
# @return [Stoplight::Domain::Metrics]
|
|
97
|
+
def get_metrics(config)
|
|
98
|
+
config.name
|
|
97
99
|
|
|
98
100
|
window_end_ts = current_time.to_f
|
|
99
|
-
window_start_ts = window_end_ts -
|
|
100
|
-
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
|
101
|
+
window_start_ts = window_end_ts - config.window_size.to_i
|
|
101
102
|
|
|
102
103
|
if config.window_size
|
|
103
104
|
failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
|
|
@@ -106,54 +107,121 @@ module Stoplight
|
|
|
106
107
|
failure_keys = []
|
|
107
108
|
success_keys = []
|
|
108
109
|
end
|
|
110
|
+
|
|
111
|
+
successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
|
|
112
|
+
client.evalsha(
|
|
113
|
+
get_metrics_sha,
|
|
114
|
+
argv: [
|
|
115
|
+
failure_keys.count,
|
|
116
|
+
window_start_ts,
|
|
117
|
+
window_end_ts,
|
|
118
|
+
"last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
|
|
119
|
+
],
|
|
120
|
+
keys: [
|
|
121
|
+
metadata_key(config),
|
|
122
|
+
*success_keys,
|
|
123
|
+
*failure_keys
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
Domain::Metrics.new(
|
|
129
|
+
successes: (successes if config.window_size),
|
|
130
|
+
errors: (errors if config.window_size),
|
|
131
|
+
total_consecutive_errors: consecutive_errors.to_i,
|
|
132
|
+
total_consecutive_successes: consecutive_successes.to_i,
|
|
133
|
+
last_error: deserialize_failure(last_error_json),
|
|
134
|
+
last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param config [Stoplight::Domain::Config]
|
|
139
|
+
# @return [Stoplight::Domain::Metrics]
|
|
140
|
+
def get_recovery_metrics(config)
|
|
141
|
+
config.name
|
|
142
|
+
|
|
143
|
+
window_end_ts = current_time.to_f
|
|
144
|
+
window_start_ts = window_end_ts - config.cool_off_time
|
|
145
|
+
|
|
109
146
|
recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
|
|
110
147
|
recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
|
|
111
148
|
|
|
112
|
-
successes, errors,
|
|
149
|
+
successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
|
|
113
150
|
client.evalsha(
|
|
114
|
-
|
|
151
|
+
get_metrics_sha,
|
|
115
152
|
argv: [
|
|
116
|
-
failure_keys.count,
|
|
117
153
|
recovery_probe_failure_keys.count,
|
|
118
154
|
window_start_ts,
|
|
119
155
|
window_end_ts,
|
|
120
|
-
|
|
156
|
+
"last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
|
|
121
157
|
],
|
|
122
158
|
keys: [
|
|
123
159
|
metadata_key(config),
|
|
124
|
-
*success_keys,
|
|
125
|
-
*failure_keys,
|
|
126
160
|
*recovery_probe_success_keys,
|
|
127
161
|
*recovery_probe_failure_keys
|
|
128
162
|
]
|
|
129
163
|
)
|
|
130
164
|
end
|
|
131
|
-
meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
|
|
132
|
-
last_error_json = meta_hash.delete(:last_error_json)
|
|
133
|
-
last_error = deserialize_failure(last_error_json) if last_error_json
|
|
134
165
|
|
|
135
|
-
Domain::
|
|
136
|
-
current_time:,
|
|
166
|
+
Domain::Metrics.new(
|
|
137
167
|
successes:,
|
|
138
168
|
errors:,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
last_error
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
total_consecutive_errors: consecutive_errors.to_i,
|
|
170
|
+
total_consecutive_successes: consecutive_successes.to_i,
|
|
171
|
+
last_error: deserialize_failure(last_error_json),
|
|
172
|
+
last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @return [Stoplight::Domain::StateSnapshot]
|
|
177
|
+
def get_state_snapshot(config)
|
|
178
|
+
detect_clock_skew
|
|
179
|
+
|
|
180
|
+
breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw = @redis.with do |client|
|
|
181
|
+
client.hmget(metadata_key(config), :breached_at, :locked_state, :recovery_scheduled_after, :recovery_started_at)
|
|
182
|
+
end
|
|
183
|
+
breached_at = breached_at_raw&.to_f
|
|
184
|
+
recovery_scheduled_after = recovery_scheduled_after_raw&.to_f
|
|
185
|
+
recovery_started_at = recovery_started_at_raw&.to_f
|
|
186
|
+
|
|
187
|
+
Domain::StateSnapshot.new(
|
|
188
|
+
breached_at: (Time.at(breached_at) if breached_at),
|
|
189
|
+
locked_state: locked_state || Domain::State::UNLOCKED,
|
|
190
|
+
recovery_scheduled_after: (Time.at(recovery_scheduled_after) if recovery_scheduled_after),
|
|
191
|
+
recovery_started_at: (Time.at(recovery_started_at) if recovery_started_at),
|
|
192
|
+
time: current_time
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def clear_windowed_metrics(config)
|
|
197
|
+
if config.window_size
|
|
198
|
+
window_end_ts = current_time.to_i
|
|
199
|
+
@redis.with do |client|
|
|
200
|
+
client.unlink(
|
|
201
|
+
*failure_bucket_keys(config, window_end: window_end_ts),
|
|
202
|
+
*success_bucket_keys(config, window_end: window_end_ts)
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private def state_snapshot_from_hash(data, time: current_time)
|
|
209
|
+
breached_at = data[:breached_at]&.to_f
|
|
210
|
+
recovery_scheduled_after = data[:recovery_scheduled_after]&.to_f
|
|
211
|
+
recovery_started_at = data[:recovery_started_at]&.to_f
|
|
212
|
+
|
|
213
|
+
Domain::StateSnapshot.new(
|
|
214
|
+
breached_at: (Time.at(breached_at) if breached_at),
|
|
215
|
+
locked_state: data[:locked_state] || Domain::State::UNLOCKED,
|
|
216
|
+
recovery_scheduled_after: (Time.at(recovery_scheduled_after) if recovery_scheduled_after),
|
|
217
|
+
recovery_started_at: (Time.at(recovery_started_at) if recovery_started_at),
|
|
218
|
+
time:
|
|
151
219
|
)
|
|
152
220
|
end
|
|
153
221
|
|
|
154
222
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
155
223
|
# @param exception [Exception]
|
|
156
|
-
# @return [
|
|
224
|
+
# @return [void]
|
|
157
225
|
def record_failure(config, exception)
|
|
158
226
|
current_time = self.current_time
|
|
159
227
|
current_ts = current_time.to_f
|
|
@@ -169,7 +237,6 @@ module Stoplight
|
|
|
169
237
|
].compact
|
|
170
238
|
)
|
|
171
239
|
end
|
|
172
|
-
get_metadata(config)
|
|
173
240
|
end
|
|
174
241
|
|
|
175
242
|
def record_success(config, request_id: SecureRandom.hex(12))
|
|
@@ -191,7 +258,7 @@ module Stoplight
|
|
|
191
258
|
#
|
|
192
259
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
193
260
|
# @param exception [Exception]
|
|
194
|
-
# @return [
|
|
261
|
+
# @return [void]
|
|
195
262
|
def record_recovery_probe_failure(config, exception)
|
|
196
263
|
current_time = self.current_time
|
|
197
264
|
current_ts = current_time.to_f
|
|
@@ -207,14 +274,13 @@ module Stoplight
|
|
|
207
274
|
].compact
|
|
208
275
|
)
|
|
209
276
|
end
|
|
210
|
-
get_metadata(config)
|
|
211
277
|
end
|
|
212
278
|
|
|
213
279
|
# Records a successful recovery probe for a specific light configuration.
|
|
214
280
|
#
|
|
215
281
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
216
282
|
# @param request_id [String] The unique identifier for the request
|
|
217
|
-
# @return [
|
|
283
|
+
# @return [void]
|
|
218
284
|
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
|
|
219
285
|
current_ts = current_time.to_f
|
|
220
286
|
|
|
@@ -228,7 +294,6 @@ module Stoplight
|
|
|
228
294
|
].compact
|
|
229
295
|
)
|
|
230
296
|
end
|
|
231
|
-
get_metadata(config)
|
|
232
297
|
end
|
|
233
298
|
|
|
234
299
|
def set_state(config, state)
|
|
@@ -283,7 +348,7 @@ module Stoplight
|
|
|
283
348
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
284
349
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
285
350
|
private def transition_to_yellow(config)
|
|
286
|
-
current_ts = current_time.
|
|
351
|
+
current_ts = current_time.to_f
|
|
287
352
|
meta_key = metadata_key(config)
|
|
288
353
|
|
|
289
354
|
became_yellow = @redis.then do |client|
|
|
@@ -301,7 +366,7 @@ module Stoplight
|
|
|
301
366
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
302
367
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
303
368
|
private def transition_to_red(config)
|
|
304
|
-
current_ts = current_time.
|
|
369
|
+
current_ts = current_time.to_f
|
|
305
370
|
meta_key = metadata_key(config)
|
|
306
371
|
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
307
372
|
|
|
@@ -316,9 +381,19 @@ module Stoplight
|
|
|
316
381
|
became_red == 1
|
|
317
382
|
end
|
|
318
383
|
|
|
319
|
-
#
|
|
320
|
-
#
|
|
384
|
+
# Removes all traces of a light from Redis metadata (metrics will expire by TTL).
|
|
385
|
+
#
|
|
386
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
387
|
+
# @return [Integer] number of keys removed
|
|
388
|
+
def delete_light(config)
|
|
389
|
+
@redis.then { |client| client.del(metadata_key(config)) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# @param failure_json [String, nil]
|
|
393
|
+
# @return [Domain::Failure, nil]
|
|
321
394
|
private def deserialize_failure(failure_json)
|
|
395
|
+
return if failure_json.nil?
|
|
396
|
+
|
|
322
397
|
object = JSON.parse(failure_json)
|
|
323
398
|
error_object = object["error"]
|
|
324
399
|
|
|
@@ -439,9 +514,9 @@ module Stoplight
|
|
|
439
514
|
end
|
|
440
515
|
end
|
|
441
516
|
|
|
442
|
-
private def
|
|
443
|
-
@
|
|
444
|
-
client.script("load", Lua::
|
|
517
|
+
private def get_metrics_sha
|
|
518
|
+
@get_metrics_sha ||= @redis.then do |client|
|
|
519
|
+
client.script("load", Lua::GET_METRICS)
|
|
445
520
|
end
|
|
446
521
|
end
|
|
447
522
|
|
data/lib/stoplight/version.rb
CHANGED
|
@@ -62,9 +62,27 @@ module Stoplight
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def
|
|
66
|
-
with_fallback(:
|
|
67
|
-
data_store.
|
|
65
|
+
def get_metrics(config, *args, **kwargs)
|
|
66
|
+
with_fallback(:get_metrics, config, *args, **kwargs) do
|
|
67
|
+
data_store.get_metrics(config, *args, **kwargs)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_recovery_metrics(config, *args, **kwargs)
|
|
72
|
+
with_fallback(:get_recovery_metrics, config, *args, **kwargs) do
|
|
73
|
+
data_store.get_recovery_metrics(config, *args, **kwargs)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_state_snapshot(config)
|
|
78
|
+
with_fallback(:get_state_snapshot, config) do
|
|
79
|
+
data_store.get_state_snapshot(config)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear_windowed_metrics(config)
|
|
84
|
+
with_fallback(:clear_windowed_metrics, config) do
|
|
85
|
+
data_store.clear_windowed_metrics(config)
|
|
68
86
|
end
|
|
69
87
|
end
|
|
70
88
|
|
|
@@ -104,6 +122,12 @@ module Stoplight
|
|
|
104
122
|
end
|
|
105
123
|
end
|
|
106
124
|
|
|
125
|
+
def delete_light(config, *args, **kwargs)
|
|
126
|
+
with_fallback(:delete_light, config, *args, **kwargs) do
|
|
127
|
+
data_store.delete_light(config, *args, **kwargs)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
107
131
|
def ==(other)
|
|
108
132
|
other.is_a?(self.class) && other.data_store == data_store && other.error_notifier == error_notifier
|
|
109
133
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stoplight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cameron Desautels
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/stoplight/admin/actions/lock_all_green.rb
|
|
48
48
|
- lib/stoplight/admin/actions/lock_green.rb
|
|
49
49
|
- lib/stoplight/admin/actions/lock_red.rb
|
|
50
|
+
- lib/stoplight/admin/actions/remove.rb
|
|
50
51
|
- lib/stoplight/admin/actions/stats.rb
|
|
51
52
|
- lib/stoplight/admin/actions/unlock.rb
|
|
52
53
|
- lib/stoplight/admin/dependencies.rb
|
|
@@ -66,8 +67,9 @@ files:
|
|
|
66
67
|
- lib/stoplight/domain/light.rb
|
|
67
68
|
- lib/stoplight/domain/light/configuration_builder_interface.rb
|
|
68
69
|
- lib/stoplight/domain/light_factory.rb
|
|
69
|
-
- lib/stoplight/domain/
|
|
70
|
+
- lib/stoplight/domain/metrics.rb
|
|
70
71
|
- lib/stoplight/domain/state.rb
|
|
72
|
+
- lib/stoplight/domain/state_snapshot.rb
|
|
71
73
|
- lib/stoplight/domain/state_transition_notifier.rb
|
|
72
74
|
- lib/stoplight/domain/strategies/green_run_strategy.rb
|
|
73
75
|
- lib/stoplight/domain/strategies/red_run_strategy.rb
|
|
@@ -83,9 +85,11 @@ files:
|
|
|
83
85
|
- lib/stoplight/domain/traffic_recovery/base.rb
|
|
84
86
|
- lib/stoplight/domain/traffic_recovery/consecutive_successes.rb
|
|
85
87
|
- lib/stoplight/infrastructure/data_store/memory.rb
|
|
88
|
+
- lib/stoplight/infrastructure/data_store/memory/metrics.rb
|
|
86
89
|
- lib/stoplight/infrastructure/data_store/memory/sliding_window.rb
|
|
90
|
+
- lib/stoplight/infrastructure/data_store/memory/state.rb
|
|
87
91
|
- lib/stoplight/infrastructure/data_store/redis.rb
|
|
88
|
-
- lib/stoplight/infrastructure/data_store/redis/
|
|
92
|
+
- lib/stoplight/infrastructure/data_store/redis/get_metrics.lua
|
|
89
93
|
- lib/stoplight/infrastructure/data_store/redis/lua.rb
|
|
90
94
|
- lib/stoplight/infrastructure/data_store/redis/record_failure.lua
|
|
91
95
|
- lib/stoplight/infrastructure/data_store/redis/record_success.lua
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
local number_of_metric_buckets = tonumber(ARGV[1])
|
|
2
|
-
local number_of_recovery_buckets = tonumber(ARGV[2])
|
|
3
|
-
local window_start_ts = tonumber(ARGV[3])
|
|
4
|
-
local window_end_ts = tonumber(ARGV[4])
|
|
5
|
-
local recovery_window_start_ts = tonumber(ARGV[5])
|
|
6
|
-
|
|
7
|
-
local metadata_key = KEYS[1]
|
|
8
|
-
|
|
9
|
-
-- It possible that after a successful recovery, Stoplight still see metrics
|
|
10
|
-
-- that are older than the recovery window. To prevent this from happening,
|
|
11
|
-
-- we need to limit the start time of the window to the time of the last recovery.
|
|
12
|
-
local recovered_at = redis.call('HGET', metadata_key, "recovered_at")
|
|
13
|
-
if recovered_at then
|
|
14
|
-
window_start_ts = math.max(window_start_ts, tonumber(recovered_at))
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
local function count_events(start_idx, bucket_count, start_ts)
|
|
18
|
-
local total = 0
|
|
19
|
-
for idx = start_idx, start_idx + bucket_count - 1 do
|
|
20
|
-
total = total + tonumber(redis.call('ZCOUNT', KEYS[idx], start_ts, window_end_ts))
|
|
21
|
-
end
|
|
22
|
-
return total
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
local offset = 2
|
|
26
|
-
local successes = count_events(2, number_of_metric_buckets, window_start_ts)
|
|
27
|
-
|
|
28
|
-
offset = offset + number_of_metric_buckets
|
|
29
|
-
local errors = count_events(offset, number_of_metric_buckets, window_start_ts)
|
|
30
|
-
|
|
31
|
-
offset = offset + number_of_metric_buckets
|
|
32
|
-
local recovery_probe_successes = count_events(offset, number_of_recovery_buckets, recovery_window_start_ts)
|
|
33
|
-
|
|
34
|
-
offset = offset + number_of_recovery_buckets
|
|
35
|
-
local recovery_probe_errors = count_events(offset, number_of_recovery_buckets, recovery_window_start_ts)
|
|
36
|
-
|
|
37
|
-
local metadata = redis.call('HGETALL', metadata_key)
|
|
38
|
-
return {successes, errors, recovery_probe_successes, recovery_probe_errors, metadata}
|