stoplight 5.4.0 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/stoplight/admin/views/layout.erb +3 -3
  4. data/lib/stoplight/admin.rb +4 -4
  5. data/lib/stoplight/domain/color.rb +11 -0
  6. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  7. data/lib/stoplight/domain/config.rb +55 -0
  8. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  9. data/lib/stoplight/domain/error.rb +42 -0
  10. data/lib/stoplight/domain/failure.rb +42 -0
  11. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  12. data/lib/stoplight/domain/light.rb +198 -0
  13. data/lib/stoplight/domain/light_factory.rb +75 -0
  14. data/lib/stoplight/domain/metadata.rb +65 -0
  15. data/lib/stoplight/domain/state.rb +11 -0
  16. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  17. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  18. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  19. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  20. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  21. data/lib/stoplight/domain/tracker/base.rb +41 -0
  22. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  23. data/lib/stoplight/domain/tracker/request.rb +67 -0
  24. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  25. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  26. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  27. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  28. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  29. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  30. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  31. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  32. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  33. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  34. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  36. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  37. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  38. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  39. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  40. data/lib/stoplight/version.rb +1 -1
  41. data/lib/stoplight/wiring/container.rb +80 -0
  42. data/lib/stoplight/wiring/default.rb +28 -0
  43. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  44. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  45. data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +22 -11
  46. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  47. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  48. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  49. data/lib/stoplight/wiring/light_factory.rb +188 -0
  50. data/lib/stoplight/wiring/public_api.rb +28 -0
  51. data/lib/stoplight/wiring/system_container.rb +9 -0
  52. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  53. data/lib/stoplight.rb +38 -28
  54. metadata +53 -43
  55. data/lib/stoplight/color.rb +0 -9
  56. data/lib/stoplight/config/dsl.rb +0 -97
  57. data/lib/stoplight/config/library_default_config.rb +0 -21
  58. data/lib/stoplight/config/system_config.rb +0 -10
  59. data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
  60. data/lib/stoplight/data_store/memory.rb +0 -285
  61. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  62. data/lib/stoplight/data_store/redis.rb +0 -446
  63. data/lib/stoplight/data_store.rb +0 -6
  64. data/lib/stoplight/default.rb +0 -30
  65. data/lib/stoplight/error.rb +0 -39
  66. data/lib/stoplight/failure.rb +0 -71
  67. data/lib/stoplight/light/config.rb +0 -112
  68. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  69. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  70. data/lib/stoplight/light/red_run_strategy.rb +0 -31
  71. data/lib/stoplight/light/run_strategy.rb +0 -32
  72. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  73. data/lib/stoplight/light.rb +0 -191
  74. data/lib/stoplight/metadata.rb +0 -99
  75. data/lib/stoplight/notifier/generic.rb +0 -79
  76. data/lib/stoplight/notifier/io.rb +0 -21
  77. data/lib/stoplight/notifier/logger.rb +0 -19
  78. data/lib/stoplight/state.rb +0 -9
  79. data/lib/stoplight/traffic_control/base.rb +0 -70
  80. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  81. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  82. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  83. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  84. data/lib/stoplight/traffic_recovery.rb +0 -11
  85. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  90. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -1,446 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "forwardable"
4
-
5
- module Stoplight
6
- module DataStore
7
- # == Errors
8
- # All errors are stored in the sorted set where keys are serialized errors and
9
- # values (Redis uses "score" term) contain integer representations of the time
10
- # when an error happened.
11
- #
12
- # This data structure enables us to query errors that happened within a specific
13
- # period. We use this feature to support +window_size+ option.
14
- #
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
- #
18
- # @see Base
19
- class Redis < Base
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
67
-
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
81
- @redis = redis
82
- end
83
-
84
- def names
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
91
- end
92
- end
93
-
94
- def get_metadata(config)
95
- detect_clock_skew
96
-
97
- window_end_ts = current_time.to_i
98
- window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
99
- recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
100
-
101
- if config.window_size
102
- failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
103
- success_keys = success_bucket_keys(config, window_end: window_end_ts)
104
- else
105
- failure_keys = []
106
- success_keys = []
107
- end
108
- recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
109
- recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
110
-
111
- successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
112
- client.evalsha(
113
- get_metadata_sha,
114
- argv: [
115
- failure_keys.count,
116
- recovery_probe_failure_keys.count,
117
- window_start_ts,
118
- window_end_ts,
119
- recovery_window_start_ts
120
- ],
121
- keys: [
122
- metadata_key(config),
123
- *success_keys,
124
- *failure_keys,
125
- *recovery_probe_success_keys,
126
- *recovery_probe_failure_keys
127
- ]
128
- )
129
- end
130
- meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
131
- last_error_json = meta_hash.delete(:last_error_json)
132
- last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
133
-
134
- Metadata.new(
135
- current_time:,
136
- successes:,
137
- errors:,
138
- recovery_probe_successes:,
139
- recovery_probe_errors:,
140
- last_error:,
141
- **meta_hash
142
- )
143
- end
144
-
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 = current_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
- )
161
- end
162
- get_metadata(config)
163
- end
164
-
165
- def record_success(config, request_id: SecureRandom.hex(12))
166
- current_ts = current_time.to_i
167
-
168
- @redis.then do |client|
169
- client.evalsha(
170
- record_success_sha,
171
- argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
172
- keys: [
173
- metadata_key(config),
174
- config.window_size && successes_key(config, time: current_ts)
175
- ].compact
176
- )
177
- end
178
- end
179
-
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 = current_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)
200
- end
201
-
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
- # @return [Stoplight::Metadata] The updated metadata after recording the success.
207
- def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
208
- current_ts = current_time.to_i
209
-
210
- @redis.then do |client|
211
- client.evalsha(
212
- record_success_sha,
213
- argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
214
- keys: [
215
- metadata_key(config),
216
- recovery_probe_successes_key(config, time: current_ts)
217
- ].compact
218
- )
219
- end
220
- get_metadata(config)
221
- end
222
-
223
- def set_state(config, state)
224
- @redis.then do |client|
225
- client.hset(metadata_key(config), "locked_state", state)
226
- end
227
- state
228
- end
229
-
230
- def inspect
231
- "#<#{self.class.name} redis=#{@redis.inspect}>"
232
- end
233
-
234
- # Combined method that performs the state transition based on color
235
- #
236
- # @param config [Stoplight::Light::Config] The light configuration
237
- # @param color [String] The color to transition to ("green", "yellow", or "red")
238
- # @return [Boolean] true if this is the first instance to detect this transition
239
- def transition_to_color(config, color)
240
- case color
241
- when Color::GREEN
242
- transition_to_green(config)
243
- when Color::YELLOW
244
- transition_to_yellow(config)
245
- when Color::RED
246
- transition_to_red(config)
247
- else
248
- raise ArgumentError, "Invalid color: #{color}"
249
- end
250
- end
251
-
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)
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
- )
266
- end
267
- became_green == 1
268
- end
269
-
270
- # Transitions to YELLOW (recovery) state and ensures only one notification
271
- #
272
- # @param config [Stoplight::Light::Config] The light configuration
273
- # @return [Boolean] true if this is the first instance to detect this transition
274
- private def transition_to_yellow(config)
275
- current_ts = current_time.to_i
276
- meta_key = metadata_key(config)
277
-
278
- became_yellow = @redis.then do |client|
279
- client.evalsha(
280
- transition_to_yellow_sha,
281
- argv: [current_ts],
282
- keys: [meta_key]
283
- )
284
- end
285
- became_yellow == 1
286
- end
287
-
288
- # Transitions to RED state and ensures only one notification
289
- #
290
- # @param config [Stoplight::Light::Config] The light configuration
291
- # @return [Boolean] true if this is the first instance to detect this transition
292
- private def transition_to_red(config)
293
- current_ts = current_time.to_i
294
- meta_key = metadata_key(config)
295
- recovery_scheduled_after_ts = current_ts + config.cool_off_time
296
-
297
- became_red = @redis.then do |client|
298
- client.evalsha(
299
- transition_to_red_sha,
300
- argv: [current_ts, recovery_scheduled_after_ts],
301
- keys: [meta_key]
302
- )
303
- end
304
-
305
- became_red == 1
306
- end
307
-
308
- private def normalize_failure(failure, error_notifier)
309
- Failure.from_json(failure)
310
- rescue => e
311
- error_notifier.call(e)
312
- Failure.from_error(e)
313
- end
314
-
315
- def_delegator "self.class", :key
316
-
317
- private def failure_bucket_keys(config, window_end:)
318
- self.class.buckets_for_window(
319
- config.name,
320
- metric: "failure",
321
- window_end: window_end,
322
- window_size: config.window_size
323
- )
324
- end
325
-
326
- private def success_bucket_keys(config, window_end:)
327
- self.class.buckets_for_window(
328
- config.name,
329
- metric: "success",
330
- window_end: window_end,
331
- window_size: config.window_size
332
- )
333
- end
334
-
335
- private def recovery_probe_failure_bucket_keys(config, window_end:)
336
- self.class.buckets_for_window(
337
- config.name,
338
- metric: "recovery_probe_failure",
339
- window_end: window_end,
340
- window_size: config.cool_off_time
341
- )
342
- end
343
-
344
- private def recovery_probe_success_bucket_keys(config, window_end:)
345
- self.class.buckets_for_window(
346
- config.name,
347
- metric: "recovery_probe_success",
348
- window_end: window_end,
349
- window_size: config.cool_off_time
350
- )
351
- end
352
-
353
- private def successes_key(config, time:)
354
- self.class.bucket_key(config.name, metric: "success", time:)
355
- end
356
-
357
- private def errors_key(config, time:)
358
- self.class.bucket_key(config.name, metric: "failure", time:)
359
- end
360
-
361
- private def recovery_probe_successes_key(config, time:)
362
- self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
363
- end
364
-
365
- private def recovery_probe_errors_key(config, time:)
366
- self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
367
- end
368
-
369
- private def metadata_key(config)
370
- key("metadata", config.name)
371
- end
372
-
373
- METRICS_TTL = 86400 # 1 day
374
- private_constant :METRICS_TTL
375
-
376
- private def metrics_ttl
377
- METRICS_TTL
378
- end
379
-
380
- METADATA_TTL = 86400 * 7 # 7 days
381
- private_constant :METADATA_TTL
382
-
383
- private def metadata_ttl
384
- METADATA_TTL
385
- end
386
-
387
- SKEW_TOLERANCE = 5 # seconds
388
- private_constant :SKEW_TOLERANCE
389
-
390
- private def detect_clock_skew
391
- return unless @warn_on_clock_skew
392
- return unless should_sample?(0.01) # 1% chance
393
-
394
- redis_seconds, _redis_millis = @redis.then(&:time)
395
- app_seconds = current_time.to_i
396
- if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
397
- 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")
398
- end
399
- end
400
-
401
- private def should_sample?(probability)
402
- rand <= probability
403
- end
404
-
405
- private def record_success_sha
406
- @record_success_sha ||= @redis.then do |client|
407
- client.script("load", Lua::RECORD_SUCCESS)
408
- end
409
- end
410
-
411
- private def get_metadata_sha
412
- @get_metadata_sha ||= @redis.then do |client|
413
- client.script("load", Lua::GET_METADATA)
414
- end
415
- end
416
-
417
- private def transition_to_yellow_sha
418
- @transition_to_yellow_sha ||= @redis.then do |client|
419
- client.script("load", Lua::TRANSITION_TO_YELLOW)
420
- end
421
- end
422
-
423
- private def transition_to_red_sha
424
- @transition_to_red_sha ||= @redis.then do |client|
425
- client.script("load", Lua::TRANSITION_TO_RED)
426
- end
427
- end
428
-
429
- private def transition_to_green_sha
430
- @transition_to_green_sha ||= @redis.then do |client|
431
- client.script("load", Lua::TRANSITION_TO_GREEN)
432
- end
433
- end
434
-
435
- private def record_failure_sha
436
- @record_failure_sha ||= @redis.then do |client|
437
- client.script("load", Lua::RECORD_FAILURE)
438
- end
439
- end
440
-
441
- private def current_time
442
- Time.now
443
- end
444
- end
445
- end
446
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module DataStore # rubocop:disable Style/Documentation
5
- end
6
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module Default
5
- COOL_OFF_TIME = 60.0
6
-
7
- DATA_STORE = DataStore::Memory.new
8
-
9
- ERROR_NOTIFIER = ->(error) { warn error }
10
-
11
- FORMATTER = lambda do |light, from_color, to_color, error|
12
- words = ["Switching", light.name, "from", from_color, "to", to_color]
13
- words += ["because", error.class, error.message] if error
14
- words.join(" ")
15
- end
16
-
17
- NOTIFIERS = [Notifier::IO.new($stderr)].freeze
18
-
19
- THRESHOLD = 3
20
- RECOVERY_THRESHOLD = 1
21
-
22
- WINDOW_SIZE = nil
23
-
24
- TRACKED_ERRORS = [StandardError].freeze
25
- SKIPPED_ERRORS = [].freeze
26
-
27
- TRAFFIC_CONTROL = TrafficControl::ConsecutiveErrors.new
28
- TRAFFIC_RECOVERY = TrafficRecovery::ConsecutiveSuccesses.new
29
- end
30
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module Error
5
- Base = Class.new(StandardError)
6
- ConfigurationError = Class.new(Base)
7
- IncorrectColor = Class.new(Base)
8
- class RedLight < Base
9
- # @!attribute light_name
10
- # @return [String] The light's name
11
- attr_reader :light_name
12
-
13
- # @!attribute cool_off_time
14
- # @return [Numeric] Cool-off period in seconds
15
- attr_reader :cool_off_time
16
-
17
- # @!attribute retry_after
18
- # @return [Time] Absolute Time after which a recovery attempt can occur
19
- attr_reader :retry_after
20
-
21
- # Initializes a new RedLight error.
22
- #
23
- # @param light_name [String] The light's name
24
- #
25
- # @option cool_off_time [Numeric] Cool-off period in seconds
26
- #
27
- # @option retry_after [Time] Absolute Time after which a recovery attempt can occur
28
- #
29
- # @return [Stoplight::Error::RedLight]
30
- def initialize(light_name, cool_off_time:, retry_after:)
31
- @light_name = light_name
32
- @cool_off_time = cool_off_time
33
- @retry_after = retry_after
34
-
35
- super(light_name)
36
- end
37
- end
38
- end
39
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "time"
5
-
6
- module Stoplight
7
- class Failure # rubocop:disable Style/Documentation
8
- TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%N%:z"
9
-
10
- # @return [String]
11
- attr_reader :error_class
12
- # @return [String]
13
- attr_reader :error_message
14
- # @return [Time]
15
- attr_reader :time
16
-
17
- # @param error [Exception]
18
- # @return (see #initialize)
19
- def self.from_error(error, time: Time.now)
20
- new(error.class.name, error.message, time)
21
- end
22
-
23
- # @param json [String]
24
- # @return (see #initialize)
25
- # @raise [JSON::ParserError]
26
- # @raise [ArgumentError]
27
- def self.from_json(json)
28
- object = JSON.parse(json)
29
- error_object = object["error"]
30
-
31
- error_class = error_object["class"]
32
- error_message = error_object["message"]
33
- time = Time.at(object["time"])
34
-
35
- new(error_class, error_message, time)
36
- end
37
-
38
- # @param error_class [String]
39
- # @param error_message [String]
40
- # @param time [Time]
41
- def initialize(error_class, error_message, time)
42
- @error_class = error_class
43
- @error_message = error_message
44
- @time = Time.at(time.to_i) # truncate to seconds
45
- end
46
-
47
- # @param other [Failure]
48
- # @return [Boolean]
49
- def ==(other)
50
- other.is_a?(self.class) &&
51
- error_class == other.error_class &&
52
- error_message == other.error_message &&
53
- time.to_i == other.time.to_i
54
- end
55
-
56
- # @param options [Object, nil]
57
- # @return [String]
58
- def to_json(options = nil)
59
- JSON.generate(
60
- {
61
- error: {
62
- class: error_class,
63
- message: error_message
64
- },
65
- time: time.to_i
66
- },
67
- options
68
- )
69
- end
70
- end
71
- end
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- class Light
5
- # A +Stoplight::Light+ configuration object.
6
- #
7
- # # @!attribute [r] name
8
- # @return [String]
9
- #
10
- # @!attribute [r] cool_off_time
11
- # @return [Numeric]
12
- #
13
- # @!attribute [r] data_store
14
- # @return [Stoplight::DataStore::Base]
15
- #
16
- # @!attribute [r] error_notifier
17
- # @return [StandardError => void]
18
- #
19
- # @!attribute [r] notifiers
20
- # @return [Array<Stoplight::Notifier::Base>]
21
- #
22
- # @!attribute [r] threshold
23
- # @return [Numeric]
24
- #
25
- # @!attribute [r] window_size
26
- # @return [Numeric]
27
- #
28
- # @!attribute [r] tracked_errors
29
- # @return [Array<StandardError>]
30
- #
31
- # @!attribute [r] skipped_errors
32
- # @return [Array<Exception>]
33
- #
34
- # @!attribute [r] traffic_control
35
- # @return [Stoplight::TrafficControl::Base]
36
- #
37
- # @!attribute [r] traffic_recovery
38
- # @return [Stoplight::TrafficRecovery::Base]
39
- # @api private
40
- Config = Data.define(
41
- :name,
42
- :cool_off_time,
43
- :data_store,
44
- :error_notifier,
45
- :notifiers,
46
- :threshold,
47
- :recovery_threshold,
48
- :window_size,
49
- :tracked_errors,
50
- :skipped_errors,
51
- :traffic_control,
52
- :traffic_recovery
53
- ) do
54
- class << self
55
- # Creates a new NULL configuration object.
56
- # @return [Stoplight::Light::Config]
57
- def empty
58
- new(**members.map { |key| [key, nil] }.to_h)
59
- end
60
- end
61
-
62
- # Checks if the given error should be tracked
63
- #
64
- # @param error [#==] The error to check, e.g. an Exception, Class or Proc
65
- # @return [Boolean]
66
- def track_error?(error)
67
- skip = skipped_errors.any? { |klass| klass === error }
68
- track = tracked_errors.any? { |klass| klass === error }
69
-
70
- !skip && track
71
- end
72
-
73
- # This method applies configuration dsl and revalidates the configuration
74
- # @return [Stoplight::Light::Config]
75
- def with(**settings)
76
- super(**CONFIG_DSL.transform(settings)).then do |config|
77
- config.validate_config!
78
- end
79
- end
80
-
81
- # @raise [Stoplight::Error::ConfigurationError]
82
- # @return [Stoplight::Light::Config] The validated configuration object.
83
- def validate_config!
84
- validate_traffic_control_compatibility!
85
- validate_traffic_recovery_compatibility!
86
- self
87
- end
88
-
89
- private
90
-
91
- def validate_traffic_control_compatibility!
92
- traffic_control.check_compatibility(self).then do |compatibility_result|
93
- if compatibility_result.incompatible?
94
- raise Stoplight::Error::ConfigurationError.new(
95
- "#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
96
- )
97
- end
98
- end
99
- end
100
-
101
- def validate_traffic_recovery_compatibility!
102
- traffic_recovery.check_compatibility(self).then do |compatibility_result|
103
- if compatibility_result.incompatible?
104
- raise Stoplight::Error::ConfigurationError.new(
105
- "#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
106
- )
107
- end
108
- end
109
- end
110
- end
111
- end
112
- end