stoplight 5.4.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/stoplight/admin/actions/remove.rb +23 -0
  4. data/lib/stoplight/admin/dependencies.rb +5 -0
  5. data/lib/stoplight/admin/lights_repository.rb +12 -3
  6. data/lib/stoplight/admin/views/_card.erb +13 -1
  7. data/lib/stoplight/admin/views/layout.erb +3 -3
  8. data/lib/stoplight/admin.rb +12 -4
  9. data/lib/stoplight/domain/color.rb +11 -0
  10. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  11. data/lib/stoplight/domain/config.rb +55 -0
  12. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +55 -17
  13. data/lib/stoplight/domain/error.rb +42 -0
  14. data/lib/stoplight/domain/failure.rb +44 -0
  15. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  16. data/lib/stoplight/domain/light.rb +197 -0
  17. data/lib/stoplight/domain/light_factory.rb +75 -0
  18. data/lib/stoplight/domain/metrics.rb +85 -0
  19. data/lib/stoplight/domain/state.rb +11 -0
  20. data/lib/stoplight/domain/state_snapshot.rb +57 -0
  21. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  22. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  23. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  24. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  25. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +99 -0
  26. data/lib/stoplight/domain/tracker/base.rb +41 -0
  27. data/lib/stoplight/domain/tracker/recovery_probe.rb +75 -0
  28. data/lib/stoplight/domain/tracker/request.rb +68 -0
  29. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  30. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +53 -0
  31. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  32. data/lib/stoplight/domain/traffic_recovery/base.rb +80 -0
  33. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +72 -0
  34. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  35. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  36. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  37. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  38. data/lib/stoplight/infrastructure/data_store/memory.rb +309 -0
  39. data/lib/stoplight/infrastructure/data_store/redis/get_metrics.lua +26 -0
  40. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  41. data/lib/stoplight/infrastructure/data_store/redis.rb +553 -0
  42. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  43. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  44. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  45. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  46. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  47. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  48. data/lib/stoplight/version.rb +1 -1
  49. data/lib/stoplight/wiring/container.rb +80 -0
  50. data/lib/stoplight/wiring/default.rb +28 -0
  51. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  52. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  53. data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +49 -14
  54. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  55. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  56. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  57. data/lib/stoplight/wiring/light_factory.rb +188 -0
  58. data/lib/stoplight/wiring/public_api.rb +28 -0
  59. data/lib/stoplight/wiring/system_container.rb +9 -0
  60. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  61. data/lib/stoplight.rb +38 -28
  62. metadata +57 -43
  63. data/lib/stoplight/color.rb +0 -9
  64. data/lib/stoplight/config/dsl.rb +0 -97
  65. data/lib/stoplight/config/library_default_config.rb +0 -21
  66. data/lib/stoplight/config/system_config.rb +0 -10
  67. data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
  68. data/lib/stoplight/data_store/memory.rb +0 -285
  69. data/lib/stoplight/data_store/redis/get_metadata.lua +0 -38
  70. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  71. data/lib/stoplight/data_store/redis.rb +0 -446
  72. data/lib/stoplight/data_store.rb +0 -6
  73. data/lib/stoplight/default.rb +0 -30
  74. data/lib/stoplight/error.rb +0 -39
  75. data/lib/stoplight/failure.rb +0 -71
  76. data/lib/stoplight/light/config.rb +0 -112
  77. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  78. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  79. data/lib/stoplight/light/red_run_strategy.rb +0 -31
  80. data/lib/stoplight/light/run_strategy.rb +0 -32
  81. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  82. data/lib/stoplight/light.rb +0 -191
  83. data/lib/stoplight/metadata.rb +0 -99
  84. data/lib/stoplight/notifier/generic.rb +0 -79
  85. data/lib/stoplight/notifier/io.rb +0 -21
  86. data/lib/stoplight/notifier/logger.rb +0 -19
  87. data/lib/stoplight/state.rb +0 -9
  88. data/lib/stoplight/traffic_control/base.rb +0 -70
  89. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  90. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  91. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  92. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  93. data/lib/stoplight/traffic_recovery.rb +0 -11
  94. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  95. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  96. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  97. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  98. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -0,0 +1,553 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Stoplight
6
+ module Infrastructure
7
+ module DataStore
8
+ # == Errors
9
+ # All errors are stored in the sorted set where keys are serialized errors and
10
+ # values (Redis uses "score" term) contain integer representations of the time
11
+ # when an error happened.
12
+ #
13
+ # This data structure enables us to query errors that happened within a specific
14
+ # period. We use this feature to support +window_size+ option.
15
+ #
16
+ # To avoid uncontrolled memory consumption, we keep at most +config.threshold+ number
17
+ # of errors happened within last +config.window_size+ seconds (by default infinity).
18
+ #
19
+ # @see Base
20
+ class Redis < Domain::DataStore
21
+ extend Forwardable
22
+
23
+ class << self
24
+ # Generates a Redis key by joining the prefix with the provided pieces.
25
+ #
26
+ # @param pieces [Array<String, Integer>] Parts of the key to be joined.
27
+ # @return [String] The generated Redis key.
28
+ # @api private
29
+ def key(*pieces)
30
+ [KEY_PREFIX, *pieces].join(KEY_SEPARATOR)
31
+ end
32
+
33
+ # Retrieves the list of Redis bucket keys required to cover a specific time window.
34
+ #
35
+ # @param light_name [String] The name of the light (used as part of the Redis key).
36
+ # @param metric [String] The metric type (e.g., "errors").
37
+ # @param window_end [Time, Numeric] The end time of the window (can be a Time object or a numeric timestamp).
38
+ # @param window_size [Numeric] The size of the time window in seconds.
39
+ # @return [Array<String>] A list of Redis keys for the buckets that cover the time window.
40
+ # @api private
41
+ def buckets_for_window(light_name, metric:, window_end:, window_size:)
42
+ window_end_ts = window_end.to_i
43
+ window_start_ts = window_end_ts - [window_size, Domain::DataStore::METRICS_RETENTION_TIME].compact.min.to_i
44
+
45
+ # Find bucket timestamps that contain any part of the window
46
+ start_bucket = (window_start_ts / bucket_size) * bucket_size
47
+
48
+ # End bucket is the last bucket that contains data within our window
49
+ end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size
50
+
51
+ (start_bucket..end_bucket).step(bucket_size).map do |bucket_start|
52
+ bucket_key(light_name, metric: metric, time: bucket_start)
53
+ end
54
+ end
55
+
56
+ # Generates a Redis key for a specific metric and time.
57
+ #
58
+ # @param light_name [String] The name of the light.
59
+ # @param metric [String] The metric type (e.g., "errors").
60
+ # @param time [Time, Numeric] The time for which to generate the key.
61
+ # @return [String] The generated Redis key.
62
+ def bucket_key(light_name, metric:, time:)
63
+ key("metrics", light_name, metric, (time.to_i / bucket_size) * bucket_size)
64
+ end
65
+
66
+ BUCKET_SIZE = 3600 # 1h
67
+ private_constant :BUCKET_SIZE
68
+
69
+ private def bucket_size
70
+ BUCKET_SIZE
71
+ end
72
+ end
73
+
74
+ KEY_SEPARATOR = ":"
75
+ KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
76
+
77
+ # @param redis [::Redis, ConnectionPool<::Redis>]
78
+ # @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
79
+ # the application server
80
+ def initialize(redis, warn_on_clock_skew: true)
81
+ @warn_on_clock_skew = warn_on_clock_skew
82
+ @redis = redis
83
+ end
84
+
85
+ def names
86
+ pattern = key("metadata", "*")
87
+ prefix_regex = /^#{key("metadata", "")}/
88
+ @redis.then do |client|
89
+ client.scan_each(match: pattern).to_a.map do |key|
90
+ key.sub(prefix_regex, "")
91
+ end
92
+ end
93
+ end
94
+
95
+ # @param config [Stoplight::Domain::Config]
96
+ # @return [Stoplight::Domain::Metrics]
97
+ def get_metrics(config)
98
+ config.name
99
+
100
+ window_end_ts = current_time.to_f
101
+ window_start_ts = window_end_ts - config.window_size.to_i
102
+
103
+ if config.window_size
104
+ failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
105
+ success_keys = success_bucket_keys(config, window_end: window_end_ts)
106
+ else
107
+ failure_keys = []
108
+ success_keys = []
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
+
146
+ recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
147
+ recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
148
+
149
+ successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
150
+ client.evalsha(
151
+ get_metrics_sha,
152
+ argv: [
153
+ recovery_probe_failure_keys.count,
154
+ window_start_ts,
155
+ window_end_ts,
156
+ "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
157
+ ],
158
+ keys: [
159
+ metadata_key(config),
160
+ *recovery_probe_success_keys,
161
+ *recovery_probe_failure_keys
162
+ ]
163
+ )
164
+ end
165
+
166
+ Domain::Metrics.new(
167
+ successes:,
168
+ errors:,
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:
219
+ )
220
+ end
221
+
222
+ # @param config [Stoplight::Domain::Config] The light configuration.
223
+ # @param exception [Exception]
224
+ # @return [void]
225
+ def record_failure(config, exception)
226
+ current_time = self.current_time
227
+ current_ts = current_time.to_f
228
+ failure = Domain::Failure.from_error(exception, time: current_time)
229
+
230
+ @redis.then do |client|
231
+ client.evalsha(
232
+ record_failure_sha,
233
+ argv: [current_ts, SecureRandom.hex(12), serialize_failure(failure), metrics_ttl, metadata_ttl],
234
+ keys: [
235
+ metadata_key(config),
236
+ config.window_size && errors_key(config, time: current_ts)
237
+ ].compact
238
+ )
239
+ end
240
+ end
241
+
242
+ def record_success(config, request_id: SecureRandom.hex(12))
243
+ current_ts = current_time.to_f
244
+
245
+ @redis.then do |client|
246
+ client.evalsha(
247
+ record_success_sha,
248
+ argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
249
+ keys: [
250
+ metadata_key(config),
251
+ config.window_size && successes_key(config, time: current_ts)
252
+ ].compact
253
+ )
254
+ end
255
+ end
256
+
257
+ # Records a failed recovery probe for a specific light configuration.
258
+ #
259
+ # @param config [Stoplight::Domain::Config] The light configuration.
260
+ # @param exception [Exception]
261
+ # @return [void]
262
+ def record_recovery_probe_failure(config, exception)
263
+ current_time = self.current_time
264
+ current_ts = current_time.to_f
265
+ failure = Domain::Failure.from_error(exception, time: current_time)
266
+
267
+ @redis.then do |client|
268
+ client.evalsha(
269
+ record_failure_sha,
270
+ argv: [current_ts, SecureRandom.uuid, serialize_failure(failure), metrics_ttl, metrics_ttl],
271
+ keys: [
272
+ metadata_key(config),
273
+ recovery_probe_errors_key(config, time: current_ts)
274
+ ].compact
275
+ )
276
+ end
277
+ end
278
+
279
+ # Records a successful recovery probe for a specific light configuration.
280
+ #
281
+ # @param config [Stoplight::Domain::Config] The light configuration.
282
+ # @param request_id [String] The unique identifier for the request
283
+ # @return [void]
284
+ def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
285
+ current_ts = current_time.to_f
286
+
287
+ @redis.then do |client|
288
+ client.evalsha(
289
+ record_success_sha,
290
+ argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
291
+ keys: [
292
+ metadata_key(config),
293
+ recovery_probe_successes_key(config, time: current_ts)
294
+ ].compact
295
+ )
296
+ end
297
+ end
298
+
299
+ def set_state(config, state)
300
+ @redis.then do |client|
301
+ client.hset(metadata_key(config), "locked_state", state)
302
+ end
303
+ state
304
+ end
305
+
306
+ def inspect
307
+ "#<#{self.class.name} redis=#{@redis.inspect}>"
308
+ end
309
+
310
+ # Combined method that performs the state transition based on color
311
+ #
312
+ # @param config [Stoplight::Domain::Config] The light configuration
313
+ # @param color [String] The color to transition to ("green", "yellow", or "red")
314
+ # @return [Boolean] true if this is the first instance to detect this transition
315
+ def transition_to_color(config, color)
316
+ case color
317
+ when Domain::Color::GREEN
318
+ transition_to_green(config)
319
+ when Domain::Color::YELLOW
320
+ transition_to_yellow(config)
321
+ when Domain::Color::RED
322
+ transition_to_red(config)
323
+ else
324
+ raise ArgumentError, "Invalid color: #{color}"
325
+ end
326
+ end
327
+
328
+ # Transitions to GREEN state and ensures only one notification
329
+ #
330
+ # @param config [Stoplight::Domain::Config] The light configuration
331
+ # @return [Boolean] true if this is the first instance to detect this transition
332
+ private def transition_to_green(config)
333
+ current_ts = current_time.to_f
334
+ meta_key = metadata_key(config)
335
+
336
+ became_green = @redis.then do |client|
337
+ client.evalsha(
338
+ transition_to_green_sha,
339
+ argv: [current_ts],
340
+ keys: [meta_key]
341
+ )
342
+ end
343
+ became_green == 1
344
+ end
345
+
346
+ # Transitions to YELLOW (recovery) state and ensures only one notification
347
+ #
348
+ # @param config [Stoplight::Domain::Config] The light configuration
349
+ # @return [Boolean] true if this is the first instance to detect this transition
350
+ private def transition_to_yellow(config)
351
+ current_ts = current_time.to_f
352
+ meta_key = metadata_key(config)
353
+
354
+ became_yellow = @redis.then do |client|
355
+ client.evalsha(
356
+ transition_to_yellow_sha,
357
+ argv: [current_ts],
358
+ keys: [meta_key]
359
+ )
360
+ end
361
+ became_yellow == 1
362
+ end
363
+
364
+ # Transitions to RED state and ensures only one notification
365
+ #
366
+ # @param config [Stoplight::Domain::Config] The light configuration
367
+ # @return [Boolean] true if this is the first instance to detect this transition
368
+ private def transition_to_red(config)
369
+ current_ts = current_time.to_f
370
+ meta_key = metadata_key(config)
371
+ recovery_scheduled_after_ts = current_ts + config.cool_off_time
372
+
373
+ became_red = @redis.then do |client|
374
+ client.evalsha(
375
+ transition_to_red_sha,
376
+ argv: [current_ts, recovery_scheduled_after_ts],
377
+ keys: [meta_key]
378
+ )
379
+ end
380
+
381
+ became_red == 1
382
+ end
383
+
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]
394
+ private def deserialize_failure(failure_json)
395
+ return if failure_json.nil?
396
+
397
+ object = JSON.parse(failure_json)
398
+ error_object = object["error"]
399
+
400
+ error_class = error_object["class"]
401
+ error_message = error_object["message"]
402
+ time = Time.at(object["time"])
403
+
404
+ Domain::Failure.new(error_class, error_message, time)
405
+ end
406
+
407
+ # @param failure [Domain::Failure]
408
+ # @return [String]
409
+ private def serialize_failure(failure)
410
+ JSON.generate(
411
+ {
412
+ error: {
413
+ class: failure.error_class,
414
+ message: failure.error_message
415
+ },
416
+ time: failure.time.to_f
417
+ }
418
+ )
419
+ end
420
+
421
+ def_delegator "self.class", :key
422
+
423
+ private def failure_bucket_keys(config, window_end:)
424
+ self.class.buckets_for_window(
425
+ config.name,
426
+ metric: "failure",
427
+ window_end: window_end,
428
+ window_size: config.window_size
429
+ )
430
+ end
431
+
432
+ private def success_bucket_keys(config, window_end:)
433
+ self.class.buckets_for_window(
434
+ config.name,
435
+ metric: "success",
436
+ window_end: window_end,
437
+ window_size: config.window_size
438
+ )
439
+ end
440
+
441
+ private def recovery_probe_failure_bucket_keys(config, window_end:)
442
+ self.class.buckets_for_window(
443
+ config.name,
444
+ metric: "recovery_probe_failure",
445
+ window_end: window_end,
446
+ window_size: config.cool_off_time
447
+ )
448
+ end
449
+
450
+ private def recovery_probe_success_bucket_keys(config, window_end:)
451
+ self.class.buckets_for_window(
452
+ config.name,
453
+ metric: "recovery_probe_success",
454
+ window_end: window_end,
455
+ window_size: config.cool_off_time
456
+ )
457
+ end
458
+
459
+ private def successes_key(config, time:)
460
+ self.class.bucket_key(config.name, metric: "success", time:)
461
+ end
462
+
463
+ private def errors_key(config, time:)
464
+ self.class.bucket_key(config.name, metric: "failure", time:)
465
+ end
466
+
467
+ private def recovery_probe_successes_key(config, time:)
468
+ self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
469
+ end
470
+
471
+ private def recovery_probe_errors_key(config, time:)
472
+ self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
473
+ end
474
+
475
+ private def metadata_key(config)
476
+ key("metadata", config.name)
477
+ end
478
+
479
+ METRICS_TTL = 86400 # 1 day
480
+ private_constant :METRICS_TTL
481
+
482
+ private def metrics_ttl
483
+ METRICS_TTL
484
+ end
485
+
486
+ METADATA_TTL = 86400 * 7 # 7 days
487
+ private_constant :METADATA_TTL
488
+
489
+ private def metadata_ttl
490
+ METADATA_TTL
491
+ end
492
+
493
+ SKEW_TOLERANCE = 5 # seconds
494
+ private_constant :SKEW_TOLERANCE
495
+
496
+ private def detect_clock_skew
497
+ return unless @warn_on_clock_skew
498
+ return unless should_sample?(0.01) # 1% chance
499
+
500
+ redis_seconds, _redis_millis = @redis.then(&:time)
501
+ app_seconds = current_time.to_i
502
+ if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
503
+ 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")
504
+ end
505
+ end
506
+
507
+ private def should_sample?(probability)
508
+ rand <= probability
509
+ end
510
+
511
+ private def record_success_sha
512
+ @record_success_sha ||= @redis.then do |client|
513
+ client.script("load", Lua::RECORD_SUCCESS)
514
+ end
515
+ end
516
+
517
+ private def get_metrics_sha
518
+ @get_metrics_sha ||= @redis.then do |client|
519
+ client.script("load", Lua::GET_METRICS)
520
+ end
521
+ end
522
+
523
+ private def transition_to_yellow_sha
524
+ @transition_to_yellow_sha ||= @redis.then do |client|
525
+ client.script("load", Lua::TRANSITION_TO_YELLOW)
526
+ end
527
+ end
528
+
529
+ private def transition_to_red_sha
530
+ @transition_to_red_sha ||= @redis.then do |client|
531
+ client.script("load", Lua::TRANSITION_TO_RED)
532
+ end
533
+ end
534
+
535
+ private def transition_to_green_sha
536
+ @transition_to_green_sha ||= @redis.then do |client|
537
+ client.script("load", Lua::TRANSITION_TO_GREEN)
538
+ end
539
+ end
540
+
541
+ private def record_failure_sha
542
+ @record_failure_sha ||= @redis.then do |client|
543
+ client.script("load", Lua::RECORD_FAILURE)
544
+ end
545
+ end
546
+
547
+ private def current_time
548
+ Time.now
549
+ end
550
+ end
551
+ end
552
+ end
553
+ end