stoplight 5.6.0 → 5.7.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stoplight/admin/dependencies.rb +1 -1
  3. data/lib/stoplight/admin/helpers.rb +10 -5
  4. data/lib/stoplight/admin/lights_repository.rb +18 -15
  5. data/lib/stoplight/admin.rb +2 -1
  6. data/lib/stoplight/common/deprecations.rb +11 -0
  7. data/lib/stoplight/domain/config.rb +5 -1
  8. data/lib/stoplight/domain/data_store.rb +17 -1
  9. data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
  10. data/lib/stoplight/domain/light.rb +31 -20
  11. data/lib/stoplight/domain/metrics.rb +6 -27
  12. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  13. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  14. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  15. data/lib/stoplight/domain/storage/state.rb +87 -0
  16. data/lib/stoplight/domain/strategies/run_strategy.rb +0 -5
  17. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +58 -32
  18. data/lib/stoplight/domain/tracker/base.rb +0 -29
  19. data/lib/stoplight/domain/tracker/recovery_probe.rb +23 -22
  20. data/lib/stoplight/domain/tracker/request.rb +23 -19
  21. data/lib/stoplight/domain/traffic_recovery/base.rb +1 -2
  22. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +2 -8
  23. data/lib/stoplight/domain/traffic_recovery.rb +0 -1
  24. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  25. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  26. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  27. data/lib/stoplight/infrastructure/data_store/memory.rb +61 -32
  28. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  29. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  30. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  31. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  32. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  33. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  34. data/lib/stoplight/infrastructure/data_store/redis.rb +133 -162
  35. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  36. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  37. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  38. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  39. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  40. data/lib/stoplight/version.rb +1 -1
  41. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  42. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  43. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  44. data/lib/stoplight/wiring/default.rb +1 -1
  45. data/lib/stoplight/wiring/default_configuration.rb +1 -1
  46. data/lib/stoplight/wiring/default_factory_builder.rb +1 -1
  47. data/lib/stoplight/wiring/light_builder.rb +185 -0
  48. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  49. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  50. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  51. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  52. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  53. data/lib/stoplight/wiring/light_factory.rb +45 -132
  54. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  55. data/lib/stoplight/wiring/public_api.rb +3 -2
  56. data/lib/stoplight.rb +18 -3
  57. metadata +50 -15
  58. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
  59. data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
  60. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
  61. data/lib/stoplight/wiring/container.rb +0 -80
  62. data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -147
  63. data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
  64. data/lib/stoplight/wiring/system_container.rb +0 -9
  65. data/lib/stoplight/wiring/system_light_factory.rb +0 -17
  66. /data/lib/stoplight/infrastructure/data_store/redis/{get_metrics.lua → lua_scripts/get_metrics.lua} +0 -0
  67. /data/lib/stoplight/infrastructure/data_store/redis/{record_failure.lua → lua_scripts/record_failure.lua} +0 -0
  68. /data/lib/stoplight/infrastructure/data_store/redis/{record_success.lua → lua_scripts/record_success.lua} +0 -0
  69. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_green.lua → lua_scripts/transition_to_green.lua} +0 -0
  70. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_red.lua → lua_scripts/transition_to_red.lua} +0 -0
  71. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_yellow.lua → lua_scripts/transition_to_yellow.lua} +0 -0
@@ -74,22 +74,46 @@ module Stoplight
74
74
  KEY_SEPARATOR = ":"
75
75
  KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
76
76
 
77
+ # @!attribute recovery_lock_store
78
+ # @return [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockStore]
79
+ protected attr_reader :recovery_lock_store
80
+
81
+ # @!attribute scripting
82
+ # @return [Stoplight::Infrastructure::DataStore::Redis::Scripting]
83
+ protected attr_reader :scripting
84
+
85
+ # @!attribute redis
86
+ # @return [::Redis | ConnectionPool<::Redis>]
87
+ protected attr_reader :redis
88
+
89
+ # @!attribute warn_on_clock_skew
90
+ # @return [Boolean]
91
+ protected attr_reader :warn_on_clock_skew
92
+
77
93
  # @param redis [::Redis, ConnectionPool<::Redis>]
94
+ # @param recovery_lock_store [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockStore]
78
95
  # @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
96
+ # @param scripting [Stoplight::Infrastructure::DataStore::Redis::Scripting]
79
97
  # the application server
80
- def initialize(redis, warn_on_clock_skew: true)
98
+ def initialize(redis:, recovery_lock_store:, scripting:, warn_on_clock_skew: true)
81
99
  @warn_on_clock_skew = warn_on_clock_skew
82
100
  @redis = redis
101
+ @recovery_lock_store = recovery_lock_store
102
+ @scripting = scripting
83
103
  end
84
104
 
85
105
  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, "")
106
+ metadata, recovery_metrics = @redis.then do |client|
107
+ [
108
+ [key("metadata", "*"), /^#{key("metadata", "")}/],
109
+ [key("recovery_metrics", "*"), /^#{key("recovery_metrics", "")}/]
110
+ ].map do |(pattern, prefix_regex)|
111
+ client.scan_each(match: pattern).to_a.map do |key|
112
+ key.sub(prefix_regex, "")
113
+ end
91
114
  end
92
115
  end
116
+ metadata + recovery_metrics
93
117
  end
94
118
 
95
119
  # @param config [Stoplight::Domain::Config]
@@ -108,28 +132,28 @@ module Stoplight
108
132
  success_keys = []
109
133
  end
110
134
 
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
135
+ successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = scripting.call(
136
+ :get_metrics,
137
+ args: [
138
+ failure_keys.count,
139
+ window_start_ts,
140
+ window_end_ts,
141
+ "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
142
+ ],
143
+ keys: [
144
+ metadata_key(config),
145
+ *success_keys,
146
+ *failure_keys
147
+ ]
148
+ )
149
+ consecutive_errors = config.window_size ? [consecutive_errors.to_i, errors].min : consecutive_errors.to_i
150
+ consecutive_successes = config.window_size ? [consecutive_successes.to_i, successes].min : consecutive_successes.to_i
127
151
 
128
152
  Domain::Metrics.new(
129
153
  successes: (successes if config.window_size),
130
154
  errors: (errors if config.window_size),
131
- total_consecutive_errors: consecutive_errors.to_i,
132
- total_consecutive_successes: consecutive_successes.to_i,
155
+ consecutive_errors:,
156
+ consecutive_successes:,
133
157
  last_error: deserialize_failure(last_error_json),
134
158
  last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
135
159
  )
@@ -138,36 +162,17 @@ module Stoplight
138
162
  # @param config [Stoplight::Domain::Config]
139
163
  # @return [Stoplight::Domain::Metrics]
140
164
  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
- ]
165
+ last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
166
+ client.hmget(
167
+ recovery_metrics_key(config),
168
+ "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
163
169
  )
164
170
  end
165
171
 
166
172
  Domain::Metrics.new(
167
- successes:,
168
- errors:,
169
- total_consecutive_errors: consecutive_errors.to_i,
170
- total_consecutive_successes: consecutive_successes.to_i,
173
+ successes: nil, errors: nil,
174
+ consecutive_errors: consecutive_errors.to_i,
175
+ consecutive_successes: consecutive_successes.to_i,
171
176
  last_error: deserialize_failure(last_error_json),
172
177
  last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
173
178
  )
@@ -193,18 +198,31 @@ module Stoplight
193
198
  )
194
199
  end
195
200
 
196
- def clear_windowed_metrics(config)
201
+ def clear_metrics(config)
197
202
  if config.window_size
198
203
  window_end_ts = current_time.to_i
199
204
  @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
- )
205
+ client.multi do |tx|
206
+ tx.unlink(
207
+ *failure_bucket_keys(config, window_end: window_end_ts),
208
+ *success_bucket_keys(config, window_end: window_end_ts)
209
+ )
210
+ tx.hdel(metadata_key(config), "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes")
211
+ end
212
+ end
213
+ else
214
+ @redis.with do |client|
215
+ client.hdel(metadata_key(config), "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes")
204
216
  end
205
217
  end
206
218
  end
207
219
 
220
+ def clear_recovery_metrics(config)
221
+ @redis.with do |client|
222
+ client.del(recovery_metrics_key(config))
223
+ end
224
+ end
225
+
208
226
  private def state_snapshot_from_hash(data, time: current_time)
209
227
  breached_at = data[:breached_at]&.to_f
210
228
  recovery_scheduled_after = data[:recovery_scheduled_after]&.to_f
@@ -227,31 +245,27 @@ module Stoplight
227
245
  current_ts = current_time.to_f
228
246
  failure = Domain::Failure.from_error(exception, time: current_time)
229
247
 
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
248
+ scripting.call(
249
+ :record_failure,
250
+ args: [current_ts, SecureRandom.hex(12), serialize_failure(failure), metrics_ttl, metadata_ttl],
251
+ keys: [
252
+ metadata_key(config),
253
+ config.window_size && errors_key(config, time: current_ts)
254
+ ].compact
255
+ )
240
256
  end
241
257
 
242
258
  def record_success(config, request_id: SecureRandom.hex(12))
243
259
  current_ts = current_time.to_f
244
260
 
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
261
+ scripting.call(
262
+ :record_success,
263
+ args: [current_ts, request_id, metrics_ttl, metadata_ttl],
264
+ keys: [
265
+ metadata_key(config),
266
+ config.window_size && successes_key(config, time: current_ts)
267
+ ].compact
268
+ )
255
269
  end
256
270
 
257
271
  # Records a failed recovery probe for a specific light configuration.
@@ -264,36 +278,25 @@ module Stoplight
264
278
  current_ts = current_time.to_f
265
279
  failure = Domain::Failure.from_error(exception, time: current_time)
266
280
 
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
281
+ scripting.call(
282
+ :record_recovery_probe_failure,
283
+ args: [current_ts, serialize_failure(failure)],
284
+ keys: [recovery_metrics_key(config)]
285
+ )
277
286
  end
278
287
 
279
288
  # Records a successful recovery probe for a specific light configuration.
280
289
  #
281
290
  # @param config [Stoplight::Domain::Config] The light configuration.
282
- # @param request_id [String] The unique identifier for the request
283
291
  # @return [void]
284
- def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
292
+ def record_recovery_probe_success(config)
285
293
  current_ts = current_time.to_f
286
294
 
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
295
+ scripting.call(
296
+ :record_recovery_probe_success,
297
+ args: [current_ts],
298
+ keys: [recovery_metrics_key(config)]
299
+ )
297
300
  end
298
301
 
299
302
  def set_state(config, state)
@@ -325,6 +328,18 @@ module Stoplight
325
328
  end
326
329
  end
327
330
 
331
+ # @param config [Stoplight::Domain::Config]
332
+ # @return [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockToken, nil]
333
+ def acquire_recovery_lock(config)
334
+ recovery_lock_store.acquire_lock(config.name)
335
+ end
336
+
337
+ # @param lock [Stoplight::Infrastructure::DataStore::Redis::RecoveryLockToken]
338
+ # @return [void]
339
+ def release_recovery_lock(lock)
340
+ recovery_lock_store.release_lock(lock)
341
+ end
342
+
328
343
  # Transitions to GREEN state and ensures only one notification
329
344
  #
330
345
  # @param config [Stoplight::Domain::Config] The light configuration
@@ -333,13 +348,11 @@ module Stoplight
333
348
  current_ts = current_time.to_f
334
349
  meta_key = metadata_key(config)
335
350
 
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
351
+ became_green = scripting.call(
352
+ :transition_to_green,
353
+ args: [current_ts],
354
+ keys: [meta_key]
355
+ )
343
356
  became_green == 1
344
357
  end
345
358
 
@@ -351,13 +364,11 @@ module Stoplight
351
364
  current_ts = current_time.to_f
352
365
  meta_key = metadata_key(config)
353
366
 
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
367
+ became_yellow = scripting.call(
368
+ :transition_to_yellow,
369
+ args: [current_ts],
370
+ keys: [meta_key]
371
+ )
361
372
  became_yellow == 1
362
373
  end
363
374
 
@@ -370,13 +381,11 @@ module Stoplight
370
381
  meta_key = metadata_key(config)
371
382
  recovery_scheduled_after_ts = current_ts + config.cool_off_time
372
383
 
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
384
+ became_red = scripting.call(
385
+ :transition_to_red,
386
+ args: [current_ts, recovery_scheduled_after_ts],
387
+ keys: [meta_key]
388
+ )
380
389
 
381
390
  became_red == 1
382
391
  end
@@ -384,9 +393,11 @@ module Stoplight
384
393
  # Removes all traces of a light from Redis metadata (metrics will expire by TTL).
385
394
  #
386
395
  # @param config [Stoplight::Domain::Config] The light configuration.
387
- # @return [Integer] number of keys removed
396
+ # @return [void]
388
397
  def delete_light(config)
389
- @redis.then { |client| client.del(metadata_key(config)) }
398
+ @redis.then do |client|
399
+ client.del(metadata_key(config), recovery_metrics_key(config))
400
+ end
390
401
  end
391
402
 
392
403
  # @param failure_json [String, nil]
@@ -464,12 +475,8 @@ module Stoplight
464
475
  self.class.bucket_key(config.name, metric: "failure", time:)
465
476
  end
466
477
 
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:)
478
+ private def recovery_metrics_key(config)
479
+ key("recovery_metrics", config.name)
473
480
  end
474
481
 
475
482
  private def metadata_key(config)
@@ -508,42 +515,6 @@ module Stoplight
508
515
  rand <= probability
509
516
  end
510
517
 
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
518
  private def current_time
548
519
  Time.now
549
520
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Notifier
6
+ # A wrapper around a notifier that provides fail-safe mechanisms using a
7
+ # circuit breaker. It ensures that a notification can gracefully
8
+ # handle failures.
9
+ #
10
+ # @api private
11
+ class FailSafe < Domain::StateTransitionNotifier
12
+ # @!attribute [r] notifier
13
+ # @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
14
+ attr_reader :notifier
15
+
16
+ # @!attribute [r] error_notifier
17
+ # @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
18
+ attr_reader :error_notifier
19
+
20
+ # Initializes a new instance of the +FailSafe+ class.
21
+ #
22
+ # @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
23
+ # @param error_notifier [Proc] called when wrapped data store fails
24
+ def initialize(notifier:, error_notifier:)
25
+ @notifier = notifier
26
+ @error_notifier = error_notifier
27
+ end
28
+
29
+ # Sends a notification using the wrapped notifier with fail-safe mechanisms.
30
+ #
31
+ # @param config [Stoplight::Domain::Config] The light configuration.
32
+ # @param from_color [String] The initial color of the light.
33
+ # @param to_color [String] The target color of the light.
34
+ # @param error [Exception, nil] An optional error to include in the notification.
35
+ # @return [void]
36
+ def notify(config, from_color, to_color, error = nil)
37
+ fallback = proc do |exception|
38
+ error_notifier.call(exception) if exception
39
+ nil
40
+ end
41
+
42
+ circuit_breaker.run(fallback) do
43
+ notifier.notify(config, from_color, to_color, error)
44
+ end
45
+ end
46
+
47
+ # @return [Boolean]
48
+ def ==(other)
49
+ other.is_a?(self.class) && notifier == other.notifier
50
+ end
51
+
52
+ # @return [Stoplight::Light] The circuit breaker used to handle failures.
53
+ private def circuit_breaker
54
+ @circuit_breaker ||= Stoplight.system_light(
55
+ "stoplight:notifier:fail_safe:#{notifier.class.name}",
56
+ notifiers: []
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges Domain::Storage::Metrics to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the metrics abstraction to be introduced
9
+ # without breaking existing data store implementations. It delegates all
10
+ # operations to the data store's original methods.
11
+ #
12
+ # This class will be removed in a future versions once all data stores
13
+ # have native metrics implementations.
14
+ #
15
+ # @example Creating metrics for a circuit
16
+ # metrics = CompatibilityMetrics.new(
17
+ # data_store: redis_store,
18
+ # config: config
19
+ # )
20
+ # metrics.record_success
21
+ #
22
+ # @see Stoplight::Domain::Storage::Metrics
23
+ class CompatibilityMetrics < Domain::Storage::Metrics
24
+ private attr_reader :data_store
25
+ private attr_reader :config
26
+
27
+ # @param data_store [Stoplight::Domain::DataStore]
28
+ # @param config [Stoplight::Domain::Config]
29
+ def initialize(data_store:, config:)
30
+ @data_store = data_store
31
+ @config = config
32
+ end
33
+
34
+ def metrics_snapshot = data_store.get_metrics(config)
35
+
36
+ # @return [void]
37
+ def record_success = data_store.record_success(config)
38
+
39
+ # @param error [StandardError]
40
+ # @return [void]
41
+ def record_failure(error) = data_store.record_failure(config, error)
42
+
43
+ # @return [void]
44
+ def clear = data_store.clear_metrics(config)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges +Domain::Storage::RecoveryLock+ to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the recovery lock abstraction to be
9
+ # introduced without breaking existing data store implementations. It
10
+ # delegates all lock operations to the data store's original methods.
11
+ #
12
+ # This adapter will be removed in a future versions once all
13
+ # data stores have native recovery lock implementations.
14
+ #
15
+ # @see Stoplight::Domain::Storage::RecoveryLock
16
+ class CompatibilityRecoveryLock < Domain::Storage::RecoveryLock
17
+ private attr_reader :data_store
18
+ private attr_reader :config
19
+
20
+ # @param data_store [Stoplight::Domain::DataStore]
21
+ # @param config [Stoplight::Domain::Config]
22
+ def initialize(data_store:, config:)
23
+ @data_store = data_store
24
+ @config = config
25
+ end
26
+
27
+ # @return [Stoplight::Domain::RecoveryLockToken, nil]
28
+ def acquire_lock = data_store.acquire_recovery_lock(config)
29
+
30
+ # @param lock [Stoplight::Domain::LockToken]
31
+ # @return [void]
32
+ def release_lock(lock) = data_store.release_recovery_lock(lock)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # When a circuit is RED (open), Stoplight periodically sends "recovery probes"
7
+ # to test whether the protected service has recovered. These test requests have
8
+ # different semantics than normal requests and their metrics are tracked separately.
9
+ #
10
+ # Like +CompatibilityMetrics+, this adapter will be replaced with purpose-built
11
+ # recovery metrics implementations (e.g., +ConsecutiveSuccessMetrics+) once the
12
+ # metrics extraction is complete.
13
+ #
14
+ # @example Recovery probe flow
15
+ # # Circuit is RED, start probing
16
+ # recovery_metrics = CompatibilityRecoveryMetrics.new(
17
+ # data_store: redis_store,
18
+ # config: circuit_config
19
+ # )
20
+ #
21
+ # recovery_metrics.record_success
22
+ # recovery_metrics.metrics_snapshot # => 1 success, 0 failures
23
+ #
24
+ # @see Stoplight::Domain::Storage::Metrics
25
+ class CompatibilityRecoveryMetrics < Domain::Storage::Metrics
26
+ private attr_reader :data_store
27
+ private attr_reader :config
28
+
29
+ # @param data_store [Stoplight::Domain::DataStore]
30
+ # @param config [Stoplight::Domain::Config]
31
+ def initialize(data_store:, config:)
32
+ @data_store = data_store
33
+ @config = config
34
+ end
35
+
36
+ def metrics_snapshot = data_store.get_recovery_metrics(config)
37
+
38
+ # Tracks successful circuit breaker execution
39
+ #
40
+ # @return [void]
41
+ def record_success = data_store.record_recovery_probe_success(config)
42
+
43
+ # Tracks failed circuit breaker execution
44
+ #
45
+ # @param error [StandardError]
46
+ # @return [void]
47
+ def record_failure(error) = data_store.record_recovery_probe_failure(config, error)
48
+
49
+ # Clears metrics
50
+ # @return [void]
51
+ def clear = data_store.clear_recovery_metrics(config)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges Domain::Storage::State to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the state abstraction to be introduced
9
+ # without breaking existing data store implementations. It delegates all
10
+ # state operations to the data store's original methods.
11
+ #
12
+ # This adapter will be removed in a future versions once all
13
+ # data stores have native state storage implementations.
14
+ #
15
+ # @example Creating state storage for a circuit
16
+ # state = CompatibilityState.new(
17
+ # data_store: redis_store,
18
+ # config: circuit_config
19
+ # )
20
+ # state.set_state(State::LOCKED_RED)
21
+ # snapshot = state.state_snapshot
22
+ #
23
+ class CompatibilityState < Domain::Storage::State
24
+ # @!attribute data_store
25
+ # @return [Stoplight::Domain::DataStore]
26
+ private attr_reader :data_store
27
+
28
+ # @!attribute config
29
+ # @return [Stoplight::Domain::Config]
30
+ private attr_reader :config
31
+
32
+ # @param data_store [Stoplight::Domain::DataStore]
33
+ # @param config [Stoplight::Domain::Config]
34
+ def initialize(data_store:, config:)
35
+ @data_store = data_store
36
+ @config = config
37
+ end
38
+
39
+ # @return [Stoplight::Domain::StateSnapshot]
40
+ def state_snapshot = data_store.get_state_snapshot(config)
41
+
42
+ # @param state [String]
43
+ # @return [String]
44
+ def set_state(state) = data_store.set_state(config, state)
45
+
46
+ # @param color [String]
47
+ # @return [Boolean]
48
+ def transition_to_color(color) = data_store.transition_to_color(config, color)
49
+
50
+ # @return [void]
51
+ def clear = data_store.delete_light(config)
52
+ end
53
+ end
54
+ end
55
+ end