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,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,411 @@ 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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@
|
26
|
-
@
|
27
|
-
|
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
|
28
31
|
|
29
|
-
|
30
|
-
|
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
|
31
54
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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)
|
36
63
|
end
|
37
64
|
|
38
|
-
|
39
|
-
|
65
|
+
BUCKET_SIZE = 3600 # 1h
|
66
|
+
private_constant :BUCKET_SIZE
|
40
67
|
|
41
|
-
|
42
|
-
|
43
|
-
query_failures(light, transaction: transaction)
|
44
|
-
transaction.hget(states_key, light.name)
|
68
|
+
private def bucket_size
|
69
|
+
BUCKET_SIZE
|
45
70
|
end
|
71
|
+
end
|
46
72
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
81
|
+
@redis = redis
|
82
|
+
@redis.then do |client|
|
83
|
+
@record_failure_sha,
|
84
|
+
@record_success_sha,
|
85
|
+
@get_metadata_sha,
|
86
|
+
@transition_to_yellow_sha,
|
87
|
+
@transition_to_red_sha,
|
88
|
+
@transition_to_green_sha = client.pipelined do |pipeline|
|
89
|
+
pipeline.script("load", Lua::RECORD_FAILURE)
|
90
|
+
pipeline.script("load", Lua::RECORD_SUCCESS)
|
91
|
+
pipeline.script("load", Lua::GET_METADATA)
|
92
|
+
pipeline.script("load", Lua::TRANSITION_TO_YELLOW)
|
93
|
+
pipeline.script("load", Lua::TRANSITION_TO_RED)
|
94
|
+
pipeline.script("load", Lua::TRANSITION_TO_GREEN)
|
95
|
+
end
|
96
|
+
end
|
51
97
|
end
|
52
98
|
|
53
|
-
def
|
54
|
-
|
99
|
+
def names
|
100
|
+
pattern = key("metadata", "*")
|
101
|
+
prefix_regex = /^#{key("metadata", "")}/
|
102
|
+
@redis.then do |client|
|
103
|
+
client.scan_each(match: pattern).to_a.map do |key|
|
104
|
+
key.sub(prefix_regex, "")
|
105
|
+
end
|
106
|
+
end
|
55
107
|
end
|
56
108
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
109
|
+
def get_metadata(config)
|
110
|
+
detect_clock_skew
|
111
|
+
|
112
|
+
window_end = Time.now
|
113
|
+
window_end_ts = window_end.to_i
|
114
|
+
window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
115
|
+
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
61
116
|
|
62
|
-
|
63
|
-
|
64
|
-
|
117
|
+
if config.window_size
|
118
|
+
failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
|
119
|
+
success_keys = success_bucket_keys(config, window_end: window_end_ts)
|
120
|
+
else
|
121
|
+
failure_keys = []
|
122
|
+
success_keys = []
|
123
|
+
end
|
124
|
+
recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
|
125
|
+
recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
|
126
|
+
|
127
|
+
successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
|
128
|
+
client.evalsha(
|
129
|
+
@get_metadata_sha,
|
130
|
+
argv: [
|
131
|
+
failure_keys.count,
|
132
|
+
recovery_probe_failure_keys.count,
|
133
|
+
window_start_ts,
|
134
|
+
window_end_ts,
|
135
|
+
recovery_window_start_ts
|
136
|
+
],
|
137
|
+
keys: [
|
138
|
+
metadata_key(config),
|
139
|
+
*success_keys,
|
140
|
+
*failure_keys,
|
141
|
+
*recovery_probe_success_keys,
|
142
|
+
*recovery_probe_failure_keys
|
143
|
+
]
|
144
|
+
)
|
65
145
|
end
|
146
|
+
meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
|
147
|
+
last_error_json = meta_hash.delete(:last_error_json)
|
148
|
+
last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
|
149
|
+
|
150
|
+
Metadata.new(
|
151
|
+
successes: successes,
|
152
|
+
errors: errors,
|
153
|
+
recovery_probe_successes: recovery_probe_successes,
|
154
|
+
recovery_probe_errors: recovery_probe_errors,
|
155
|
+
last_error:,
|
156
|
+
**meta_hash
|
157
|
+
)
|
158
|
+
end
|
66
159
|
|
67
|
-
|
160
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
161
|
+
# @param failure [Stoplight::Failure] The failure to record.
|
162
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
163
|
+
def record_failure(config, failure)
|
164
|
+
current_ts = failure.time.to_i
|
165
|
+
failure_json = failure.to_json
|
166
|
+
|
167
|
+
@redis.then do |client|
|
168
|
+
client.evalsha(
|
169
|
+
@record_failure_sha,
|
170
|
+
argv: [current_ts, SecureRandom.hex(12), failure_json, metrics_ttl, metadata_ttl],
|
171
|
+
keys: [
|
172
|
+
metadata_key(config),
|
173
|
+
config.window_size && errors_key(config, time: current_ts)
|
174
|
+
].compact
|
175
|
+
)
|
176
|
+
end
|
177
|
+
get_metadata(config)
|
68
178
|
end
|
69
179
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
180
|
+
def record_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
181
|
+
request_ts = request_time.to_i
|
182
|
+
|
183
|
+
@redis.then do |client|
|
184
|
+
client.evalsha(
|
185
|
+
@record_success_sha,
|
186
|
+
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
187
|
+
keys: [
|
188
|
+
metadata_key(config),
|
189
|
+
config.window_size && successes_key(config, time: request_ts)
|
190
|
+
].compact
|
191
|
+
)
|
74
192
|
end
|
193
|
+
end
|
75
194
|
|
76
|
-
|
195
|
+
# Records a failed recovery probe for a specific light configuration.
|
196
|
+
#
|
197
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
198
|
+
# @param failure [Failure] The failure to record.
|
199
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
200
|
+
def record_recovery_probe_failure(config, failure)
|
201
|
+
current_ts = failure.time.to_i
|
202
|
+
failure_json = failure.to_json
|
203
|
+
|
204
|
+
@redis.then do |client|
|
205
|
+
client.evalsha(
|
206
|
+
@record_failure_sha,
|
207
|
+
argv: [current_ts, SecureRandom.uuid, failure_json, metrics_ttl, metrics_ttl],
|
208
|
+
keys: [
|
209
|
+
metadata_key(config),
|
210
|
+
recovery_probe_errors_key(config, time: current_ts)
|
211
|
+
].compact
|
212
|
+
)
|
213
|
+
end
|
214
|
+
get_metadata(config)
|
77
215
|
end
|
78
216
|
|
79
|
-
|
80
|
-
|
217
|
+
# Records a successful recovery probe for a specific light configuration.
|
218
|
+
#
|
219
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
220
|
+
# @param request_id [String] The unique identifier for the request
|
221
|
+
# @param request_time [Time] The time of the request
|
222
|
+
# @return [Stoplight::Metadata] The updated metadata after recording the success.
|
223
|
+
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
224
|
+
request_ts = request_time.to_i
|
225
|
+
|
226
|
+
@redis.then do |client|
|
227
|
+
client.evalsha(
|
228
|
+
@record_success_sha,
|
229
|
+
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
230
|
+
keys: [
|
231
|
+
metadata_key(config),
|
232
|
+
recovery_probe_successes_key(config, time: request_ts)
|
233
|
+
].compact
|
234
|
+
)
|
235
|
+
end
|
236
|
+
get_metadata(config)
|
81
237
|
end
|
82
238
|
|
83
|
-
def set_state(
|
84
|
-
@redis.
|
239
|
+
def set_state(config, state)
|
240
|
+
@redis.then do |client|
|
241
|
+
client.hset(metadata_key(config), "locked_state", state)
|
242
|
+
end
|
85
243
|
state
|
86
244
|
end
|
87
245
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
246
|
+
# Combined method that performs the state transition based on color
|
247
|
+
#
|
248
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
249
|
+
# @param color [String] The color to transition to ("green", "yellow", or "red")
|
250
|
+
# @param current_time [Time] Current timestamp
|
251
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
252
|
+
def transition_to_color(config, color, current_time: Time.now)
|
253
|
+
current_time.to_i
|
254
|
+
|
255
|
+
case color
|
256
|
+
when Color::GREEN
|
257
|
+
transition_to_green(config)
|
258
|
+
when Color::YELLOW
|
259
|
+
transition_to_yellow(config, current_time:)
|
260
|
+
when Color::RED
|
261
|
+
transition_to_red(config, current_time:)
|
262
|
+
else
|
263
|
+
raise ArgumentError, "Invalid color: #{color}"
|
92
264
|
end
|
93
|
-
|
94
|
-
normalize_state(state)
|
95
265
|
end
|
96
266
|
|
97
|
-
|
267
|
+
# Transitions to GREEN state and ensures only one notification
|
268
|
+
#
|
269
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
270
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
271
|
+
private def transition_to_green(config, current_time: Time.now)
|
272
|
+
current_ts = current_time.to_i
|
273
|
+
meta_key = metadata_key(config)
|
274
|
+
|
275
|
+
became_green = @redis.then do |client|
|
276
|
+
client.evalsha(
|
277
|
+
@transition_to_green_sha,
|
278
|
+
argv: [current_ts],
|
279
|
+
keys: [meta_key]
|
280
|
+
)
|
281
|
+
end
|
282
|
+
became_green == 1
|
283
|
+
end
|
98
284
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
285
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
286
|
+
#
|
287
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
288
|
+
# @param current_time [Time] Current timestamp
|
289
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
290
|
+
private def transition_to_yellow(config, current_time: Time.now)
|
291
|
+
current_ts = current_time.to_i
|
292
|
+
meta_key = metadata_key(config)
|
293
|
+
|
294
|
+
became_yellow = @redis.then do |client|
|
295
|
+
client.evalsha(
|
296
|
+
@transition_to_yellow_sha,
|
297
|
+
argv: [current_ts],
|
298
|
+
keys: [meta_key]
|
299
|
+
)
|
300
|
+
end
|
301
|
+
became_yellow == 1
|
302
|
+
end
|
103
303
|
|
104
|
-
|
105
|
-
|
304
|
+
# Transitions to RED state and ensures only one notification
|
305
|
+
#
|
306
|
+
# @param config [Stoplight::Light::Config] The light configuration
|
307
|
+
# @param current_time [Time] Current timestamp
|
308
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
309
|
+
private def transition_to_red(config, current_time: Time.now)
|
310
|
+
current_ts = current_time.to_i
|
311
|
+
meta_key = metadata_key(config)
|
312
|
+
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
313
|
+
|
314
|
+
became_red = @redis.then do |client|
|
315
|
+
client.evalsha(
|
316
|
+
@transition_to_red_sha,
|
317
|
+
argv: [current_ts, recovery_scheduled_after_ts],
|
318
|
+
keys: [meta_key]
|
319
|
+
)
|
106
320
|
end
|
321
|
+
|
322
|
+
became_red == 1
|
107
323
|
end
|
108
324
|
|
109
|
-
private
|
325
|
+
private def normalize_failure(failure, error_notifier)
|
326
|
+
Failure.from_json(failure)
|
327
|
+
rescue => e
|
328
|
+
error_notifier.call(e)
|
329
|
+
Failure.from_error(e)
|
330
|
+
end
|
110
331
|
|
111
|
-
|
112
|
-
# @param time [Time]
|
113
|
-
def remove_outdated_failures(light, time, transaction: @redis)
|
114
|
-
failures_key = failures_key(light)
|
332
|
+
def_delegator "self.class", :key
|
115
333
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
334
|
+
private def failure_bucket_keys(config, window_end:)
|
335
|
+
self.class.buckets_for_window(
|
336
|
+
config.name,
|
337
|
+
metric: "failure",
|
338
|
+
window_end: window_end,
|
339
|
+
window_size: config.window_size
|
340
|
+
)
|
120
341
|
end
|
121
342
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
343
|
+
private def success_bucket_keys(config, window_end:)
|
344
|
+
self.class.buckets_for_window(
|
345
|
+
config.name,
|
346
|
+
metric: "success",
|
347
|
+
window_end: window_end,
|
348
|
+
window_size: config.window_size
|
349
|
+
)
|
126
350
|
end
|
127
351
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
352
|
+
private def recovery_probe_failure_bucket_keys(config, window_end:)
|
353
|
+
self.class.buckets_for_window(
|
354
|
+
config.name,
|
355
|
+
metric: "recovery_probe_failure",
|
356
|
+
window_end: window_end,
|
357
|
+
window_size: config.cool_off_time
|
358
|
+
)
|
134
359
|
end
|
135
360
|
|
136
|
-
def
|
137
|
-
|
361
|
+
private def recovery_probe_success_bucket_keys(config, window_end:)
|
362
|
+
self.class.buckets_for_window(
|
363
|
+
config.name,
|
364
|
+
metric: "recovery_probe_success",
|
365
|
+
window_end: window_end,
|
366
|
+
window_size: config.cool_off_time
|
367
|
+
)
|
368
|
+
end
|
138
369
|
|
139
|
-
|
370
|
+
private def successes_key(config, time:)
|
371
|
+
self.class.bucket_key(config.name, metric: "success", time:)
|
140
372
|
end
|
141
373
|
|
142
|
-
def
|
143
|
-
|
144
|
-
Failure.from_json(json)
|
145
|
-
rescue StandardError => e
|
146
|
-
error_notifier.call(e)
|
147
|
-
Failure.from_error(e)
|
148
|
-
end
|
374
|
+
private def errors_key(config, time:)
|
375
|
+
self.class.bucket_key(config.name, metric: "failure", time:)
|
149
376
|
end
|
150
377
|
|
151
|
-
def
|
152
|
-
|
378
|
+
private def recovery_probe_successes_key(config, time:)
|
379
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
|
153
380
|
end
|
154
381
|
|
155
|
-
def
|
156
|
-
|
382
|
+
private def recovery_probe_errors_key(config, time:)
|
383
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
|
157
384
|
end
|
158
385
|
|
159
|
-
|
160
|
-
|
161
|
-
# @param light [Stoplight::Light]
|
162
|
-
# @return [String]
|
163
|
-
def failures_key(light)
|
164
|
-
key('failures', light.name)
|
386
|
+
private def metadata_key(config)
|
387
|
+
key("metadata", config.name)
|
165
388
|
end
|
166
389
|
|
167
|
-
|
168
|
-
|
390
|
+
METRICS_TTL = 86400 # 1 day
|
391
|
+
private_constant :METRICS_TTL
|
392
|
+
|
393
|
+
private def metrics_ttl
|
394
|
+
METRICS_TTL
|
169
395
|
end
|
170
396
|
|
171
|
-
|
172
|
-
|
397
|
+
METADATA_TTL = 86400 * 7 # 7 days
|
398
|
+
private_constant :METADATA_TTL
|
399
|
+
|
400
|
+
private def metadata_ttl
|
401
|
+
METADATA_TTL
|
173
402
|
end
|
174
403
|
|
175
|
-
|
176
|
-
|
404
|
+
SKEW_TOLERANCE = 5 # seconds
|
405
|
+
private_constant :SKEW_TOLERANCE
|
406
|
+
|
407
|
+
private def detect_clock_skew
|
408
|
+
return unless @warn_on_clock_skew
|
409
|
+
return unless should_sample?(0.01) # 1% chance
|
410
|
+
|
411
|
+
redis_seconds, _redis_millis = @redis.then(&:time)
|
412
|
+
app_seconds = Time.now.to_i
|
413
|
+
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
414
|
+
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")
|
415
|
+
end
|
177
416
|
end
|
178
417
|
|
179
|
-
def
|
180
|
-
|
418
|
+
private def should_sample?(probability)
|
419
|
+
rand <= probability
|
181
420
|
end
|
182
421
|
end
|
183
422
|
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
|
)
|