stoplight 5.7.0 → 5.8.2

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 (235) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/UPGRADING.md +303 -0
  4. data/lib/generators/stoplight/install/install_generator.rb +6 -1
  5. data/lib/stoplight/admin/dependencies.rb +1 -1
  6. data/lib/stoplight/admin/helpers.rb +20 -4
  7. data/lib/stoplight/admin/lights_repository/light.rb +22 -6
  8. data/lib/stoplight/admin/lights_repository.rb +6 -5
  9. data/lib/stoplight/admin/views/_card.erb +8 -5
  10. data/lib/stoplight/color.rb +9 -0
  11. data/lib/stoplight/data_store.rb +28 -0
  12. data/lib/stoplight/domain/compatibility_result.rb +7 -7
  13. data/lib/stoplight/domain/config.rb +38 -39
  14. data/lib/stoplight/domain/error_tracking_policy.rb +27 -0
  15. data/lib/stoplight/domain/failure.rb +1 -1
  16. data/lib/stoplight/domain/light/configuration_builder_interface.rb +2 -0
  17. data/lib/stoplight/domain/light.rb +15 -46
  18. data/lib/stoplight/domain/light_info.rb +7 -0
  19. data/lib/stoplight/domain/metrics_snapshot.rb +58 -0
  20. data/lib/stoplight/domain/state_snapshot.rb +29 -23
  21. data/lib/stoplight/domain/storage/recovery_lock_token.rb +15 -0
  22. data/lib/stoplight/domain/strategies/green_run_strategy.rb +18 -26
  23. data/lib/stoplight/domain/strategies/red_run_strategy.rb +9 -12
  24. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +41 -51
  25. data/lib/stoplight/domain/tracker/recovery_probe.rb +16 -33
  26. data/lib/stoplight/domain/tracker/request.rb +12 -31
  27. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +8 -11
  28. data/lib/stoplight/domain/traffic_control/error_rate.rb +19 -15
  29. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +6 -10
  30. data/lib/stoplight/domain/traffic_recovery.rb +3 -4
  31. data/lib/stoplight/error.rb +46 -0
  32. data/lib/stoplight/infrastructure/{data_store/fail_safe.rb → fail_safe/data_store.rb} +39 -51
  33. data/lib/stoplight/infrastructure/fail_safe/storage/metrics.rb +65 -0
  34. data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock.rb +69 -0
  35. data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rb +19 -0
  36. data/lib/stoplight/infrastructure/fail_safe/storage/state.rb +62 -0
  37. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/metrics.rb +2 -2
  38. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/recovery_lock_store.rb +10 -12
  39. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/recovery_lock_token.rb +3 -6
  40. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/sliding_window.rb +21 -26
  41. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/state.rb +3 -3
  42. data/lib/stoplight/infrastructure/{data_store/memory.rb → memory/data_store.rb} +36 -32
  43. data/lib/stoplight/infrastructure/memory/storage/recovery_lock.rb +35 -0
  44. data/lib/stoplight/infrastructure/memory/storage/recovery_metrics.rb +16 -0
  45. data/lib/stoplight/infrastructure/memory/storage/state.rb +155 -0
  46. data/lib/stoplight/infrastructure/memory/storage/unbounded_metrics.rb +103 -0
  47. data/lib/stoplight/infrastructure/memory/storage/window_metrics.rb +101 -0
  48. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +9 -21
  49. data/lib/stoplight/infrastructure/notifier/generic.rb +4 -14
  50. data/lib/stoplight/infrastructure/notifier/io.rb +1 -2
  51. data/lib/stoplight/infrastructure/notifier/logger.rb +1 -2
  52. data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/recovery_lock_store.rb +9 -22
  53. data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/recovery_lock_token.rb +7 -14
  54. data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/scripting.rb +22 -20
  55. data/lib/stoplight/infrastructure/{data_store/redis.rb → redis/data_store.rb} +47 -55
  56. data/lib/stoplight/infrastructure/redis/storage/key_space.rb +51 -0
  57. data/lib/stoplight/infrastructure/redis/storage/metrics.rb +40 -0
  58. data/lib/stoplight/infrastructure/redis/storage/recovery_lock/release_lock.lua +6 -0
  59. data/lib/stoplight/infrastructure/redis/storage/recovery_lock.rb +64 -0
  60. data/lib/stoplight/infrastructure/redis/storage/recovery_metrics.rb +20 -0
  61. data/lib/stoplight/infrastructure/redis/storage/scripting.rb +18 -0
  62. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua +10 -0
  63. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +10 -0
  64. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua +9 -0
  65. data/lib/stoplight/infrastructure/redis/storage/state.rb +141 -0
  66. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_failure.lua +28 -0
  67. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_success.lua +26 -0
  68. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb +123 -0
  69. data/lib/stoplight/infrastructure/redis/storage/window_metrics/metrics_snapshot.lua +26 -0
  70. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_failure.lua +36 -0
  71. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_success.lua +35 -0
  72. data/lib/stoplight/infrastructure/redis/storage/window_metrics.rb +174 -0
  73. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +3 -10
  74. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +8 -11
  75. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +6 -14
  76. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +6 -17
  77. data/lib/stoplight/infrastructure/system_clock.rb +16 -0
  78. data/lib/stoplight/notifier.rb +11 -0
  79. data/lib/stoplight/state.rb +9 -0
  80. data/lib/stoplight/types.rb +29 -0
  81. data/lib/stoplight/undefined.rb +16 -0
  82. data/lib/stoplight/version.rb +1 -1
  83. data/lib/stoplight/wiring/config_compatibility_validator.rb +54 -0
  84. data/lib/stoplight/wiring/configuration_dsl.rb +101 -0
  85. data/lib/stoplight/wiring/data_store_backend.rb +26 -0
  86. data/lib/stoplight/wiring/default.rb +1 -1
  87. data/lib/stoplight/wiring/default_config.rb +21 -0
  88. data/lib/stoplight/wiring/default_configuration.rb +70 -53
  89. data/lib/stoplight/wiring/light_builder.rb +76 -63
  90. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +3 -3
  91. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +4 -4
  92. data/lib/stoplight/wiring/light_factory.rb +78 -52
  93. data/lib/stoplight/wiring/memory/backend.rb +57 -0
  94. data/lib/stoplight/wiring/redis/backend.rb +116 -0
  95. data/lib/stoplight/wiring/storage_set.rb +12 -0
  96. data/lib/stoplight/wiring/storage_set_builder.rb +51 -0
  97. data/lib/stoplight/wiring/system/light_builder.rb +47 -0
  98. data/lib/stoplight/wiring/system/light_factory.rb +64 -0
  99. data/lib/stoplight/wiring/system.rb +129 -0
  100. data/lib/stoplight.rb +196 -25
  101. data/sig/_private/generators/stoplight/install/install_generator.rbs +22 -0
  102. data/sig/_private/stoplight/common/deprecations.rbs +9 -0
  103. data/sig/_private/stoplight/data_store.rbs +6 -0
  104. data/sig/_private/stoplight/domain/compatibility_result.rbs +18 -0
  105. data/sig/_private/stoplight/domain/config.rbs +65 -0
  106. data/sig/_private/stoplight/domain/error_tracking_policy.rbs +14 -0
  107. data/sig/_private/stoplight/domain/failure.rbs +16 -0
  108. data/sig/_private/stoplight/domain/light.rbs +25 -0
  109. data/sig/_private/stoplight/domain/light_info.rbs +19 -0
  110. data/sig/_private/stoplight/domain/metrics_snapshot.rbs +38 -0
  111. data/sig/_private/stoplight/domain/ports/clock.rbs +18 -0
  112. data/sig/_private/stoplight/domain/ports/data_store.rbs +76 -0
  113. data/{lib/stoplight/domain/light_factory.rb → sig/_private/stoplight/domain/ports/light_factory.rbs} +33 -28
  114. data/sig/_private/stoplight/domain/ports/metrics_store.rbs +29 -0
  115. data/sig/_private/stoplight/domain/ports/recovery_lock_store.rbs +52 -0
  116. data/sig/_private/stoplight/domain/ports/recovery_lock_token.rbs +6 -0
  117. data/sig/_private/stoplight/domain/ports/run_strategy.rbs +14 -0
  118. data/sig/_private/stoplight/domain/ports/state_store.rbs +79 -0
  119. data/sig/_private/stoplight/domain/ports/traffic_control.rbs +41 -0
  120. data/sig/_private/stoplight/domain/ports/traffic_recovery.rbs +47 -0
  121. data/sig/_private/stoplight/domain/state_snapshot.rbs +32 -0
  122. data/sig/_private/stoplight/domain/storage/recovery_lock_token.rbs +11 -0
  123. data/sig/_private/stoplight/domain/strategies/green_run_strategy.rbs +17 -0
  124. data/sig/_private/stoplight/domain/strategies/red_run_strategy.rbs +17 -0
  125. data/sig/_private/stoplight/domain/strategies/yellow_run_strategy.rbs +42 -0
  126. data/{lib/stoplight/domain/tracker/base.rb → sig/_private/stoplight/domain/tracker/base.rbs} +0 -4
  127. data/sig/_private/stoplight/domain/tracker/recovery_probe.rbs +25 -0
  128. data/sig/_private/stoplight/domain/tracker/request.rbs +26 -0
  129. data/sig/_private/stoplight/domain/traffic_control/consecutive_errors.rbs +9 -0
  130. data/sig/_private/stoplight/domain/traffic_control/error_rate.rbs +13 -0
  131. data/sig/_private/stoplight/domain/traffic_recovery/consecutive_successes.rbs +9 -0
  132. data/sig/_private/stoplight/domain/traffic_recovery.rbs +9 -0
  133. data/sig/_private/stoplight/infrastructure/fail_safe/data_store.rbs +26 -0
  134. data/sig/_private/stoplight/infrastructure/fail_safe/storage/metrics.rbs +25 -0
  135. data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock.rbs +29 -0
  136. data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rbs +19 -0
  137. data/sig/_private/stoplight/infrastructure/fail_safe/storage/state.rbs +25 -0
  138. data/sig/_private/stoplight/infrastructure/memory/data_store/metrics.rbs +25 -0
  139. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_store.rbs +19 -0
  140. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_token.rbs +17 -0
  141. data/sig/_private/stoplight/infrastructure/memory/data_store/sliding_window.rbs +27 -0
  142. data/sig/_private/stoplight/infrastructure/memory/data_store/state.rbs +17 -0
  143. data/sig/_private/stoplight/infrastructure/memory/data_store.rbs +30 -0
  144. data/sig/_private/stoplight/infrastructure/memory/storage/recovery_lock.rbs +15 -0
  145. data/sig/_private/stoplight/infrastructure/memory/storage/recovery_metrics.rbs +10 -0
  146. data/sig/_private/stoplight/infrastructure/memory/storage/state.rbs +28 -0
  147. data/sig/_private/stoplight/infrastructure/memory/storage/unbounded_metrics.rbs +25 -0
  148. data/sig/_private/stoplight/infrastructure/memory/storage/window_metrics.rbs +26 -0
  149. data/sig/_private/stoplight/infrastructure/notifier/fail_safe.rbs +17 -0
  150. data/sig/_private/stoplight/infrastructure/notifier/generic.rbs +18 -0
  151. data/sig/_private/stoplight/infrastructure/notifier/io.rbs +14 -0
  152. data/sig/_private/stoplight/infrastructure/notifier/logger.rbs +14 -0
  153. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_store.rbs +24 -0
  154. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_token.rbs +21 -0
  155. data/sig/_private/stoplight/infrastructure/redis/data_store/scripting.rbs +34 -0
  156. data/sig/_private/stoplight/infrastructure/redis/data_store.rbs +67 -0
  157. data/sig/_private/stoplight/infrastructure/redis/storage/key_space.rbs +19 -0
  158. data/sig/_private/stoplight/infrastructure/redis/storage/metrics.rbs +17 -0
  159. data/sig/_private/stoplight/infrastructure/redis/storage/recovery_lock.rbs +26 -0
  160. data/sig/_private/stoplight/infrastructure/redis/storage/recovery_metrics.rbs +10 -0
  161. data/sig/_private/stoplight/infrastructure/redis/storage/scripting.rbs +13 -0
  162. data/sig/_private/stoplight/infrastructure/redis/storage/state.rbs +32 -0
  163. data/sig/_private/stoplight/infrastructure/redis/storage/unbounded_metrics.rbs +21 -0
  164. data/sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs +34 -0
  165. data/sig/_private/stoplight/infrastructure/storage/compatibility_metrics.rbs +17 -0
  166. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_lock.rbs +13 -0
  167. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_metrics.rbs +14 -0
  168. data/sig/_private/stoplight/infrastructure/storage/compatibility_state.rbs +14 -0
  169. data/sig/_private/stoplight/infrastructure/system_clock.rbs +7 -0
  170. data/sig/_private/stoplight/system/light_builder.rbs +23 -0
  171. data/sig/_private/stoplight/system/light_factory.rbs +17 -0
  172. data/sig/_private/stoplight/types.rbs +6 -0
  173. data/sig/_private/stoplight/wiring/config_compatibility_validator.rbs +19 -0
  174. data/sig/_private/stoplight/wiring/configuration_dsl.rbs +43 -0
  175. data/sig/_private/stoplight/wiring/data_store_backend.rbs +11 -0
  176. data/sig/_private/stoplight/wiring/default.rbs +26 -0
  177. data/{lib/stoplight/wiring/data_store/memory.rb → sig/_private/stoplight/wiring/default_config.rbs} +1 -4
  178. data/sig/_private/stoplight/wiring/default_configuration.rbs +29 -0
  179. data/sig/_private/stoplight/wiring/light_builder.rbs +48 -0
  180. data/sig/_private/stoplight/wiring/light_factory/traffic_control_dsl.rbs +7 -0
  181. data/sig/_private/stoplight/wiring/light_factory/traffic_recovery_dsl.rbs +7 -0
  182. data/sig/_private/stoplight/wiring/light_factory.rbs +16 -0
  183. data/sig/_private/stoplight/wiring/memory/backend.rbs +26 -0
  184. data/sig/_private/stoplight/wiring/notifier_factory.rbs +10 -0
  185. data/sig/_private/stoplight/wiring/redis/backend.rbs +38 -0
  186. data/sig/_private/stoplight/wiring/storage_set.rbs +38 -0
  187. data/sig/_private/stoplight/wiring/storage_set_builder.rbs +15 -0
  188. data/sig/_private/stoplight/wiring/system.rbs +15 -0
  189. data/sig/_private/stoplight.rbs +48 -0
  190. data/sig/stoplight/color.rbs +7 -0
  191. data/sig/stoplight/data_store.rbs +19 -0
  192. data/sig/stoplight/error.rbs +20 -0
  193. data/sig/stoplight/notifier.rbs +11 -0
  194. data/sig/stoplight/ports/configuration.rbs +19 -0
  195. data/sig/stoplight/ports/exception_matcher.rbs +8 -0
  196. data/sig/stoplight/ports/light.rbs +12 -0
  197. data/sig/stoplight/ports/light_info.rbs +5 -0
  198. data/sig/stoplight/ports/state_transition_notifier.rbs +15 -0
  199. data/sig/stoplight/ports/system.rbs +21 -0
  200. data/sig/stoplight/state.rbs +7 -0
  201. data/sig/stoplight/undefined.rbs +9 -0
  202. data/sig/stoplight/version.rbs +3 -0
  203. data/sig/stoplight.rbs +66 -0
  204. metadata +175 -47
  205. data/lib/stoplight/domain/color.rb +0 -11
  206. data/lib/stoplight/domain/data_store.rb +0 -146
  207. data/lib/stoplight/domain/error.rb +0 -42
  208. data/lib/stoplight/domain/metrics.rb +0 -64
  209. data/lib/stoplight/domain/recovery_lock_token.rb +0 -15
  210. data/lib/stoplight/domain/state.rb +0 -11
  211. data/lib/stoplight/domain/state_transition_notifier.rb +0 -25
  212. data/lib/stoplight/domain/storage/metrics.rb +0 -42
  213. data/lib/stoplight/domain/storage/recovery_lock.rb +0 -56
  214. data/lib/stoplight/domain/storage/state.rb +0 -87
  215. data/lib/stoplight/domain/strategies/run_strategy.rb +0 -22
  216. data/lib/stoplight/domain/traffic_control/base.rb +0 -74
  217. data/lib/stoplight/domain/traffic_recovery/base.rb +0 -79
  218. data/lib/stoplight/wiring/data_store/base.rb +0 -11
  219. data/lib/stoplight/wiring/data_store/redis.rb +0 -25
  220. data/lib/stoplight/wiring/default_factory_builder.rb +0 -25
  221. data/lib/stoplight/wiring/light/default_config.rb +0 -18
  222. data/lib/stoplight/wiring/light/system_config.rb +0 -11
  223. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +0 -55
  224. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +0 -71
  225. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +0 -72
  226. data/lib/stoplight/wiring/public_api.rb +0 -29
  227. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/get_metrics.lua +0 -0
  228. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_failure.lua +0 -0
  229. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_recovery_probe_failure.lua +0 -0
  230. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_recovery_probe_success.lua +0 -0
  231. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_success.lua +0 -0
  232. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/release_lock.lua +0 -0
  233. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_green.lua +0 -0
  234. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_red.lua +0 -0
  235. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_yellow.lua +0 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module FailSafe
6
+ module Storage
7
+ # A wrapper around a store that provides fail-safe mechanisms using a
8
+ # circuit breaker. It ensures that operations on the store can gracefully
9
+ # handle failures by falling back to default values when necessary.
10
+ #
11
+ # @api private
12
+ class Metrics
13
+ # The underlying primary store being used
14
+ attr_reader :primary_store
15
+ attr_reader :error_notifier
16
+ # The fallback store used when the primary fails.
17
+ attr_reader :failover_store
18
+
19
+ def initialize(primary_store:, error_notifier:, failover_store:, circuit_breaker:)
20
+ @primary_store = primary_store
21
+ @error_notifier = error_notifier
22
+ @failover_store = failover_store
23
+ @circuit_breaker = circuit_breaker
24
+ end
25
+
26
+ def metrics_snapshot
27
+ circuit_breaker.run(fallback { failover_store.metrics_snapshot }) do
28
+ primary_store.metrics_snapshot
29
+ end
30
+ end
31
+
32
+ def record_success
33
+ circuit_breaker.run(fallback { failover_store.record_success }) do
34
+ primary_store.record_success
35
+ end
36
+ end
37
+
38
+ def record_failure(exception)
39
+ circuit_breaker.run(fallback { failover_store.record_failure(exception) }) do
40
+ primary_store.record_failure(exception)
41
+ end
42
+ end
43
+
44
+ def clear
45
+ circuit_breaker.run(fallback { failover_store.clear }) do
46
+ primary_store.clear
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # The circuit breaker used to handle store failures.
53
+ attr_reader :circuit_breaker
54
+
55
+ def fallback(&fallback)
56
+ ->(error) {
57
+ error_notifier.call(error) if error
58
+ fallback.call
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Stoplight
6
+ module Infrastructure
7
+ module FailSafe
8
+ module Storage
9
+ # A wrapper around a store that provides fail-safe mechanisms using a
10
+ # circuit breaker. It ensures that operations on the store can gracefully
11
+ # handle failures by falling back to default values when necessary.
12
+ #
13
+ # @api private
14
+ class RecoveryLock
15
+ # The underlying primary store being used
16
+ attr_reader :primary_store
17
+ attr_reader :error_notifier
18
+ # The fallback store used when the primary fails.
19
+ attr_reader :failover_store
20
+
21
+ def initialize(primary_store:, error_notifier:, failover_store:, circuit_breaker:)
22
+ @primary_store = primary_store
23
+ @error_notifier = error_notifier
24
+ @failover_store = failover_store
25
+ @circuit_breaker = circuit_breaker
26
+ end
27
+
28
+ def acquire_lock
29
+ fallback = ->(error) {
30
+ error_notifier.call(error) if error
31
+ wrap_token(:failover, failover_store.acquire_lock)
32
+ }
33
+ circuit_breaker.run(fallback) do
34
+ wrap_token(:primary, primary_store.acquire_lock)
35
+ end
36
+ end
37
+
38
+ # Routes release to correct store based on token type.
39
+ # Redis tokens release via primary (with error notification on failure).
40
+ # Memory tokens release via failover directly.
41
+ #
42
+ def release_lock(recovery_lock_token)
43
+ case recovery_lock_token.origin
44
+ in :primary
45
+ fallback = ->(error) {
46
+ error_notifier.call(error) if error
47
+ }
48
+
49
+ circuit_breaker.run(fallback) do
50
+ primary_store.release_lock(recovery_lock_token.underlying_token)
51
+ end
52
+ in :failover
53
+ failover_store.release_lock(recovery_lock_token.underlying_token)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # The circuit breaker used to handle store failures.
60
+ attr_reader :circuit_breaker
61
+
62
+ def wrap_token(origin, token)
63
+ RecoveryLockToken.new(origin:, underlying_token: token) if token
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module FailSafe
6
+ module Storage
7
+ class RecoveryLockToken
8
+ attr_reader :underlying_token
9
+ attr_reader :origin
10
+
11
+ def initialize(underlying_token:, origin:)
12
+ @underlying_token = underlying_token
13
+ @origin = origin
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module FailSafe
6
+ module Storage
7
+ # A wrapper around a store that provides fail-safe mechanisms using a
8
+ # circuit breaker. It ensures that operations on the store can gracefully
9
+ # handle failures by falling back to default values when necessary.
10
+ #
11
+ # @api private
12
+ class State
13
+ attr_reader :primary_store
14
+ attr_reader :error_notifier
15
+ attr_reader :failover_store
16
+ attr_reader :circuit_breaker
17
+
18
+ def initialize(primary_store:, error_notifier:, failover_store:, circuit_breaker:)
19
+ @primary_store = primary_store
20
+ @error_notifier = error_notifier
21
+ @failover_store = failover_store
22
+ @circuit_breaker = circuit_breaker
23
+ end
24
+
25
+ def set_state(state)
26
+ circuit_breaker.run(fallback { failover_store.set_state(state) }) do
27
+ primary_store.set_state(state)
28
+ end
29
+ end
30
+
31
+ # @return [Stoplight::Domain::StateSnapshot]
32
+ def state_snapshot
33
+ circuit_breaker.run(fallback { failover_store.state_snapshot }) do
34
+ primary_store.state_snapshot
35
+ end
36
+ end
37
+
38
+ # @param color [String]
39
+ # @return [Boolean]
40
+ def transition_to_color(color)
41
+ circuit_breaker.run(fallback { failover_store.transition_to_color(color) }) do
42
+ primary_store.transition_to_color(color)
43
+ end
44
+ end
45
+
46
+ def clear
47
+ circuit_breaker.run(fallback { failover_store.clear }) do
48
+ primary_store.clear
49
+ end
50
+ end
51
+
52
+ private def fallback(&fallback)
53
+ ->(error) {
54
+ error_notifier.call(error) if error
55
+ fallback.call
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Stoplight
4
4
  module Infrastructure
5
- module DataStore
6
- class Memory
5
+ module Memory
6
+ class DataStore
7
7
  class Metrics
8
8
  attr_accessor :consecutive_errors
9
9
  attr_accessor :consecutive_successes
@@ -4,8 +4,8 @@ require "concurrent/map"
4
4
 
5
5
  module Stoplight
6
6
  module Infrastructure
7
- module DataStore
8
- class Memory
7
+ module Memory
8
+ class DataStore
9
9
  # Process-local recovery lock using Ruby's Thread::Mutex.
10
10
  #
11
11
  # This only serializes recovery within a single Ruby process.
@@ -17,31 +17,29 @@ module Stoplight
17
17
  # - Mutexes persist for process lifetime (never GC'd)
18
18
  #
19
19
  class RecoveryLockStore
20
- # @!attribute locks
21
- # Stores one mutex per unique light_name for the lifetime of the process.
22
- # Mutexes are never garbage collected.
23
- # @return [Concurrent::Map<Thread::Mutex>]
24
- private attr_reader :locks
25
-
26
20
  def initialize
27
21
  @locks = Concurrent::Map.new
28
22
  end
29
23
 
30
24
  # @param light_name [String]
31
- # @return [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockToken, nil]
25
+ # @return [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockToken, nil]
32
26
  def acquire_lock(light_name)
33
27
  lock = lock_for(light_name)
34
28
  RecoveryLockToken.new(light_name:) if lock.try_lock
35
29
  end
36
30
 
37
- # @param recovery_lock_token [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockToken]
31
+ # @param recovery_lock_token [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockToken]
38
32
  # @return [void]
39
33
  def release_lock(recovery_lock_token)
40
34
  lock_for(recovery_lock_token.light_name).unlock
41
35
  end
42
36
 
43
- # @param light_name [String]
44
- # @return [Thread::Mutex]
37
+ private
38
+
39
+ # Stores one mutex per unique light_name for the lifetime of the process.
40
+ # Mutexes are never garbage collected.
41
+ attr_reader :locks
42
+
45
43
  private def lock_for(light_name)
46
44
  locks.compute_if_absent(light_name) do
47
45
  Thread::Mutex.new
@@ -2,14 +2,11 @@
2
2
 
3
3
  module Stoplight
4
4
  module Infrastructure
5
- module DataStore
6
- class Memory
7
- class RecoveryLockToken < Domain::RecoveryLockToken
8
- # @!attribute light_name
9
- # @return [String]
5
+ module Memory
6
+ class DataStore
7
+ class RecoveryLockToken
10
8
  attr_reader :light_name
11
9
 
12
- # @param light_name [String]
13
10
  def initialize(light_name:)
14
11
  @light_name = light_name
15
12
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Stoplight
4
4
  module Infrastructure
5
- module DataStore
6
- class Memory < Domain::DataStore
5
+ module Memory
6
+ class DataStore
7
7
  # Hash-based sliding window for O(1) amortized operations.
8
8
  #
9
9
  # Maintains a running sum and stores per-second counts in a Hash. Ruby's Hash
@@ -17,17 +17,10 @@ module Stoplight
17
17
  # @note Not thread-safe; synchronization must be handled externally
18
18
  # @api private
19
19
  class SlidingWindow
20
- # @!attribute buckets
21
- # @return [Hash<Integer, Integer>] A hash mapping time buckets to their counts
22
- private attr_reader :buckets
23
-
24
- # @!attribute running_sum
25
- # @return [Integer] The running sum of all increments in the current window
26
- private attr_accessor :running_sum
27
-
28
- def initialize
20
+ def initialize(clock:)
29
21
  @buckets = Hash.new { |buckets, bucket| buckets[bucket] = 0 }
30
22
  @running_sum = 0
23
+ @clock = clock
31
24
  end
32
25
 
33
26
  # Increment the count at a given timestamp
@@ -36,14 +29,24 @@ module Stoplight
36
29
  self.running_sum += 1
37
30
  end
38
31
 
39
- # @param window_start [Time]
40
- # @return [Integer]
41
32
  def sum_in_window(window_start)
42
33
  slide_window!(window_start)
43
34
  self.running_sum
44
35
  end
45
36
 
46
- private def slide_window!(window_start)
37
+ def inspect
38
+ "#<#{self.class.name} #{buckets}>"
39
+ end
40
+
41
+ private
42
+
43
+ # A hash mapping time buckets to their counts
44
+ attr_reader :buckets
45
+ # The running sum of all increments in the current window
46
+ attr_accessor :running_sum
47
+ attr_reader :clock
48
+
49
+ def slide_window!(window_start)
47
50
  window_start_ts = window_start.to_i
48
51
 
49
52
  loop do
@@ -51,27 +54,19 @@ module Stoplight
51
54
  if timestamp.nil? || timestamp >= window_start_ts
52
55
  break
53
56
  else
54
- self.running_sum -= sum
57
+ self.running_sum -= sum.to_i
55
58
  buckets.shift
56
59
  end
57
60
  end
58
61
  end
59
62
 
60
- private def current_bucket
61
- bucket_for_time(current_time)
63
+ def current_bucket
64
+ bucket_for_time(clock.current_time)
62
65
  end
63
66
 
64
- private def bucket_for_time(time)
67
+ def bucket_for_time(time)
65
68
  time.to_i
66
69
  end
67
-
68
- private def current_time
69
- Time.now
70
- end
71
-
72
- def inspect
73
- "#<#{self.class.name} #{buckets}>"
74
- end
75
70
  end
76
71
  end
77
72
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Stoplight
4
4
  module Infrastructure
5
- module DataStore
6
- class Memory
5
+ module Memory
6
+ class DataStore
7
7
  class State
8
8
  attr_accessor :recovered_at
9
9
  attr_accessor :locked_state
@@ -12,7 +12,7 @@ module Stoplight
12
12
  attr_accessor :breached_at
13
13
 
14
14
  def initialize
15
- @locked_state = Domain::State::UNLOCKED
15
+ @locked_state = Stoplight::State::UNLOCKED
16
16
  end
17
17
  end
18
18
  end
@@ -4,23 +4,30 @@ require "monitor"
4
4
 
5
5
  module Stoplight
6
6
  module Infrastructure
7
- module DataStore
8
- # @see +Domain::DataStore+
9
- class Memory < Domain::DataStore
7
+ module Memory
8
+ # steep:ignore:start
9
+ # @see +Domain::_DataStore+
10
+ class DataStore
10
11
  include MonitorMixin
11
12
 
12
13
  KEY_SEPARATOR = ":"
13
14
 
14
15
  # @!attribute recovery_lock_store
15
- # @return [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockStore]
16
+ # @return [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockStore]
16
17
  # @api private
17
18
  private attr_reader :recovery_lock_store
18
19
 
19
- # @param recovery_lock_store [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockStore]
20
- def initialize(recovery_lock_store:)
20
+ # @!attribute clock
21
+ # @return [Stoplight::Domain::_Clock]
22
+ private attr_reader :clock
23
+
24
+ # @param recovery_lock_store [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockStore]
25
+ # @param clock [Stoplight::Domain::_Clock]
26
+ def initialize(recovery_lock_store:, clock:)
27
+ @clock = clock
21
28
  @recovery_lock_store = recovery_lock_store
22
- @errors = Hash.new { |errors, light_name| errors[light_name] = SlidingWindow.new }
23
- @successes = Hash.new { |successes, light_name| successes[light_name] = SlidingWindow.new }
29
+ @errors = Hash.new { |errors, light_name| errors[light_name] = SlidingWindow.new(clock:) }
30
+ @successes = Hash.new { |successes, light_name| successes[light_name] = SlidingWindow.new(clock:) }
24
31
  @metrics = Hash.new { |metrics, light_name| metrics[light_name] = Metrics.new }
25
32
 
26
33
  @recovery_metrics = Hash.new { |metrics, light_name| metrics[light_name] = Metrics.new }
@@ -36,12 +43,12 @@ module Stoplight
36
43
  end
37
44
 
38
45
  # @param config [Stoplight::Domain::Config]
39
- # @return [Stoplight::Domain::Metrics]
46
+ # @return [Stoplight::Domain::MetricsSnapshot]
40
47
  def get_metrics(config)
41
48
  light_name = config.name
42
49
 
43
50
  synchronize do
44
- current_time = self.current_time
51
+ current_time = clock.current_time
45
52
  window_start = if config.window_size
46
53
  (current_time - config.window_size)
47
54
  else
@@ -55,7 +62,7 @@ module Stoplight
55
62
  consecutive_errors = config.window_size ? [metrics.consecutive_errors, errors].min : metrics.consecutive_errors
56
63
  consecutive_successes = config.window_size ? [metrics.consecutive_successes.to_i, successes].min : metrics.consecutive_successes.to_i
57
64
 
58
- Domain::Metrics.new(
65
+ Domain::MetricsSnapshot.new(
59
66
  errors:,
60
67
  successes:,
61
68
  consecutive_errors:,
@@ -66,14 +73,14 @@ module Stoplight
66
73
  end
67
74
  end
68
75
 
69
- # @return [Stoplight::Domain::Metrics]
76
+ # @return [Stoplight::Domain::MetricsSnapshot]
70
77
  def get_recovery_metrics(config)
71
78
  light_name = config.name
72
79
 
73
80
  synchronize do
74
81
  metrics = @recovery_metrics[light_name]
75
82
 
76
- Domain::Metrics.new(
83
+ Domain::MetricsSnapshot.new(
77
84
  errors: nil, successes: nil,
78
85
  consecutive_errors: metrics.consecutive_errors,
79
86
  consecutive_successes: metrics.consecutive_successes,
@@ -86,7 +93,7 @@ module Stoplight
86
93
  # @return [Stoplight::Domain::StateSnapshot]
87
94
  def get_state_snapshot(config)
88
95
  time, state = synchronize do
89
- [current_time, @states[config.name]]
96
+ [clock.current_time, @states[config.name]]
90
97
  end
91
98
 
92
99
  Domain::StateSnapshot.new(
@@ -102,7 +109,7 @@ module Stoplight
102
109
  # @param exception [Exception]
103
110
  # @return [void]
104
111
  def record_failure(config, exception)
105
- current_time = self.current_time
112
+ current_time = clock.current_time
106
113
  light_name = config.name
107
114
  failure = Domain::Failure.from_error(exception, time: current_time)
108
115
 
@@ -124,8 +131,8 @@ module Stoplight
124
131
  light_name = config.name
125
132
  synchronize do
126
133
  if config.window_size
127
- @errors[light_name] = SlidingWindow.new
128
- @successes[light_name] = SlidingWindow.new
134
+ @errors[light_name] = SlidingWindow.new(clock:)
135
+ @successes[light_name] = SlidingWindow.new(clock:)
129
136
  end
130
137
  @metrics[light_name] = Metrics.new
131
138
  end
@@ -141,7 +148,7 @@ module Stoplight
141
148
  # @return [void]
142
149
  def record_success(config)
143
150
  light_name = config.name
144
- current_time = self.current_time
151
+ current_time = clock.current_time
145
152
 
146
153
  synchronize do
147
154
  @successes[light_name].increment if config.window_size
@@ -162,7 +169,7 @@ module Stoplight
162
169
  # @return [void]
163
170
  def record_recovery_probe_failure(config, exception)
164
171
  light_name = config.name
165
- current_time = self.current_time
172
+ current_time = clock.current_time
166
173
  failure = Domain::Failure.from_error(exception, time: current_time)
167
174
 
168
175
  synchronize do
@@ -181,7 +188,7 @@ module Stoplight
181
188
  # @return [void]
182
189
  def record_recovery_probe_success(config)
183
190
  light_name = config.name
184
- current_time = self.current_time
191
+ current_time = clock.current_time
185
192
 
186
193
  synchronize do
187
194
  metrics = @recovery_metrics[light_name]
@@ -232,11 +239,11 @@ module Stoplight
232
239
  # @return [Boolean] true if this is the first instance to detect this transition
233
240
  def transition_to_color(config, color)
234
241
  case color
235
- when Domain::Color::GREEN
242
+ when Color::GREEN
236
243
  transition_to_green(config)
237
- when Domain::Color::YELLOW
244
+ when Color::YELLOW
238
245
  transition_to_yellow(config)
239
- when Domain::Color::RED
246
+ when Color::RED
240
247
  transition_to_red(config)
241
248
  else
242
249
  raise ArgumentError, "Invalid color: #{color}"
@@ -244,12 +251,12 @@ module Stoplight
244
251
  end
245
252
 
246
253
  # @param config [Stoplight::Domain::Config]
247
- # @return [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockToken, nil]
254
+ # @return [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockToken, nil]
248
255
  def acquire_recovery_lock(config)
249
256
  recovery_lock_store.acquire_lock(config.name)
250
257
  end
251
258
 
252
- # @param lock [Stoplight::Infrastructure::DataStore::Memory::RecoveryLockToken]
259
+ # @param lock [Stoplight::Infrastructure::Memory::DataStore::RecoveryLockToken]
253
260
  # @return [void]
254
261
  def release_recovery_lock(lock)
255
262
  recovery_lock_store.release_lock(lock)
@@ -261,7 +268,7 @@ module Stoplight
261
268
  # @return [Boolean] true if this is the first instance to detect this transition
262
269
  private def transition_to_green(config)
263
270
  light_name = config.name
264
- current_time = self.current_time
271
+ current_time = clock.current_time
265
272
 
266
273
  synchronize do
267
274
  state = @states[light_name]
@@ -284,7 +291,7 @@ module Stoplight
284
291
  # @return [Boolean] true if this is the first instance to detect this transition
285
292
  private def transition_to_yellow(config)
286
293
  light_name = config.name
287
- current_time = self.current_time
294
+ current_time = clock.current_time
288
295
 
289
296
  synchronize do
290
297
  state = @states[light_name]
@@ -309,7 +316,7 @@ module Stoplight
309
316
  # @return [Boolean] true if this is the first instance to detect this transition
310
317
  private def transition_to_red(config)
311
318
  light_name = config.name
312
- current_time = self.current_time
319
+ current_time = clock.current_time
313
320
  recovery_scheduled_after = current_time + config.cool_off_time
314
321
 
315
322
  synchronize do
@@ -328,11 +335,8 @@ module Stoplight
328
335
  end
329
336
  end
330
337
  end
331
-
332
- private def current_time
333
- Time.now
334
- end
335
338
  end
339
+ # steep:ignore:end
336
340
  end
337
341
  end
338
342
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Memory
6
+ module Storage
7
+ # Process-local recovery lock using Ruby's Thread::Mutex.
8
+ #
9
+ # This only serializes recovery within a single Ruby process.
10
+ # Multiple processes/servers will NOT coordinate - each process
11
+ # can send probes independently.
12
+ #
13
+ class RecoveryLock
14
+ def initialize
15
+ @lock = Thread::Mutex.new
16
+ end
17
+
18
+ def acquire_lock
19
+ if lock.try_lock
20
+ Domain::Storage::RecoveryLockToken.new
21
+ end
22
+ end
23
+
24
+ def release_lock(_recovery_lock_token)
25
+ lock.unlock
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :lock
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Memory
6
+ module Storage
7
+ # When a circuit is RED (open), Stoplight periodically sends "recovery probes"
8
+ # to test whether the protected service has recovered. These test requests have
9
+ # different semantics than normal requests and their metrics are tracked separately.
10
+ #
11
+ class RecoveryMetrics < UnboundedMetrics
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end