stoplight 5.3.8 → 5.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/views/layout.erb +3 -3
  5. data/lib/stoplight/admin.rb +4 -4
  6. data/lib/stoplight/domain/color.rb +11 -0
  7. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  8. data/lib/stoplight/domain/config.rb +55 -0
  9. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  10. data/lib/stoplight/domain/error.rb +42 -0
  11. data/lib/stoplight/domain/failure.rb +42 -0
  12. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  13. data/lib/stoplight/domain/light.rb +198 -0
  14. data/lib/stoplight/domain/light_factory.rb +75 -0
  15. data/lib/stoplight/domain/metadata.rb +65 -0
  16. data/lib/stoplight/domain/state.rb +11 -0
  17. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  18. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  19. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  20. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  21. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  22. data/lib/stoplight/domain/tracker/base.rb +41 -0
  23. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  24. data/lib/stoplight/domain/tracker/request.rb +67 -0
  25. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  26. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  27. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  28. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  29. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  30. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  31. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  32. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  33. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  34. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  36. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  37. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  38. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  39. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  40. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  41. data/lib/stoplight/version.rb +1 -1
  42. data/lib/stoplight/wiring/container.rb +80 -0
  43. data/lib/stoplight/wiring/default.rb +28 -0
  44. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  45. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  46. data/lib/stoplight/wiring/fail_safe_data_store.rb +123 -0
  47. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  48. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  49. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  50. data/lib/stoplight/wiring/light_factory.rb +188 -0
  51. data/lib/stoplight/wiring/public_api.rb +28 -0
  52. data/lib/stoplight/wiring/system_container.rb +9 -0
  53. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  54. data/lib/stoplight.rb +38 -28
  55. metadata +53 -42
  56. data/lib/stoplight/color.rb +0 -9
  57. data/lib/stoplight/config/dsl.rb +0 -97
  58. data/lib/stoplight/config/library_default_config.rb +0 -21
  59. data/lib/stoplight/config/system_config.rb +0 -7
  60. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  61. data/lib/stoplight/data_store/memory.rb +0 -311
  62. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  63. data/lib/stoplight/data_store/redis.rb +0 -449
  64. data/lib/stoplight/data_store.rb +0 -6
  65. data/lib/stoplight/default.rb +0 -30
  66. data/lib/stoplight/error.rb +0 -10
  67. data/lib/stoplight/failure.rb +0 -71
  68. data/lib/stoplight/light/config.rb +0 -111
  69. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  70. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  71. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  72. data/lib/stoplight/light/run_strategy.rb +0 -32
  73. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  74. data/lib/stoplight/light.rb +0 -191
  75. data/lib/stoplight/metadata.rb +0 -99
  76. data/lib/stoplight/notifier/generic.rb +0 -79
  77. data/lib/stoplight/notifier/io.rb +0 -21
  78. data/lib/stoplight/notifier/logger.rb +0 -19
  79. data/lib/stoplight/state.rb +0 -9
  80. data/lib/stoplight/traffic_control/base.rb +0 -70
  81. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  82. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  83. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  84. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  85. data/lib/stoplight/traffic_recovery.rb +0 -11
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  90. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  91. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -0,0 +1,478 @@
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
+ def get_metadata(config)
96
+ detect_clock_skew
97
+
98
+ window_end_ts = current_time.to_f
99
+ window_start_ts = window_end_ts - [config.window_size, METRICS_RETENTION_TIME].compact.min.to_f
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 = deserialize_failure(last_error_json) if last_error_json
134
+
135
+ Domain::Metadata.new(
136
+ current_time:,
137
+ successes:,
138
+ errors:,
139
+ recovery_probe_successes:,
140
+ recovery_probe_errors:,
141
+ last_error:,
142
+ last_error_at: (Time.at(meta_hash[:last_error_at].to_f) if meta_hash[:last_error_at]),
143
+ last_success_at: (Time.at(meta_hash[:last_success_at].to_f) if meta_hash[:last_success_at]),
144
+ consecutive_errors: meta_hash[:consecutive_errors].to_i,
145
+ consecutive_successes: meta_hash[:consecutive_successes].to_i,
146
+ breached_at: (Time.at(meta_hash[:breached_at].to_f) if meta_hash[:breached_at]),
147
+ locked_state: meta_hash[:locked_state] || Domain::State::UNLOCKED,
148
+ recovery_scheduled_after: (Time.at(meta_hash[:recovery_scheduled_after].to_f) if meta_hash[:recovery_scheduled_after]),
149
+ recovery_started_at: (Time.at(meta_hash[:recovery_started_at].to_f) if meta_hash[:recovery_started_at]),
150
+ recovered_at: (Time.at(meta_hash[:recovered_at].to_f) if meta_hash[:recovered_at])
151
+ )
152
+ end
153
+
154
+ # @param config [Stoplight::Domain::Config] The light configuration.
155
+ # @param exception [Exception]
156
+ # @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
157
+ def record_failure(config, exception)
158
+ current_time = self.current_time
159
+ current_ts = current_time.to_f
160
+ failure = Domain::Failure.from_error(exception, time: current_time)
161
+
162
+ @redis.then do |client|
163
+ client.evalsha(
164
+ record_failure_sha,
165
+ argv: [current_ts, SecureRandom.hex(12), serialize_failure(failure), metrics_ttl, metadata_ttl],
166
+ keys: [
167
+ metadata_key(config),
168
+ config.window_size && errors_key(config, time: current_ts)
169
+ ].compact
170
+ )
171
+ end
172
+ get_metadata(config)
173
+ end
174
+
175
+ def record_success(config, request_id: SecureRandom.hex(12))
176
+ current_ts = current_time.to_f
177
+
178
+ @redis.then do |client|
179
+ client.evalsha(
180
+ record_success_sha,
181
+ argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
182
+ keys: [
183
+ metadata_key(config),
184
+ config.window_size && successes_key(config, time: current_ts)
185
+ ].compact
186
+ )
187
+ end
188
+ end
189
+
190
+ # Records a failed recovery probe for a specific light configuration.
191
+ #
192
+ # @param config [Stoplight::Domain::Config] The light configuration.
193
+ # @param exception [Exception]
194
+ # @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
195
+ def record_recovery_probe_failure(config, exception)
196
+ current_time = self.current_time
197
+ current_ts = current_time.to_f
198
+ failure = Domain::Failure.from_error(exception, time: current_time)
199
+
200
+ @redis.then do |client|
201
+ client.evalsha(
202
+ record_failure_sha,
203
+ argv: [current_ts, SecureRandom.uuid, serialize_failure(failure), metrics_ttl, metrics_ttl],
204
+ keys: [
205
+ metadata_key(config),
206
+ recovery_probe_errors_key(config, time: current_ts)
207
+ ].compact
208
+ )
209
+ end
210
+ get_metadata(config)
211
+ end
212
+
213
+ # Records a successful recovery probe for a specific light configuration.
214
+ #
215
+ # @param config [Stoplight::Domain::Config] The light configuration.
216
+ # @param request_id [String] The unique identifier for the request
217
+ # @return [Stoplight::Domain::Metadata] The updated metadata after recording the success.
218
+ def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
219
+ current_ts = current_time.to_f
220
+
221
+ @redis.then do |client|
222
+ client.evalsha(
223
+ record_success_sha,
224
+ argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
225
+ keys: [
226
+ metadata_key(config),
227
+ recovery_probe_successes_key(config, time: current_ts)
228
+ ].compact
229
+ )
230
+ end
231
+ get_metadata(config)
232
+ end
233
+
234
+ def set_state(config, state)
235
+ @redis.then do |client|
236
+ client.hset(metadata_key(config), "locked_state", state)
237
+ end
238
+ state
239
+ end
240
+
241
+ def inspect
242
+ "#<#{self.class.name} redis=#{@redis.inspect}>"
243
+ end
244
+
245
+ # Combined method that performs the state transition based on color
246
+ #
247
+ # @param config [Stoplight::Domain::Config] The light configuration
248
+ # @param color [String] The color to transition to ("green", "yellow", or "red")
249
+ # @return [Boolean] true if this is the first instance to detect this transition
250
+ def transition_to_color(config, color)
251
+ case color
252
+ when Domain::Color::GREEN
253
+ transition_to_green(config)
254
+ when Domain::Color::YELLOW
255
+ transition_to_yellow(config)
256
+ when Domain::Color::RED
257
+ transition_to_red(config)
258
+ else
259
+ raise ArgumentError, "Invalid color: #{color}"
260
+ end
261
+ end
262
+
263
+ # Transitions to GREEN state and ensures only one notification
264
+ #
265
+ # @param config [Stoplight::Domain::Config] The light configuration
266
+ # @return [Boolean] true if this is the first instance to detect this transition
267
+ private def transition_to_green(config)
268
+ current_ts = current_time.to_f
269
+ meta_key = metadata_key(config)
270
+
271
+ became_green = @redis.then do |client|
272
+ client.evalsha(
273
+ transition_to_green_sha,
274
+ argv: [current_ts],
275
+ keys: [meta_key]
276
+ )
277
+ end
278
+ became_green == 1
279
+ end
280
+
281
+ # Transitions to YELLOW (recovery) state and ensures only one notification
282
+ #
283
+ # @param config [Stoplight::Domain::Config] The light configuration
284
+ # @return [Boolean] true if this is the first instance to detect this transition
285
+ private def transition_to_yellow(config)
286
+ current_ts = current_time.to_i
287
+ meta_key = metadata_key(config)
288
+
289
+ became_yellow = @redis.then do |client|
290
+ client.evalsha(
291
+ transition_to_yellow_sha,
292
+ argv: [current_ts],
293
+ keys: [meta_key]
294
+ )
295
+ end
296
+ became_yellow == 1
297
+ end
298
+
299
+ # Transitions to RED state and ensures only one notification
300
+ #
301
+ # @param config [Stoplight::Domain::Config] The light configuration
302
+ # @return [Boolean] true if this is the first instance to detect this transition
303
+ private def transition_to_red(config)
304
+ current_ts = current_time.to_i
305
+ meta_key = metadata_key(config)
306
+ recovery_scheduled_after_ts = current_ts + config.cool_off_time
307
+
308
+ became_red = @redis.then do |client|
309
+ client.evalsha(
310
+ transition_to_red_sha,
311
+ argv: [current_ts, recovery_scheduled_after_ts],
312
+ keys: [meta_key]
313
+ )
314
+ end
315
+
316
+ became_red == 1
317
+ end
318
+
319
+ # @param failure_json [String]
320
+ # @return [Domain::Failure]
321
+ private def deserialize_failure(failure_json)
322
+ object = JSON.parse(failure_json)
323
+ error_object = object["error"]
324
+
325
+ error_class = error_object["class"]
326
+ error_message = error_object["message"]
327
+ time = Time.at(object["time"])
328
+
329
+ Domain::Failure.new(error_class, error_message, time)
330
+ end
331
+
332
+ # @param failure [Domain::Failure]
333
+ # @return [String]
334
+ private def serialize_failure(failure)
335
+ JSON.generate(
336
+ {
337
+ error: {
338
+ class: failure.error_class,
339
+ message: failure.error_message
340
+ },
341
+ time: failure.time.to_f
342
+ }
343
+ )
344
+ end
345
+
346
+ def_delegator "self.class", :key
347
+
348
+ private def failure_bucket_keys(config, window_end:)
349
+ self.class.buckets_for_window(
350
+ config.name,
351
+ metric: "failure",
352
+ window_end: window_end,
353
+ window_size: config.window_size
354
+ )
355
+ end
356
+
357
+ private def success_bucket_keys(config, window_end:)
358
+ self.class.buckets_for_window(
359
+ config.name,
360
+ metric: "success",
361
+ window_end: window_end,
362
+ window_size: config.window_size
363
+ )
364
+ end
365
+
366
+ private def recovery_probe_failure_bucket_keys(config, window_end:)
367
+ self.class.buckets_for_window(
368
+ config.name,
369
+ metric: "recovery_probe_failure",
370
+ window_end: window_end,
371
+ window_size: config.cool_off_time
372
+ )
373
+ end
374
+
375
+ private def recovery_probe_success_bucket_keys(config, window_end:)
376
+ self.class.buckets_for_window(
377
+ config.name,
378
+ metric: "recovery_probe_success",
379
+ window_end: window_end,
380
+ window_size: config.cool_off_time
381
+ )
382
+ end
383
+
384
+ private def successes_key(config, time:)
385
+ self.class.bucket_key(config.name, metric: "success", time:)
386
+ end
387
+
388
+ private def errors_key(config, time:)
389
+ self.class.bucket_key(config.name, metric: "failure", time:)
390
+ end
391
+
392
+ private def recovery_probe_successes_key(config, time:)
393
+ self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
394
+ end
395
+
396
+ private def recovery_probe_errors_key(config, time:)
397
+ self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
398
+ end
399
+
400
+ private def metadata_key(config)
401
+ key("metadata", config.name)
402
+ end
403
+
404
+ METRICS_TTL = 86400 # 1 day
405
+ private_constant :METRICS_TTL
406
+
407
+ private def metrics_ttl
408
+ METRICS_TTL
409
+ end
410
+
411
+ METADATA_TTL = 86400 * 7 # 7 days
412
+ private_constant :METADATA_TTL
413
+
414
+ private def metadata_ttl
415
+ METADATA_TTL
416
+ end
417
+
418
+ SKEW_TOLERANCE = 5 # seconds
419
+ private_constant :SKEW_TOLERANCE
420
+
421
+ private def detect_clock_skew
422
+ return unless @warn_on_clock_skew
423
+ return unless should_sample?(0.01) # 1% chance
424
+
425
+ redis_seconds, _redis_millis = @redis.then(&:time)
426
+ app_seconds = current_time.to_i
427
+ if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
428
+ 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")
429
+ end
430
+ end
431
+
432
+ private def should_sample?(probability)
433
+ rand <= probability
434
+ end
435
+
436
+ private def record_success_sha
437
+ @record_success_sha ||= @redis.then do |client|
438
+ client.script("load", Lua::RECORD_SUCCESS)
439
+ end
440
+ end
441
+
442
+ private def get_metadata_sha
443
+ @get_metadata_sha ||= @redis.then do |client|
444
+ client.script("load", Lua::GET_METADATA)
445
+ end
446
+ end
447
+
448
+ private def transition_to_yellow_sha
449
+ @transition_to_yellow_sha ||= @redis.then do |client|
450
+ client.script("load", Lua::TRANSITION_TO_YELLOW)
451
+ end
452
+ end
453
+
454
+ private def transition_to_red_sha
455
+ @transition_to_red_sha ||= @redis.then do |client|
456
+ client.script("load", Lua::TRANSITION_TO_RED)
457
+ end
458
+ end
459
+
460
+ private def transition_to_green_sha
461
+ @transition_to_green_sha ||= @redis.then do |client|
462
+ client.script("load", Lua::TRANSITION_TO_GREEN)
463
+ end
464
+ end
465
+
466
+ private def record_failure_sha
467
+ @record_failure_sha ||= @redis.then do |client|
468
+ client.script("load", Lua::RECORD_FAILURE)
469
+ end
470
+ end
471
+
472
+ private def current_time
473
+ Time.now
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end