stoplight 4.1.1 → 5.0.2
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 +68 -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 +106 -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 +363 -103
- 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 -101
- 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,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "forwardable"
|
4
4
|
|
5
5
|
module Stoplight
|
6
6
|
module DataStore
|
@@ -12,172 +12,432 @@ module Stoplight
|
|
12
12
|
# This data structure enables us to query errors that happened within a specific
|
13
13
|
# period. We use this feature to support +window_size+ option.
|
14
14
|
#
|
15
|
-
# To avoid uncontrolled memory consumption, we keep at most +
|
16
|
-
# of errors happened within last +
|
15
|
+
# To avoid uncontrolled memory consumption, we keep at most +config.threshold+ number
|
16
|
+
# of errors happened within last +config.window_size+ seconds (by default infinity).
|
17
17
|
#
|
18
18
|
# @see Base
|
19
19
|
class Redis < Base
|
20
|
-
|
21
|
-
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Generates a Redis key by joining the prefix with the provided pieces.
|
24
|
+
#
|
25
|
+
# @param pieces [Array<String, Integer>] Parts of the key to be joined.
|
26
|
+
# @return [String] The generated Redis key.
|
27
|
+
# @api private
|
28
|
+
def key(*pieces)
|
29
|
+
[KEY_PREFIX, *pieces].join(KEY_SEPARATOR)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieves the list of Redis bucket keys required to cover a specific time window.
|
33
|
+
#
|
34
|
+
# @param light_name [String] The name of the light (used as part of the Redis key).
|
35
|
+
# @param metric [String] The metric type (e.g., "errors").
|
36
|
+
# @param window_end [Time, Numeric] The end time of the window (can be a Time object or a numeric timestamp).
|
37
|
+
# @param window_size [Numeric] The size of the time window in seconds.
|
38
|
+
# @return [Array<String>] A list of Redis keys for the buckets that cover the time window.
|
39
|
+
# @api private
|
40
|
+
def buckets_for_window(light_name, metric:, window_end:, window_size:)
|
41
|
+
window_end_ts = window_end.to_i
|
42
|
+
window_start_ts = window_end_ts - [window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
43
|
+
|
44
|
+
# Find bucket timestamps that contain any part of the window
|
45
|
+
start_bucket = (window_start_ts / bucket_size) * bucket_size
|
46
|
+
|
47
|
+
# End bucket is the last bucket that contains data within our window
|
48
|
+
end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size
|
49
|
+
|
50
|
+
(start_bucket..end_bucket).step(bucket_size).map do |bucket_start|
|
51
|
+
bucket_key(light_name, metric: metric, time: bucket_start)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Generates a Redis key for a specific metric and time.
|
56
|
+
#
|
57
|
+
# @param light_name [String] The name of the light.
|
58
|
+
# @param metric [String] The metric type (e.g., "errors").
|
59
|
+
# @param time [Time, Numeric] The time for which to generate the key.
|
60
|
+
# @return [String] The generated Redis key.
|
61
|
+
def bucket_key(light_name, metric:, time:)
|
62
|
+
key("metrics", light_name, metric, (time.to_i / bucket_size) * bucket_size)
|
63
|
+
end
|
64
|
+
|
65
|
+
BUCKET_SIZE = 3600 # 1h
|
66
|
+
private_constant :BUCKET_SIZE
|
22
67
|
|
23
|
-
|
24
|
-
|
68
|
+
private def bucket_size
|
69
|
+
BUCKET_SIZE
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
KEY_SEPARATOR = ":"
|
74
|
+
KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
|
75
|
+
|
76
|
+
# @param redis [::Redis, ConnectionPool<::Redis>]
|
77
|
+
# @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
|
78
|
+
# the application server
|
79
|
+
def initialize(redis, warn_on_clock_skew: true)
|
80
|
+
@warn_on_clock_skew = warn_on_clock_skew
|
25
81
|
@redis = redis
|
26
|
-
@redlock = redlock
|
27
82
|
end
|
28
83
|
|
29
84
|
def names
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
85
|
+
pattern = key("metadata", "*")
|
86
|
+
prefix_regex = /^#{key("metadata", "")}/
|
87
|
+
@redis.then do |client|
|
88
|
+
client.scan_each(match: pattern).to_a.map do |key|
|
89
|
+
key.sub(prefix_regex, "")
|
90
|
+
end
|
36
91
|
end
|
92
|
+
end
|
37
93
|
|
38
|
-
|
94
|
+
def get_metadata(config)
|
95
|
+
detect_clock_skew
|
96
|
+
|
97
|
+
window_end = Time.now
|
98
|
+
window_end_ts = window_end.to_i
|
99
|
+
window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
100
|
+
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
101
|
+
|
102
|
+
if config.window_size
|
103
|
+
failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
|
104
|
+
success_keys = success_bucket_keys(config, window_end: window_end_ts)
|
105
|
+
else
|
106
|
+
failure_keys = []
|
107
|
+
success_keys = []
|
108
|
+
end
|
109
|
+
recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
|
110
|
+
recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
|
111
|
+
|
112
|
+
successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
|
113
|
+
client.evalsha(
|
114
|
+
get_metadata_sha,
|
115
|
+
argv: [
|
116
|
+
failure_keys.count,
|
117
|
+
recovery_probe_failure_keys.count,
|
118
|
+
window_start_ts,
|
119
|
+
window_end_ts,
|
120
|
+
recovery_window_start_ts
|
121
|
+
],
|
122
|
+
keys: [
|
123
|
+
metadata_key(config),
|
124
|
+
*success_keys,
|
125
|
+
*failure_keys,
|
126
|
+
*recovery_probe_success_keys,
|
127
|
+
*recovery_probe_failure_keys
|
128
|
+
]
|
129
|
+
)
|
130
|
+
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 = normalize_failure(last_error_json, config.error_notifier) if last_error_json
|
134
|
+
|
135
|
+
Metadata.new(
|
136
|
+
successes: successes,
|
137
|
+
errors: errors,
|
138
|
+
recovery_probe_successes: recovery_probe_successes,
|
139
|
+
recovery_probe_errors: recovery_probe_errors,
|
140
|
+
last_error:,
|
141
|
+
**meta_hash
|
142
|
+
)
|
39
143
|
end
|
40
144
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
145
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
146
|
+
# @param failure [Stoplight::Failure] The failure to record.
|
147
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
148
|
+
def record_failure(config, failure)
|
149
|
+
current_ts = failure.time.to_i
|
150
|
+
failure_json = failure.to_json
|
151
|
+
|
152
|
+
@redis.then do |client|
|
153
|
+
client.evalsha(
|
154
|
+
record_failure_sha,
|
155
|
+
argv: [current_ts, SecureRandom.hex(12), failure_json, metrics_ttl, metadata_ttl],
|
156
|
+
keys: [
|
157
|
+
metadata_key(config),
|
158
|
+
config.window_size && errors_key(config, time: current_ts)
|
159
|
+
].compact
|
160
|
+
)
|
45
161
|
end
|
162
|
+
get_metadata(config)
|
163
|
+
end
|
46
164
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
165
|
+
def record_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
166
|
+
request_ts = request_time.to_i
|
167
|
+
|
168
|
+
@redis.then do |client|
|
169
|
+
client.evalsha(
|
170
|
+
record_success_sha,
|
171
|
+
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
172
|
+
keys: [
|
173
|
+
metadata_key(config),
|
174
|
+
config.window_size && successes_key(config, time: request_ts)
|
175
|
+
].compact
|
176
|
+
)
|
177
|
+
end
|
51
178
|
end
|
52
179
|
|
53
|
-
|
54
|
-
|
180
|
+
# Records a failed recovery probe for a specific light configuration.
|
181
|
+
#
|
182
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
183
|
+
# @param failure [Failure] The failure to record.
|
184
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
185
|
+
def record_recovery_probe_failure(config, failure)
|
186
|
+
current_ts = failure.time.to_i
|
187
|
+
failure_json = failure.to_json
|
188
|
+
|
189
|
+
@redis.then do |client|
|
190
|
+
client.evalsha(
|
191
|
+
record_failure_sha,
|
192
|
+
argv: [current_ts, SecureRandom.uuid, failure_json, metrics_ttl, metrics_ttl],
|
193
|
+
keys: [
|
194
|
+
metadata_key(config),
|
195
|
+
recovery_probe_errors_key(config, time: current_ts)
|
196
|
+
].compact
|
197
|
+
)
|
198
|
+
end
|
199
|
+
get_metadata(config)
|
55
200
|
end
|
56
201
|
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
202
|
+
# Records a successful recovery probe for a specific light configuration.
|
203
|
+
#
|
204
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
205
|
+
# @param request_id [String] The unique identifier for the request
|
206
|
+
# @param request_time [Time] The time of the request
|
207
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the success.
|
208
|
+
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
209
|
+
request_ts = request_time.to_i
|
210
|
+
|
211
|
+
@redis.then do |client|
|
212
|
+
client.evalsha(
|
213
|
+
record_success_sha,
|
214
|
+
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
215
|
+
keys: [
|
216
|
+
metadata_key(config),
|
217
|
+
recovery_probe_successes_key(config, time: request_ts)
|
218
|
+
].compact
|
219
|
+
)
|
220
|
+
end
|
221
|
+
get_metadata(config)
|
222
|
+
end
|
61
223
|
|
62
|
-
|
63
|
-
|
64
|
-
|
224
|
+
def set_state(config, state)
|
225
|
+
@redis.then do |client|
|
226
|
+
client.hset(metadata_key(config), "locked_state", state)
|
65
227
|
end
|
228
|
+
state
|
229
|
+
end
|
66
230
|
|
67
|
-
|
231
|
+
# Combined method that performs the state transition based on color
|
232
|
+
#
|
233
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
234
|
+
# @param color [String] The color to transition to ("green", "yellow", or "red")
|
235
|
+
# @param current_time [Time] Current timestamp
|
236
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
237
|
+
def transition_to_color(config, color, current_time: Time.now)
|
238
|
+
current_time.to_i
|
239
|
+
|
240
|
+
case color
|
241
|
+
when Color::GREEN
|
242
|
+
transition_to_green(config)
|
243
|
+
when Color::YELLOW
|
244
|
+
transition_to_yellow(config, current_time:)
|
245
|
+
when Color::RED
|
246
|
+
transition_to_red(config, current_time:)
|
247
|
+
else
|
248
|
+
raise ArgumentError, "Invalid color: #{color}"
|
249
|
+
end
|
68
250
|
end
|
69
251
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
252
|
+
# Transitions to GREEN state and ensures only one notification
|
253
|
+
#
|
254
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
255
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
256
|
+
private def transition_to_green(config, current_time: Time.now)
|
257
|
+
current_ts = current_time.to_i
|
258
|
+
meta_key = metadata_key(config)
|
259
|
+
|
260
|
+
became_green = @redis.then do |client|
|
261
|
+
client.evalsha(
|
262
|
+
transition_to_green_sha,
|
263
|
+
argv: [current_ts],
|
264
|
+
keys: [meta_key]
|
265
|
+
)
|
74
266
|
end
|
267
|
+
became_green == 1
|
268
|
+
end
|
75
269
|
|
76
|
-
|
270
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
271
|
+
#
|
272
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
273
|
+
# @param current_time [Time] Current timestamp
|
274
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
275
|
+
private def transition_to_yellow(config, current_time: Time.now)
|
276
|
+
current_ts = current_time.to_i
|
277
|
+
meta_key = metadata_key(config)
|
278
|
+
|
279
|
+
became_yellow = @redis.then do |client|
|
280
|
+
client.evalsha(
|
281
|
+
transition_to_yellow_sha,
|
282
|
+
argv: [current_ts],
|
283
|
+
keys: [meta_key]
|
284
|
+
)
|
285
|
+
end
|
286
|
+
became_yellow == 1
|
77
287
|
end
|
78
288
|
|
79
|
-
|
80
|
-
|
289
|
+
# Transitions to RED state and ensures only one notification
|
290
|
+
#
|
291
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
292
|
+
# @param current_time [Time] Current timestamp
|
293
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
294
|
+
private def transition_to_red(config, current_time: Time.now)
|
295
|
+
current_ts = current_time.to_i
|
296
|
+
meta_key = metadata_key(config)
|
297
|
+
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
298
|
+
|
299
|
+
became_red = @redis.then do |client|
|
300
|
+
client.evalsha(
|
301
|
+
transition_to_red_sha,
|
302
|
+
argv: [current_ts, recovery_scheduled_after_ts],
|
303
|
+
keys: [meta_key]
|
304
|
+
)
|
305
|
+
end
|
306
|
+
|
307
|
+
became_red == 1
|
81
308
|
end
|
82
309
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
310
|
+
private def normalize_failure(failure, error_notifier)
|
311
|
+
Failure.from_json(failure)
|
312
|
+
rescue => e
|
313
|
+
error_notifier.call(e)
|
314
|
+
Failure.from_error(e)
|
86
315
|
end
|
87
316
|
|
88
|
-
|
89
|
-
state, = @redis.multi do |transaction|
|
90
|
-
query_state(light, transaction: transaction)
|
91
|
-
transaction.hdel(states_key, light.name)
|
92
|
-
end
|
317
|
+
def_delegator "self.class", :key
|
93
318
|
|
94
|
-
|
319
|
+
private def failure_bucket_keys(config, window_end:)
|
320
|
+
self.class.buckets_for_window(
|
321
|
+
config.name,
|
322
|
+
metric: "failure",
|
323
|
+
window_end: window_end,
|
324
|
+
window_size: config.window_size
|
325
|
+
)
|
95
326
|
end
|
96
327
|
|
97
|
-
|
328
|
+
private def success_bucket_keys(config, window_end:)
|
329
|
+
self.class.buckets_for_window(
|
330
|
+
config.name,
|
331
|
+
metric: "success",
|
332
|
+
window_end: window_end,
|
333
|
+
window_size: config.window_size
|
334
|
+
)
|
335
|
+
end
|
98
336
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
337
|
+
private def recovery_probe_failure_bucket_keys(config, window_end:)
|
338
|
+
self.class.buckets_for_window(
|
339
|
+
config.name,
|
340
|
+
metric: "recovery_probe_failure",
|
341
|
+
window_end: window_end,
|
342
|
+
window_size: config.cool_off_time
|
343
|
+
)
|
344
|
+
end
|
103
345
|
|
104
|
-
|
105
|
-
|
106
|
-
|
346
|
+
private def recovery_probe_success_bucket_keys(config, window_end:)
|
347
|
+
self.class.buckets_for_window(
|
348
|
+
config.name,
|
349
|
+
metric: "recovery_probe_success",
|
350
|
+
window_end: window_end,
|
351
|
+
window_size: config.cool_off_time
|
352
|
+
)
|
107
353
|
end
|
108
354
|
|
109
|
-
private
|
355
|
+
private def successes_key(config, time:)
|
356
|
+
self.class.bucket_key(config.name, metric: "success", time:)
|
357
|
+
end
|
110
358
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
failures_key = failures_key(light)
|
359
|
+
private def errors_key(config, time:)
|
360
|
+
self.class.bucket_key(config.name, metric: "failure", time:)
|
361
|
+
end
|
115
362
|
|
116
|
-
|
117
|
-
|
118
|
-
# Keep at most +light.threshold+ number of errors
|
119
|
-
transaction.zremrangebyrank(failures_key, 0, -light.threshold - 1)
|
363
|
+
private def recovery_probe_successes_key(config, time:)
|
364
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
|
120
365
|
end
|
121
366
|
|
122
|
-
|
123
|
-
|
124
|
-
def last_notification(light)
|
125
|
-
@redis.get(last_notification_key(light))&.split('->')
|
367
|
+
private def recovery_probe_errors_key(config, time:)
|
368
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
|
126
369
|
end
|
127
370
|
|
128
|
-
|
129
|
-
|
130
|
-
# @param to_color [String]
|
131
|
-
# @return [void]
|
132
|
-
def set_last_notification(light, from_color, to_color)
|
133
|
-
@redis.set(last_notification_key(light), [from_color, to_color].join('->'))
|
371
|
+
private def metadata_key(config)
|
372
|
+
key("metadata", config.name)
|
134
373
|
end
|
135
374
|
|
136
|
-
|
137
|
-
|
375
|
+
METRICS_TTL = 86400 # 1 day
|
376
|
+
private_constant :METRICS_TTL
|
138
377
|
|
139
|
-
|
378
|
+
private def metrics_ttl
|
379
|
+
METRICS_TTL
|
140
380
|
end
|
141
381
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
382
|
+
METADATA_TTL = 86400 * 7 # 7 days
|
383
|
+
private_constant :METADATA_TTL
|
384
|
+
|
385
|
+
private def metadata_ttl
|
386
|
+
METADATA_TTL
|
387
|
+
end
|
388
|
+
|
389
|
+
SKEW_TOLERANCE = 5 # seconds
|
390
|
+
private_constant :SKEW_TOLERANCE
|
391
|
+
|
392
|
+
private def detect_clock_skew
|
393
|
+
return unless @warn_on_clock_skew
|
394
|
+
return unless should_sample?(0.01) # 1% chance
|
395
|
+
|
396
|
+
redis_seconds, _redis_millis = @redis.then(&:time)
|
397
|
+
app_seconds = Time.now.to_i
|
398
|
+
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
399
|
+
warn("Detected clock skew between Redis and the application server. Redis time: #{redis_seconds}, Application time: #{app_seconds}. See https://github.com/bolshakov/stoplight/wiki/Clock-Skew-and-Stoplight-Reliability")
|
148
400
|
end
|
149
401
|
end
|
150
402
|
|
151
|
-
def
|
152
|
-
|
403
|
+
private def should_sample?(probability)
|
404
|
+
rand <= probability
|
153
405
|
end
|
154
406
|
|
155
|
-
def
|
156
|
-
|
407
|
+
private def record_success_sha
|
408
|
+
@record_success_sha ||= @redis.then do |client|
|
409
|
+
client.script("load", Lua::RECORD_SUCCESS)
|
410
|
+
end
|
157
411
|
end
|
158
412
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
def failures_key(light)
|
164
|
-
key('failures', light.name)
|
413
|
+
private def get_metadata_sha
|
414
|
+
@get_metadata_sha ||= @redis.then do |client|
|
415
|
+
client.script("load", Lua::GET_METADATA)
|
416
|
+
end
|
165
417
|
end
|
166
418
|
|
167
|
-
def
|
168
|
-
|
419
|
+
private def transition_to_yellow_sha
|
420
|
+
@transition_to_yellow_sha ||= @redis.then do |client|
|
421
|
+
client.script("load", Lua::TRANSITION_TO_YELLOW)
|
422
|
+
end
|
169
423
|
end
|
170
424
|
|
171
|
-
def
|
172
|
-
|
425
|
+
private def transition_to_red_sha
|
426
|
+
@transition_to_red_sha ||= @redis.then do |client|
|
427
|
+
client.script("load", Lua::TRANSITION_TO_RED)
|
428
|
+
end
|
173
429
|
end
|
174
430
|
|
175
|
-
def
|
176
|
-
|
431
|
+
private def transition_to_green_sha
|
432
|
+
@transition_to_green_sha ||= @redis.then do |client|
|
433
|
+
client.script("load", Lua::TRANSITION_TO_GREEN)
|
434
|
+
end
|
177
435
|
end
|
178
436
|
|
179
|
-
def
|
180
|
-
|
437
|
+
private def record_failure_sha
|
438
|
+
@record_failure_sha ||= @redis.then do |client|
|
439
|
+
client.script("load", Lua::RECORD_FAILURE)
|
440
|
+
end
|
181
441
|
end
|
182
442
|
end
|
183
443
|
end
|
data/lib/stoplight/default.rb
CHANGED
@@ -6,24 +6,26 @@ module Stoplight
|
|
6
6
|
|
7
7
|
DATA_STORE = DataStore::Memory.new
|
8
8
|
|
9
|
-
ERROR_HANDLER = ->(error, handler) { handler.call(error) }
|
10
|
-
|
11
9
|
ERROR_NOTIFIER = ->(error) { warn error }
|
12
10
|
|
13
|
-
FALLBACK = nil
|
14
|
-
|
15
11
|
FORMATTER = lambda do |light, from_color, to_color, error|
|
16
|
-
words = [
|
17
|
-
words += [
|
18
|
-
words.join(
|
12
|
+
words = ["Switching", light.name, "from", from_color, "to", to_color]
|
13
|
+
words += ["because", error.class, error.message] if error
|
14
|
+
words.join(" ")
|
19
15
|
end
|
20
16
|
|
21
17
|
NOTIFIERS = [
|
22
|
-
Notifier::IO.new($stderr)
|
18
|
+
Notifier::FailSafe.wrap(Notifier::IO.new($stderr))
|
23
19
|
].freeze
|
24
20
|
|
25
21
|
THRESHOLD = 3
|
26
22
|
|
27
|
-
WINDOW_SIZE =
|
23
|
+
WINDOW_SIZE = nil
|
24
|
+
|
25
|
+
TRACKED_ERRORS = [StandardError].freeze
|
26
|
+
SKIPPED_ERRORS = [].freeze
|
27
|
+
|
28
|
+
TRAFFIC_CONTROL = TrafficControl::ConsecutiveFailures.new
|
29
|
+
TRAFFIC_RECOVERY = TrafficRecovery::SingleSuccess.new
|
28
30
|
end
|
29
31
|
end
|
data/lib/stoplight/error.rb
CHANGED
@@ -2,20 +2,8 @@
|
|
2
2
|
|
3
3
|
module Stoplight
|
4
4
|
module Error
|
5
|
-
HANDLER = lambda do |error|
|
6
|
-
raise error if AVOID_RESCUING.any? { |klass| error.is_a?(klass) }
|
7
|
-
end
|
8
|
-
|
9
|
-
AVOID_RESCUING = [
|
10
|
-
NoMemoryError,
|
11
|
-
ScriptError,
|
12
|
-
SecurityError,
|
13
|
-
SignalException,
|
14
|
-
SystemExit,
|
15
|
-
SystemStackError
|
16
|
-
].freeze
|
17
|
-
|
18
5
|
Base = Class.new(StandardError)
|
6
|
+
ConfigurationError = Class.new(Base)
|
19
7
|
IncorrectColor = Class.new(Base)
|
20
8
|
RedLight = Class.new(Base)
|
21
9
|
end
|
data/lib/stoplight/failure.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "json"
|
4
|
+
require "time"
|
5
5
|
|
6
6
|
module Stoplight
|
7
7
|
class Failure # rubocop:disable Style/Documentation
|
8
|
-
TIME_FORMAT =
|
8
|
+
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%N%:z"
|
9
9
|
|
10
10
|
# @return [String]
|
11
11
|
attr_reader :error_class
|
@@ -16,8 +16,8 @@ module Stoplight
|
|
16
16
|
|
17
17
|
# @param error [Exception]
|
18
18
|
# @return (see #initialize)
|
19
|
-
def self.from_error(error)
|
20
|
-
new(error.class.name, error.message,
|
19
|
+
def self.from_error(error, time: Time.now)
|
20
|
+
new(error.class.name, error.message, time)
|
21
21
|
end
|
22
22
|
|
23
23
|
# @param json [String]
|
@@ -26,11 +26,11 @@ module Stoplight
|
|
26
26
|
# @raise [ArgumentError]
|
27
27
|
def self.from_json(json)
|
28
28
|
object = JSON.parse(json)
|
29
|
-
error_object = object[
|
29
|
+
error_object = object["error"]
|
30
30
|
|
31
|
-
error_class = error_object[
|
32
|
-
error_message = error_object[
|
33
|
-
time = Time.
|
31
|
+
error_class = error_object["class"]
|
32
|
+
error_message = error_object["message"]
|
33
|
+
time = Time.at(object["time"])
|
34
34
|
|
35
35
|
new(error_class, error_message, time)
|
36
36
|
end
|
@@ -41,15 +41,16 @@ module Stoplight
|
|
41
41
|
def initialize(error_class, error_message, time)
|
42
42
|
@error_class = error_class
|
43
43
|
@error_message = error_message
|
44
|
-
@time = time
|
44
|
+
@time = Time.at(time.to_i) # truncate to seconds
|
45
45
|
end
|
46
46
|
|
47
47
|
# @param other [Failure]
|
48
48
|
# @return [Boolean]
|
49
49
|
def ==(other)
|
50
|
-
|
50
|
+
other.is_a?(self.class) &&
|
51
|
+
error_class == other.error_class &&
|
51
52
|
error_message == other.error_message &&
|
52
|
-
time == other.time
|
53
|
+
time.to_i == other.time.to_i
|
53
54
|
end
|
54
55
|
|
55
56
|
# @param options [Object, nil]
|
@@ -61,7 +62,7 @@ module Stoplight
|
|
61
62
|
class: error_class,
|
62
63
|
message: error_message
|
63
64
|
},
|
64
|
-
time: time.
|
65
|
+
time: time.to_i
|
65
66
|
},
|
66
67
|
options
|
67
68
|
)
|