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
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module DataStore
5
- class Redis
6
- # @api private
7
- module Lua
8
- class << self
9
- def read_lua_file(name_without_extension)
10
- File.read(File.join(__dir__, "#{name_without_extension}.lua"))
11
- end
12
- end
13
-
14
- RECORD_FAILURE = read_lua_file("record_failure")
15
- RECORD_SUCCESS = read_lua_file("record_success")
16
- GET_METADATA = read_lua_file("get_metadata")
17
- TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
18
- TRANSITION_TO_RED = read_lua_file("transition_to_red")
19
- TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
20
- end
21
- end
22
- end
23
- end
@@ -1,449 +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
- current_time = Time.now
98
- window_end_ts = current_time.to_i
99
- window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
100
- recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
101
-
102
- if config.window_size
103
- failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
104
- success_keys = success_bucket_keys(config, window_end: window_end_ts)
105
- else
106
- failure_keys = []
107
- success_keys = []
108
- end
109
- recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
110
- recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
111
-
112
- successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
113
- client.evalsha(
114
- get_metadata_sha,
115
- argv: [
116
- failure_keys.count,
117
- recovery_probe_failure_keys.count,
118
- window_start_ts,
119
- window_end_ts,
120
- recovery_window_start_ts
121
- ],
122
- keys: [
123
- metadata_key(config),
124
- *success_keys,
125
- *failure_keys,
126
- *recovery_probe_success_keys,
127
- *recovery_probe_failure_keys
128
- ]
129
- )
130
- end
131
- meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
132
- last_error_json = meta_hash.delete(:last_error_json)
133
- last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
134
-
135
- Metadata.new(
136
- current_time:,
137
- successes:,
138
- errors:,
139
- recovery_probe_successes:,
140
- recovery_probe_errors:,
141
- last_error:,
142
- **meta_hash
143
- )
144
- end
145
-
146
- # @param config [Stoplight::Light::Config] The light configuration.
147
- # @param failure [Stoplight::Failure] The failure to record.
148
- # @return [Stoplight::Metadata] The updated metadata after recording the failure.
149
- def record_failure(config, failure)
150
- current_ts = failure.time.to_i
151
- failure_json = failure.to_json
152
-
153
- @redis.then do |client|
154
- client.evalsha(
155
- record_failure_sha,
156
- argv: [current_ts, SecureRandom.hex(12), failure_json, metrics_ttl, metadata_ttl],
157
- keys: [
158
- metadata_key(config),
159
- config.window_size && errors_key(config, time: current_ts)
160
- ].compact
161
- )
162
- end
163
- get_metadata(config)
164
- end
165
-
166
- def record_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
167
- request_ts = request_time.to_i
168
-
169
- @redis.then do |client|
170
- client.evalsha(
171
- record_success_sha,
172
- argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
173
- keys: [
174
- metadata_key(config),
175
- config.window_size && successes_key(config, time: request_ts)
176
- ].compact
177
- )
178
- end
179
- end
180
-
181
- # Records a failed recovery probe for a specific light configuration.
182
- #
183
- # @param config [Stoplight::Light::Config] The light configuration.
184
- # @param failure [Failure] The failure to record.
185
- # @return [Stoplight::Metadata] The updated metadata after recording the failure.
186
- def record_recovery_probe_failure(config, failure)
187
- current_ts = failure.time.to_i
188
- failure_json = failure.to_json
189
-
190
- @redis.then do |client|
191
- client.evalsha(
192
- record_failure_sha,
193
- argv: [current_ts, SecureRandom.uuid, failure_json, metrics_ttl, metrics_ttl],
194
- keys: [
195
- metadata_key(config),
196
- recovery_probe_errors_key(config, time: current_ts)
197
- ].compact
198
- )
199
- end
200
- get_metadata(config)
201
- end
202
-
203
- # Records a successful recovery probe for a specific light configuration.
204
- #
205
- # @param config [Stoplight::Light::Config] The light configuration.
206
- # @param request_id [String] The unique identifier for the request
207
- # @param request_time [Time] The time of the request
208
- # @return [Stoplight::Metadata] The updated metadata after recording the success.
209
- def record_recovery_probe_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
210
- request_ts = request_time.to_i
211
-
212
- @redis.then do |client|
213
- client.evalsha(
214
- record_success_sha,
215
- argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
216
- keys: [
217
- metadata_key(config),
218
- recovery_probe_successes_key(config, time: request_ts)
219
- ].compact
220
- )
221
- end
222
- get_metadata(config)
223
- end
224
-
225
- def set_state(config, state)
226
- @redis.then do |client|
227
- client.hset(metadata_key(config), "locked_state", state)
228
- end
229
- state
230
- end
231
-
232
- def inspect
233
- "#<#{self.class.name} redis=#{@redis.inspect}>"
234
- end
235
-
236
- # Combined method that performs the state transition based on color
237
- #
238
- # @param config [Stoplight::Light::Config] The light configuration
239
- # @param color [String] The color to transition to ("green", "yellow", or "red")
240
- # @param current_time [Time] Current timestamp
241
- # @return [Boolean] true if this is the first instance to detect this transition
242
- def transition_to_color(config, color, current_time: Time.now)
243
- current_time.to_i
244
-
245
- case color
246
- when Color::GREEN
247
- transition_to_green(config)
248
- when Color::YELLOW
249
- transition_to_yellow(config, current_time:)
250
- when Color::RED
251
- transition_to_red(config, current_time:)
252
- else
253
- raise ArgumentError, "Invalid color: #{color}"
254
- end
255
- end
256
-
257
- # Transitions to GREEN state and ensures only one notification
258
- #
259
- # @param config [Stoplight::Light::Config] The light configuration
260
- # @return [Boolean] true if this is the first instance to detect this transition
261
- private def transition_to_green(config, current_time: Time.now)
262
- current_ts = current_time.to_i
263
- meta_key = metadata_key(config)
264
-
265
- became_green = @redis.then do |client|
266
- client.evalsha(
267
- transition_to_green_sha,
268
- argv: [current_ts],
269
- keys: [meta_key]
270
- )
271
- end
272
- became_green == 1
273
- end
274
-
275
- # Transitions to YELLOW (recovery) state and ensures only one notification
276
- #
277
- # @param config [Stoplight::Light::Config] The light configuration
278
- # @param current_time [Time] Current timestamp
279
- # @return [Boolean] true if this is the first instance to detect this transition
280
- private def transition_to_yellow(config, current_time: Time.now)
281
- current_ts = current_time.to_i
282
- meta_key = metadata_key(config)
283
-
284
- became_yellow = @redis.then do |client|
285
- client.evalsha(
286
- transition_to_yellow_sha,
287
- argv: [current_ts],
288
- keys: [meta_key]
289
- )
290
- end
291
- became_yellow == 1
292
- end
293
-
294
- # Transitions to RED state and ensures only one notification
295
- #
296
- # @param config [Stoplight::Light::Config] The light configuration
297
- # @param current_time [Time] Current timestamp
298
- # @return [Boolean] true if this is the first instance to detect this transition
299
- private def transition_to_red(config, current_time: Time.now)
300
- current_ts = current_time.to_i
301
- meta_key = metadata_key(config)
302
- recovery_scheduled_after_ts = current_ts + config.cool_off_time
303
-
304
- became_red = @redis.then do |client|
305
- client.evalsha(
306
- transition_to_red_sha,
307
- argv: [current_ts, recovery_scheduled_after_ts],
308
- keys: [meta_key]
309
- )
310
- end
311
-
312
- became_red == 1
313
- end
314
-
315
- private def normalize_failure(failure, error_notifier)
316
- Failure.from_json(failure)
317
- rescue => e
318
- error_notifier.call(e)
319
- Failure.from_error(e)
320
- end
321
-
322
- def_delegator "self.class", :key
323
-
324
- private def failure_bucket_keys(config, window_end:)
325
- self.class.buckets_for_window(
326
- config.name,
327
- metric: "failure",
328
- window_end: window_end,
329
- window_size: config.window_size
330
- )
331
- end
332
-
333
- private def success_bucket_keys(config, window_end:)
334
- self.class.buckets_for_window(
335
- config.name,
336
- metric: "success",
337
- window_end: window_end,
338
- window_size: config.window_size
339
- )
340
- end
341
-
342
- private def recovery_probe_failure_bucket_keys(config, window_end:)
343
- self.class.buckets_for_window(
344
- config.name,
345
- metric: "recovery_probe_failure",
346
- window_end: window_end,
347
- window_size: config.cool_off_time
348
- )
349
- end
350
-
351
- private def recovery_probe_success_bucket_keys(config, window_end:)
352
- self.class.buckets_for_window(
353
- config.name,
354
- metric: "recovery_probe_success",
355
- window_end: window_end,
356
- window_size: config.cool_off_time
357
- )
358
- end
359
-
360
- private def successes_key(config, time:)
361
- self.class.bucket_key(config.name, metric: "success", time:)
362
- end
363
-
364
- private def errors_key(config, time:)
365
- self.class.bucket_key(config.name, metric: "failure", time:)
366
- end
367
-
368
- private def recovery_probe_successes_key(config, time:)
369
- self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
370
- end
371
-
372
- private def recovery_probe_errors_key(config, time:)
373
- self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
374
- end
375
-
376
- private def metadata_key(config)
377
- key("metadata", config.name)
378
- end
379
-
380
- METRICS_TTL = 86400 # 1 day
381
- private_constant :METRICS_TTL
382
-
383
- private def metrics_ttl
384
- METRICS_TTL
385
- end
386
-
387
- METADATA_TTL = 86400 * 7 # 7 days
388
- private_constant :METADATA_TTL
389
-
390
- private def metadata_ttl
391
- METADATA_TTL
392
- end
393
-
394
- SKEW_TOLERANCE = 5 # seconds
395
- private_constant :SKEW_TOLERANCE
396
-
397
- private def detect_clock_skew
398
- return unless @warn_on_clock_skew
399
- return unless should_sample?(0.01) # 1% chance
400
-
401
- redis_seconds, _redis_millis = @redis.then(&:time)
402
- app_seconds = Time.now.to_i
403
- if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
404
- 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")
405
- end
406
- end
407
-
408
- private def should_sample?(probability)
409
- rand <= probability
410
- end
411
-
412
- private def record_success_sha
413
- @record_success_sha ||= @redis.then do |client|
414
- client.script("load", Lua::RECORD_SUCCESS)
415
- end
416
- end
417
-
418
- private def get_metadata_sha
419
- @get_metadata_sha ||= @redis.then do |client|
420
- client.script("load", Lua::GET_METADATA)
421
- end
422
- end
423
-
424
- private def transition_to_yellow_sha
425
- @transition_to_yellow_sha ||= @redis.then do |client|
426
- client.script("load", Lua::TRANSITION_TO_YELLOW)
427
- end
428
- end
429
-
430
- private def transition_to_red_sha
431
- @transition_to_red_sha ||= @redis.then do |client|
432
- client.script("load", Lua::TRANSITION_TO_RED)
433
- end
434
- end
435
-
436
- private def transition_to_green_sha
437
- @transition_to_green_sha ||= @redis.then do |client|
438
- client.script("load", Lua::TRANSITION_TO_GREEN)
439
- end
440
- end
441
-
442
- private def record_failure_sha
443
- @record_failure_sha ||= @redis.then do |client|
444
- client.script("load", Lua::RECORD_FAILURE)
445
- end
446
- end
447
- end
448
- end
449
- 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,10 +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
- RedLight = Class.new(Base)
9
- end
10
- 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,111 +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
- self
86
- end
87
-
88
- private
89
-
90
- def validate_traffic_control_compatibility!
91
- traffic_control.check_compatibility(self).then do |compatibility_result|
92
- if compatibility_result.incompatible?
93
- raise Stoplight::Error::ConfigurationError.new(
94
- "#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
95
- )
96
- end
97
- end
98
- end
99
-
100
- def validate_traffic_recovery_compatibility!
101
- traffic_recovery.check_compatibility(self).then do |compatibility_result|
102
- if compatibility_result.incompatible?
103
- raise Stoplight::Error::ConfigurationError.new(
104
- "#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
105
- )
106
- end
107
- end
108
- end
109
- end
110
- end
111
- end