stoplight 4.1.1 → 5.0.1
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 +288 -354
- data/lib/stoplight/admin/actions/action.rb +24 -0
- data/lib/stoplight/admin/actions/lock.rb +23 -0
- data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
- data/lib/stoplight/admin/actions/lock_green.rb +23 -0
- data/lib/stoplight/admin/actions/lock_red.rb +23 -0
- data/lib/stoplight/admin/actions/stats.rb +27 -0
- data/lib/stoplight/admin/actions/unlock.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +50 -0
- data/lib/stoplight/admin/helpers.rb +27 -0
- data/lib/stoplight/admin/lights_repository/light.rb +155 -0
- data/lib/stoplight/admin/lights_repository.rb +74 -0
- data/lib/stoplight/admin/lights_stats.rb +77 -0
- data/lib/stoplight/admin/views/_card.erb +120 -0
- data/lib/stoplight/admin/views/index.erb +36 -0
- data/lib/stoplight/admin/views/layout.erb +66 -0
- data/lib/stoplight/admin.rb +68 -0
- data/lib/stoplight/color.rb +3 -3
- data/lib/stoplight/config/config_provider.rb +62 -0
- data/lib/stoplight/config/library_default_config.rb +29 -0
- data/lib/stoplight/config/user_default_config.rb +83 -0
- data/lib/stoplight/data_store/base.rb +59 -33
- data/lib/stoplight/data_store/fail_safe.rb +105 -0
- data/lib/stoplight/data_store/memory.rb +257 -50
- data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
- data/lib/stoplight/data_store/redis/lua.rb +23 -0
- data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
- data/lib/stoplight/data_store/redis/record_success.lua +35 -0
- data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
- data/lib/stoplight/data_store/redis.rb +345 -106
- data/lib/stoplight/default.rb +11 -9
- data/lib/stoplight/error.rb +1 -13
- data/lib/stoplight/failure.rb +14 -13
- data/lib/stoplight/light/config.rb +118 -0
- data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
- data/lib/stoplight/light/green_run_strategy.rb +53 -0
- data/lib/stoplight/light/red_run_strategy.rb +26 -0
- data/lib/stoplight/light/run_strategy.rb +30 -0
- data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
- data/lib/stoplight/light.rb +164 -84
- data/lib/stoplight/metadata.rb +71 -0
- data/lib/stoplight/notifier/base.rb +14 -7
- data/lib/stoplight/notifier/fail_safe.rb +67 -0
- data/lib/stoplight/notifier/generic.rb +54 -5
- data/lib/stoplight/rspec/generic_notifier.rb +11 -12
- data/lib/stoplight/rspec.rb +1 -1
- data/lib/stoplight/state.rb +3 -3
- data/lib/stoplight/traffic_control/base.rb +35 -0
- data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
- data/lib/stoplight/traffic_recovery/base.rb +51 -0
- data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +111 -51
- metadata +49 -98
- data/lib/stoplight/builder.rb +0 -70
- data/lib/stoplight/circuit_breaker.rb +0 -102
- data/lib/stoplight/configurable.rb +0 -95
- data/lib/stoplight/configuration.rb +0 -126
- data/lib/stoplight/light/deprecated.rb +0 -44
- data/lib/stoplight/light/lockable.rb +0 -45
- data/lib/stoplight/light/runnable.rb +0 -127
- data/lib/stoplight/notifier.rb +0 -6
- data/spec/spec_helper.rb +0 -22
- data/spec/stoplight/builder_spec.rb +0 -165
- data/spec/stoplight/circuit_breaker_spec.rb +0 -43
- data/spec/stoplight/color_spec.rb +0 -39
- data/spec/stoplight/configurable_spec.rb +0 -25
- data/spec/stoplight/data_store/base_spec.rb +0 -71
- data/spec/stoplight/data_store/memory_spec.rb +0 -22
- data/spec/stoplight/data_store/redis_spec.rb +0 -45
- data/spec/stoplight/data_store_spec.rb +0 -9
- data/spec/stoplight/default_spec.rb +0 -80
- data/spec/stoplight/error_spec.rb +0 -39
- data/spec/stoplight/failure_spec.rb +0 -108
- data/spec/stoplight/light/lockable_spec.rb +0 -93
- data/spec/stoplight/light/runnable_spec.rb +0 -38
- data/spec/stoplight/light_spec.rb +0 -156
- data/spec/stoplight/notifier/base_spec.rb +0 -18
- data/spec/stoplight/notifier/generic_spec.rb +0 -50
- data/spec/stoplight/notifier/io_spec.rb +0 -41
- data/spec/stoplight/notifier/logger_spec.rb +0 -75
- data/spec/stoplight/notifier_spec.rb +0 -9
- data/spec/stoplight/state_spec.rb +0 -39
- data/spec/stoplight/version_spec.rb +0 -9
- data/spec/stoplight_spec.rb +0 -32
- data/spec/support/configurable.rb +0 -69
- data/spec/support/data_store/base/clear_failures.rb +0 -24
- data/spec/support/data_store/base/clear_state.rb +0 -20
- data/spec/support/data_store/base/get_all.rb +0 -44
- data/spec/support/data_store/base/get_failures.rb +0 -30
- data/spec/support/data_store/base/get_state.rb +0 -7
- data/spec/support/data_store/base/names.rb +0 -29
- data/spec/support/data_store/base/record_failures.rb +0 -70
- data/spec/support/data_store/base/set_state.rb +0 -15
- data/spec/support/data_store/base/with_notification_lock.rb +0 -27
- data/spec/support/data_store/base.rb +0 -21
- data/spec/support/database_cleaner.rb +0 -26
- data/spec/support/exception_helpers.rb +0 -9
- data/spec/support/light/runnable/color.rb +0 -79
- data/spec/support/light/runnable/run.rb +0 -247
- data/spec/support/light/runnable/state.rb +0 -31
- data/spec/support/light/runnable.rb +0 -5
@@ -1,93 +1,300 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "monitor"
|
4
4
|
|
5
5
|
module Stoplight
|
6
6
|
module DataStore
|
7
7
|
# @see Base
|
8
8
|
class Memory < Base
|
9
9
|
include MonitorMixin
|
10
|
-
KEY_SEPARATOR =
|
10
|
+
KEY_SEPARATOR = ":"
|
11
11
|
|
12
12
|
def initialize
|
13
|
-
@
|
14
|
-
@
|
15
|
-
|
16
|
-
|
13
|
+
@errors = Hash.new { |h, k| h[k] = [] }
|
14
|
+
@successes = Hash.new { |h, k| h[k] = [] }
|
15
|
+
|
16
|
+
@recovery_probe_errors = Hash.new { |h, k| h[k] = [] }
|
17
|
+
@recovery_probe_successes = Hash.new { |h, k| h[k] = [] }
|
18
|
+
|
19
|
+
@metadata = Hash.new { |h, k| h[k] = Metadata.new }
|
20
|
+
super # MonitorMixin
|
17
21
|
end
|
18
22
|
|
23
|
+
# @return [Array<String>]
|
19
24
|
def names
|
20
|
-
synchronize { @
|
25
|
+
synchronize { @metadata.keys }
|
21
26
|
end
|
22
27
|
|
23
|
-
|
24
|
-
|
28
|
+
# @param config [Stoplight::Light::Config]
|
29
|
+
# @return [Stoplight::Metadata]
|
30
|
+
def get_metadata(config)
|
31
|
+
light_name = config.name
|
32
|
+
window_end = Time.now
|
33
|
+
recovery_window = (window_end - config.cool_off_time + 1)..window_end
|
34
|
+
|
35
|
+
synchronize do
|
36
|
+
recovered_at = @metadata[light_name].recovered_at
|
37
|
+
window = if config.window_size
|
38
|
+
window_start = [recovered_at, (window_end - config.window_size + 1)].compact.max
|
39
|
+
(window_start..window_end)
|
40
|
+
else
|
41
|
+
(..window_end)
|
42
|
+
end
|
43
|
+
|
44
|
+
errors = @errors[config.name].count do |request_time|
|
45
|
+
window.cover?(request_time)
|
46
|
+
end
|
47
|
+
|
48
|
+
successes = @successes[config.name].count do |request_time|
|
49
|
+
window.cover?(request_time)
|
50
|
+
end
|
51
|
+
|
52
|
+
recovery_probe_errors = @recovery_probe_errors[config.name].count do |request_time|
|
53
|
+
recovery_window.cover?(request_time)
|
54
|
+
end
|
55
|
+
recovery_probe_successes = @recovery_probe_successes[config.name].count do |request_time|
|
56
|
+
recovery_window.cover?(request_time)
|
57
|
+
end
|
58
|
+
|
59
|
+
@metadata[light_name].with(
|
60
|
+
errors:,
|
61
|
+
successes:,
|
62
|
+
recovery_probe_errors:,
|
63
|
+
recovery_probe_successes:
|
64
|
+
)
|
65
|
+
end
|
25
66
|
end
|
26
67
|
|
27
|
-
|
28
|
-
|
68
|
+
# @param metrics [<Time>]
|
69
|
+
# @param window_size [Numeric, nil]
|
70
|
+
# @return [void]
|
71
|
+
def cleanup(metrics, window_size:)
|
72
|
+
min_age = Time.now - [window_size&.*(3), METRICS_RETENTION_TIME].compact.min
|
73
|
+
|
74
|
+
metrics.reject! { _1 < min_age }
|
29
75
|
end
|
30
76
|
|
31
|
-
|
77
|
+
# @param config [Stoplight::Light::Config]
|
78
|
+
# @param failure [Stoplight::Failure]
|
79
|
+
# @return [Stoplight::Metadata]
|
80
|
+
def record_failure(config, failure)
|
81
|
+
light_name = config.name
|
82
|
+
|
32
83
|
synchronize do
|
33
|
-
light_name
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
84
|
+
@errors[light_name].unshift(failure.time) if config.window_size
|
85
|
+
|
86
|
+
cleanup(@errors[light_name], window_size: config.window_size)
|
87
|
+
|
88
|
+
metadata = @metadata[light_name]
|
89
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || failure.time > metadata.last_error_at
|
90
|
+
metadata.with(
|
91
|
+
last_error_at: failure.time,
|
92
|
+
last_error: failure,
|
93
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
94
|
+
consecutive_successes: 0
|
95
|
+
)
|
96
|
+
else
|
97
|
+
metadata.with(
|
98
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
99
|
+
consecutive_successes: 0
|
100
|
+
)
|
101
|
+
end
|
102
|
+
get_metadata(config)
|
41
103
|
end
|
42
104
|
end
|
43
105
|
|
44
|
-
|
45
|
-
|
46
|
-
|
106
|
+
# @param config [Stoplight::Light::Config]
|
107
|
+
# @param request_id [String]
|
108
|
+
# @param request_time [Time]
|
109
|
+
# @return [void]
|
110
|
+
def record_success(config, request_time: Time.now, request_id: SecureRandom.hex(12))
|
111
|
+
light_name = config.name
|
47
112
|
|
48
|
-
|
49
|
-
|
50
|
-
|
113
|
+
synchronize do
|
114
|
+
@successes[light_name].unshift(request_time) if config.window_size
|
115
|
+
cleanup(@successes[light_name], window_size: config.window_size)
|
51
116
|
|
52
|
-
|
53
|
-
|
117
|
+
metadata = @metadata[light_name]
|
118
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
|
119
|
+
metadata.with(
|
120
|
+
last_success_at: request_time,
|
121
|
+
consecutive_errors: 0,
|
122
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
123
|
+
)
|
124
|
+
else
|
125
|
+
metadata.with(
|
126
|
+
consecutive_errors: 0,
|
127
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
128
|
+
)
|
129
|
+
end
|
130
|
+
end
|
54
131
|
end
|
55
132
|
|
56
|
-
|
57
|
-
|
133
|
+
# @param config [Stoplight::Light::Config]
|
134
|
+
# @param failure [Stoplight::Failure]
|
135
|
+
# @return [Stoplight::Metadata]
|
136
|
+
def record_recovery_probe_failure(config, failure)
|
137
|
+
light_name = config.name
|
138
|
+
|
139
|
+
synchronize do
|
140
|
+
@recovery_probe_errors[light_name].unshift(failure.time)
|
141
|
+
cleanup(@recovery_probe_errors[light_name], window_size: config.cool_off_time)
|
142
|
+
|
143
|
+
metadata = @metadata[light_name]
|
144
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || failure.time > metadata.last_error_at
|
145
|
+
metadata.with(
|
146
|
+
last_error_at: failure.time,
|
147
|
+
last_error: failure,
|
148
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
149
|
+
consecutive_successes: 0
|
150
|
+
)
|
151
|
+
else
|
152
|
+
metadata.with(
|
153
|
+
consecutive_errors: metadata.consecutive_errors.succ,
|
154
|
+
consecutive_successes: 0
|
155
|
+
)
|
156
|
+
end
|
157
|
+
get_metadata(config)
|
158
|
+
end
|
58
159
|
end
|
59
160
|
|
60
|
-
|
161
|
+
# @param config [Stoplight::Light::Config]
|
162
|
+
# @param request_id [String]
|
163
|
+
# @param request_time [Time]
|
164
|
+
# @return [Stoplight::Metadata]
|
165
|
+
def record_recovery_probe_success(config, request_time: Time.now, request_id: SecureRandom.hex(12))
|
166
|
+
light_name = config.name
|
167
|
+
|
61
168
|
synchronize do
|
62
|
-
|
63
|
-
|
169
|
+
@recovery_probe_successes[light_name].unshift(request_time)
|
170
|
+
cleanup(@recovery_probe_successes[light_name], window_size: config.cool_off_time)
|
64
171
|
|
65
|
-
|
172
|
+
metadata = @metadata[light_name]
|
173
|
+
recovery_started_at = metadata.recovery_started_at || request_time
|
174
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
|
175
|
+
metadata.with(
|
176
|
+
last_success_at: request_time,
|
177
|
+
recovery_started_at:,
|
178
|
+
consecutive_errors: 0,
|
179
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
180
|
+
)
|
181
|
+
else
|
182
|
+
metadata.with(
|
183
|
+
recovery_started_at:,
|
184
|
+
consecutive_errors: 0,
|
185
|
+
consecutive_successes: metadata.consecutive_successes.succ
|
186
|
+
)
|
66
187
|
end
|
188
|
+
get_metadata(config)
|
67
189
|
end
|
68
190
|
end
|
69
191
|
|
70
|
-
|
192
|
+
# @param config [Stoplight::Light::Config]
|
193
|
+
# @param state [String]
|
194
|
+
# @return [String]
|
195
|
+
def set_state(config, state)
|
196
|
+
light_name = config.name
|
71
197
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
198
|
+
synchronize do
|
199
|
+
metadata = @metadata[light_name]
|
200
|
+
@metadata[light_name] = metadata.with(locked_state: state)
|
201
|
+
end
|
202
|
+
state
|
76
203
|
end
|
77
204
|
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# @param
|
81
|
-
# @
|
82
|
-
|
83
|
-
|
205
|
+
# Combined method that performs the state transition based on color
|
206
|
+
#
|
207
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
208
|
+
# @param color [String] The color to transition to ("GREEN", "YELLOW", or "RED")
|
209
|
+
# @param current_time [Time]
|
210
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
211
|
+
def transition_to_color(config, color, current_time: Time.now)
|
212
|
+
case color
|
213
|
+
when Color::GREEN
|
214
|
+
transition_to_green(config)
|
215
|
+
when Color::YELLOW
|
216
|
+
transition_to_yellow(config, current_time:)
|
217
|
+
when Color::RED
|
218
|
+
transition_to_red(config, current_time:)
|
219
|
+
else
|
220
|
+
raise ArgumentError, "Invalid color: #{color}"
|
221
|
+
end
|
84
222
|
end
|
85
223
|
|
86
|
-
#
|
87
|
-
#
|
88
|
-
|
89
|
-
|
90
|
-
|
224
|
+
# Transitions to GREEN state and ensures only one notification
|
225
|
+
#
|
226
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
227
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
228
|
+
private def transition_to_green(config, current_time: Time.now)
|
229
|
+
light_name = config.name
|
230
|
+
|
231
|
+
synchronize do
|
232
|
+
metadata = @metadata[light_name]
|
233
|
+
if metadata.recovered_at
|
234
|
+
false
|
235
|
+
else
|
236
|
+
@metadata[light_name] = metadata.with(
|
237
|
+
recovered_at: current_time,
|
238
|
+
recovery_started_at: nil,
|
239
|
+
breached_at: nil,
|
240
|
+
recovery_scheduled_after: nil
|
241
|
+
)
|
242
|
+
true
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
248
|
+
#
|
249
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
250
|
+
# @param current_time [Time]
|
251
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
252
|
+
private def transition_to_yellow(config, current_time: Time.now)
|
253
|
+
light_name = config.name
|
254
|
+
|
255
|
+
synchronize do
|
256
|
+
metadata = @metadata[light_name]
|
257
|
+
if metadata.recovery_started_at
|
258
|
+
false
|
259
|
+
else
|
260
|
+
@metadata[light_name] = metadata.with(
|
261
|
+
recovery_started_at: current_time,
|
262
|
+
recovery_scheduled_after: nil,
|
263
|
+
recovered_at: nil,
|
264
|
+
breached_at: nil
|
265
|
+
)
|
266
|
+
true
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Transitions to RED state and ensures only one notification
|
272
|
+
#
|
273
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
274
|
+
# @param current_time [Time]
|
275
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
276
|
+
private def transition_to_red(config, current_time: Time.now)
|
277
|
+
light_name = config.name
|
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
|
91
298
|
end
|
92
299
|
end
|
93
300
|
end
|
@@ -0,0 +1,38 @@
|
|
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, 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}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module DataStore
|
5
|
+
class Redis
|
6
|
+
# @api private
|
7
|
+
module Lua
|
8
|
+
class << self
|
9
|
+
def read_lua_file(name_without_extension)
|
10
|
+
File.read(File.join(__dir__, "#{name_without_extension}.lua"))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RECORD_FAILURE = read_lua_file("record_failure")
|
15
|
+
RECORD_SUCCESS = read_lua_file("record_success")
|
16
|
+
GET_METADATA = read_lua_file("get_metadata")
|
17
|
+
TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
|
18
|
+
TRANSITION_TO_RED = read_lua_file("transition_to_red")
|
19
|
+
TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
local failure_ts = tonumber(ARGV[1])
|
2
|
+
local failure_id = ARGV[2]
|
3
|
+
local failure_json = ARGV[3]
|
4
|
+
local bucket_ttl = tonumber(ARGV[4])
|
5
|
+
local metadata_ttl = tonumber(ARGV[5])
|
6
|
+
|
7
|
+
local metadata_key = KEYS[1]
|
8
|
+
local errors_key = KEYS[2]
|
9
|
+
|
10
|
+
-- Record failure
|
11
|
+
if errors_key ~= nil then
|
12
|
+
redis.call('ZADD', errors_key, failure_ts, failure_id)
|
13
|
+
redis.call('EXPIRE', errors_key, bucket_ttl) -- Not supported in Redis 6.2:, 'NX')
|
14
|
+
end
|
15
|
+
|
16
|
+
-- Update metadata
|
17
|
+
local meta = redis.call('HMGET', metadata_key, 'last_error_at', 'consecutive_errors')
|
18
|
+
local prev_failure_ts = tonumber(meta[1])
|
19
|
+
local prev_consecutive_errors = tonumber(meta[2])
|
20
|
+
|
21
|
+
if not prev_failure_ts or failure_ts > prev_failure_ts then
|
22
|
+
redis.call(
|
23
|
+
'HSET', metadata_key,
|
24
|
+
'last_error_at', failure_ts,
|
25
|
+
'last_error_json', failure_json,
|
26
|
+
'consecutive_errors', (prev_consecutive_errors or 0) + 1,
|
27
|
+
'consecutive_successes', 0
|
28
|
+
)
|
29
|
+
else
|
30
|
+
redis.call(
|
31
|
+
'HSET', metadata_key,
|
32
|
+
'consecutive_errors', (prev_consecutive_errors or 0) + 1,
|
33
|
+
'consecutive_successes', 0
|
34
|
+
)
|
35
|
+
end
|
36
|
+
redis.call('EXPIRE', metadata_key, metadata_ttl) -- Not supported in Redis 6.2:, 'GT')
|
@@ -0,0 +1,35 @@
|
|
1
|
+
local request_ts = tonumber(ARGV[1])
|
2
|
+
local request_id = ARGV[2]
|
3
|
+
local bucket_ttl = tonumber(ARGV[3])
|
4
|
+
local metadata_ttl = tonumber(ARGV[4])
|
5
|
+
|
6
|
+
local metadata_key = KEYS[1]
|
7
|
+
local successes_key = KEYS[2]
|
8
|
+
|
9
|
+
-- Record success
|
10
|
+
if successes_key ~= nil then
|
11
|
+
redis.call('ZADD', successes_key, request_ts, request_id)
|
12
|
+
redis.call('EXPIRE', successes_key, bucket_ttl) -- Not supported in Redis 6.2:, 'NX')
|
13
|
+
end
|
14
|
+
|
15
|
+
-- Update metadata
|
16
|
+
local meta = redis.call('HMGET', metadata_key, 'last_success_at', 'consecutive_successes')
|
17
|
+
local prev_success_ts = tonumber(meta[1])
|
18
|
+
local prev_consecutive_successes = tonumber(meta[2])
|
19
|
+
|
20
|
+
if not prev_success_ts or request_ts > prev_success_ts then
|
21
|
+
redis.call(
|
22
|
+
'HSET', metadata_key,
|
23
|
+
'last_success_at', request_ts,
|
24
|
+
'consecutive_errors', 0,
|
25
|
+
'consecutive_successes', (prev_consecutive_successes or 0) + 1
|
26
|
+
)
|
27
|
+
else
|
28
|
+
redis.call(
|
29
|
+
'HSET', metadata_key,
|
30
|
+
'consecutive_errors', 0,
|
31
|
+
'consecutive_successes', (prev_consecutive_successes or 0) + 1
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
redis.call('EXPIRE', metadata_key, metadata_ttl) -- Not supported in Redis 6.2:, 'GT')
|
@@ -0,0 +1,10 @@
|
|
1
|
+
local meta_key = KEYS[1]
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
3
|
+
|
4
|
+
-- 1 if the field is a new field in the hash and the value was set
|
5
|
+
local became_green = redis.call('HSETNX', meta_key, 'recovered_at', current_ts)
|
6
|
+
|
7
|
+
if became_green == 1 then
|
8
|
+
redis.call("HDEL", meta_key, 'recovery_started_at', 'recovery_scheduled_after', 'breached_at')
|
9
|
+
end
|
10
|
+
return became_green
|
@@ -0,0 +1,10 @@
|
|
1
|
+
local meta_key = KEYS[1]
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
3
|
+
local recovery_scheduled_after_ts = tonumber(ARGV[2])
|
4
|
+
|
5
|
+
-- 1 if the field is a new field in the hash and the value was set
|
6
|
+
local became_red = redis.call('HSETNX', meta_key, 'breached_at', current_ts)
|
7
|
+
|
8
|
+
redis.call('HSET', meta_key, 'recovery_scheduled_after', recovery_scheduled_after_ts)
|
9
|
+
redis.call("HDEL", meta_key, "recovery_started_at", "recovered_at")
|
10
|
+
return became_red
|
@@ -0,0 +1,9 @@
|
|
1
|
+
local meta_key = KEYS[1]
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
3
|
+
|
4
|
+
-- HSETNX returns 1 if field is new and was set, 0 if field already exists
|
5
|
+
local became_yellow = redis.call('HSETNX', meta_key, 'recovery_started_at', current_ts)
|
6
|
+
if became_yellow == 1 then
|
7
|
+
redis.call('HDEL', meta_key, 'recovery_scheduled_after', 'breached_at', 'recovered_at')
|
8
|
+
end
|
9
|
+
return became_yellow
|