stoplight 5.6.0 → 5.8.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 (238) 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 +26 -5
  7. data/lib/stoplight/admin/lights_repository/light.rb +22 -6
  8. data/lib/stoplight/admin/lights_repository.rb +20 -16
  9. data/lib/stoplight/admin/views/_card.erb +8 -5
  10. data/lib/stoplight/admin.rb +2 -1
  11. data/lib/stoplight/color.rb +9 -0
  12. data/lib/stoplight/common/deprecations.rb +11 -0
  13. data/lib/stoplight/data_store.rb +28 -0
  14. data/lib/stoplight/domain/compatibility_result.rb +7 -7
  15. data/lib/stoplight/domain/config.rb +38 -35
  16. data/lib/stoplight/domain/error_tracking_policy.rb +27 -0
  17. data/lib/stoplight/domain/failure.rb +1 -1
  18. data/lib/stoplight/domain/light/configuration_builder_interface.rb +122 -16
  19. data/lib/stoplight/domain/light.rb +44 -64
  20. data/lib/stoplight/domain/light_info.rb +7 -0
  21. data/lib/stoplight/domain/metrics_snapshot.rb +58 -0
  22. data/lib/stoplight/domain/state_snapshot.rb +29 -23
  23. data/lib/stoplight/domain/storage/recovery_lock_token.rb +15 -0
  24. data/lib/stoplight/domain/strategies/green_run_strategy.rb +18 -26
  25. data/lib/stoplight/domain/strategies/red_run_strategy.rb +9 -12
  26. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +74 -58
  27. data/lib/stoplight/domain/tracker/recovery_probe.rb +27 -43
  28. data/lib/stoplight/domain/tracker/request.rb +24 -39
  29. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +8 -11
  30. data/lib/stoplight/domain/traffic_control/error_rate.rb +19 -15
  31. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +8 -18
  32. data/lib/stoplight/domain/traffic_recovery.rb +3 -5
  33. data/lib/stoplight/error.rb +46 -0
  34. data/lib/stoplight/infrastructure/fail_safe/data_store.rb +152 -0
  35. data/lib/stoplight/infrastructure/fail_safe/storage/metrics.rb +65 -0
  36. data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock.rb +69 -0
  37. data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rb +19 -0
  38. data/lib/stoplight/infrastructure/fail_safe/storage/state.rb +62 -0
  39. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/metrics.rb +2 -2
  40. data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_store.rb +52 -0
  41. data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_token.rb +17 -0
  42. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/sliding_window.rb +21 -26
  43. data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/state.rb +3 -3
  44. data/lib/stoplight/infrastructure/{data_store/memory.rb → memory/data_store.rb} +90 -57
  45. data/lib/stoplight/infrastructure/memory/storage/recovery_lock.rb +35 -0
  46. data/lib/stoplight/infrastructure/memory/storage/recovery_metrics.rb +16 -0
  47. data/lib/stoplight/infrastructure/memory/storage/state.rb +155 -0
  48. data/lib/stoplight/infrastructure/memory/storage/unbounded_metrics.rb +103 -0
  49. data/lib/stoplight/infrastructure/memory/storage/window_metrics.rb +101 -0
  50. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +50 -0
  51. data/lib/stoplight/infrastructure/notifier/generic.rb +4 -14
  52. data/lib/stoplight/infrastructure/notifier/io.rb +1 -2
  53. data/lib/stoplight/infrastructure/notifier/logger.rb +1 -2
  54. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_failure.lua +27 -0
  55. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_success.lua +23 -0
  56. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/release_lock.lua +6 -0
  57. data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_store.rb +60 -0
  58. data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_token.rb +28 -0
  59. data/lib/stoplight/infrastructure/redis/data_store/scripting.rb +73 -0
  60. data/lib/stoplight/infrastructure/{data_store/redis.rb → redis/data_store.rb} +173 -210
  61. data/lib/stoplight/infrastructure/redis/storage/key_space.rb +51 -0
  62. data/lib/stoplight/infrastructure/redis/storage/metrics.rb +40 -0
  63. data/lib/stoplight/infrastructure/redis/storage/recovery_lock/release_lock.lua +6 -0
  64. data/lib/stoplight/infrastructure/redis/storage/recovery_lock.rb +64 -0
  65. data/lib/stoplight/infrastructure/redis/storage/recovery_metrics.rb +20 -0
  66. data/lib/stoplight/infrastructure/redis/storage/scripting.rb +18 -0
  67. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua +10 -0
  68. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +10 -0
  69. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua +9 -0
  70. data/lib/stoplight/infrastructure/redis/storage/state.rb +141 -0
  71. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_failure.lua +28 -0
  72. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_success.lua +26 -0
  73. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb +123 -0
  74. data/lib/stoplight/infrastructure/redis/storage/window_metrics/metrics_snapshot.lua +26 -0
  75. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_failure.lua +36 -0
  76. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_success.lua +35 -0
  77. data/lib/stoplight/infrastructure/redis/storage/window_metrics.rb +174 -0
  78. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +41 -0
  79. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +33 -0
  80. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +47 -0
  81. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +44 -0
  82. data/lib/stoplight/infrastructure/system_clock.rb +16 -0
  83. data/lib/stoplight/notifier.rb +11 -0
  84. data/lib/stoplight/state.rb +9 -0
  85. data/lib/stoplight/types.rb +29 -0
  86. data/lib/stoplight/undefined.rb +16 -0
  87. data/lib/stoplight/version.rb +1 -1
  88. data/lib/stoplight/wiring/config_compatibility_validator.rb +54 -0
  89. data/lib/stoplight/wiring/configuration_dsl.rb +101 -0
  90. data/lib/stoplight/wiring/data_store_backend.rb +26 -0
  91. data/lib/stoplight/wiring/default.rb +2 -2
  92. data/lib/stoplight/wiring/default_config.rb +21 -0
  93. data/lib/stoplight/wiring/default_configuration.rb +70 -53
  94. data/lib/stoplight/wiring/light_builder.rb +198 -0
  95. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  96. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  97. data/lib/stoplight/wiring/light_factory.rb +74 -135
  98. data/lib/stoplight/wiring/memory/backend.rb +57 -0
  99. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  100. data/lib/stoplight/wiring/redis/backend.rb +116 -0
  101. data/lib/stoplight/wiring/storage_set.rb +12 -0
  102. data/lib/stoplight/wiring/storage_set_builder.rb +51 -0
  103. data/lib/stoplight/wiring/system/light_builder.rb +47 -0
  104. data/lib/stoplight/wiring/system/light_factory.rb +64 -0
  105. data/lib/stoplight/wiring/system.rb +129 -0
  106. data/lib/stoplight.rb +209 -23
  107. data/sig/_private/generators/stoplight/install/install_generator.rbs +22 -0
  108. data/sig/_private/stoplight/common/deprecations.rbs +9 -0
  109. data/sig/_private/stoplight/data_store.rbs +6 -0
  110. data/sig/_private/stoplight/domain/compatibility_result.rbs +18 -0
  111. data/sig/_private/stoplight/domain/config.rbs +65 -0
  112. data/sig/_private/stoplight/domain/error_tracking_policy.rbs +14 -0
  113. data/sig/_private/stoplight/domain/failure.rbs +16 -0
  114. data/sig/_private/stoplight/domain/light.rbs +25 -0
  115. data/sig/_private/stoplight/domain/light_info.rbs +19 -0
  116. data/sig/_private/stoplight/domain/metrics_snapshot.rbs +38 -0
  117. data/sig/_private/stoplight/domain/ports/clock.rbs +18 -0
  118. data/sig/_private/stoplight/domain/ports/data_store.rbs +76 -0
  119. data/{lib/stoplight/domain/light_factory.rb → sig/_private/stoplight/domain/ports/light_factory.rbs} +33 -28
  120. data/sig/_private/stoplight/domain/ports/metrics_store.rbs +29 -0
  121. data/sig/_private/stoplight/domain/ports/recovery_lock_store.rbs +52 -0
  122. data/sig/_private/stoplight/domain/ports/recovery_lock_token.rbs +6 -0
  123. data/sig/_private/stoplight/domain/ports/run_strategy.rbs +14 -0
  124. data/sig/_private/stoplight/domain/ports/state_store.rbs +79 -0
  125. data/sig/_private/stoplight/domain/ports/traffic_control.rbs +41 -0
  126. data/sig/_private/stoplight/domain/ports/traffic_recovery.rbs +47 -0
  127. data/sig/_private/stoplight/domain/state_snapshot.rbs +32 -0
  128. data/sig/_private/stoplight/domain/storage/recovery_lock_token.rbs +11 -0
  129. data/sig/_private/stoplight/domain/strategies/green_run_strategy.rbs +17 -0
  130. data/sig/_private/stoplight/domain/strategies/red_run_strategy.rbs +17 -0
  131. data/sig/_private/stoplight/domain/strategies/yellow_run_strategy.rbs +42 -0
  132. data/sig/_private/stoplight/domain/tracker/base.rbs +8 -0
  133. data/sig/_private/stoplight/domain/tracker/recovery_probe.rbs +25 -0
  134. data/sig/_private/stoplight/domain/tracker/request.rbs +26 -0
  135. data/sig/_private/stoplight/domain/traffic_control/consecutive_errors.rbs +9 -0
  136. data/sig/_private/stoplight/domain/traffic_control/error_rate.rbs +13 -0
  137. data/sig/_private/stoplight/domain/traffic_recovery/consecutive_successes.rbs +9 -0
  138. data/sig/_private/stoplight/domain/traffic_recovery.rbs +9 -0
  139. data/sig/_private/stoplight/infrastructure/fail_safe/data_store.rbs +26 -0
  140. data/sig/_private/stoplight/infrastructure/fail_safe/storage/metrics.rbs +25 -0
  141. data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock.rbs +29 -0
  142. data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rbs +19 -0
  143. data/sig/_private/stoplight/infrastructure/fail_safe/storage/state.rbs +25 -0
  144. data/sig/_private/stoplight/infrastructure/memory/data_store/metrics.rbs +25 -0
  145. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_store.rbs +19 -0
  146. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_token.rbs +17 -0
  147. data/sig/_private/stoplight/infrastructure/memory/data_store/sliding_window.rbs +27 -0
  148. data/sig/_private/stoplight/infrastructure/memory/data_store/state.rbs +17 -0
  149. data/sig/_private/stoplight/infrastructure/memory/data_store.rbs +30 -0
  150. data/sig/_private/stoplight/infrastructure/memory/storage/recovery_lock.rbs +15 -0
  151. data/sig/_private/stoplight/infrastructure/memory/storage/recovery_metrics.rbs +10 -0
  152. data/sig/_private/stoplight/infrastructure/memory/storage/state.rbs +28 -0
  153. data/sig/_private/stoplight/infrastructure/memory/storage/unbounded_metrics.rbs +25 -0
  154. data/sig/_private/stoplight/infrastructure/memory/storage/window_metrics.rbs +26 -0
  155. data/sig/_private/stoplight/infrastructure/notifier/fail_safe.rbs +17 -0
  156. data/sig/_private/stoplight/infrastructure/notifier/generic.rbs +18 -0
  157. data/sig/_private/stoplight/infrastructure/notifier/io.rbs +14 -0
  158. data/sig/_private/stoplight/infrastructure/notifier/logger.rbs +14 -0
  159. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_store.rbs +24 -0
  160. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_token.rbs +21 -0
  161. data/sig/_private/stoplight/infrastructure/redis/data_store/scripting.rbs +34 -0
  162. data/sig/_private/stoplight/infrastructure/redis/data_store.rbs +67 -0
  163. data/sig/_private/stoplight/infrastructure/redis/storage/key_space.rbs +19 -0
  164. data/sig/_private/stoplight/infrastructure/redis/storage/metrics.rbs +17 -0
  165. data/sig/_private/stoplight/infrastructure/redis/storage/recovery_lock.rbs +26 -0
  166. data/sig/_private/stoplight/infrastructure/redis/storage/recovery_metrics.rbs +10 -0
  167. data/sig/_private/stoplight/infrastructure/redis/storage/scripting.rbs +13 -0
  168. data/sig/_private/stoplight/infrastructure/redis/storage/state.rbs +32 -0
  169. data/sig/_private/stoplight/infrastructure/redis/storage/unbounded_metrics.rbs +21 -0
  170. data/sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs +34 -0
  171. data/sig/_private/stoplight/infrastructure/storage/compatibility_metrics.rbs +17 -0
  172. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_lock.rbs +13 -0
  173. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_metrics.rbs +14 -0
  174. data/sig/_private/stoplight/infrastructure/storage/compatibility_state.rbs +14 -0
  175. data/sig/_private/stoplight/infrastructure/system_clock.rbs +7 -0
  176. data/sig/_private/stoplight/system/light_builder.rbs +23 -0
  177. data/sig/_private/stoplight/system/light_factory.rbs +17 -0
  178. data/sig/_private/stoplight/types.rbs +6 -0
  179. data/sig/_private/stoplight/wiring/config_compatibility_validator.rbs +19 -0
  180. data/sig/_private/stoplight/wiring/configuration_dsl.rbs +43 -0
  181. data/sig/_private/stoplight/wiring/data_store_backend.rbs +11 -0
  182. data/sig/_private/stoplight/wiring/default.rbs +26 -0
  183. data/sig/_private/stoplight/wiring/default_config.rbs +7 -0
  184. data/sig/_private/stoplight/wiring/default_configuration.rbs +29 -0
  185. data/sig/_private/stoplight/wiring/light_builder.rbs +48 -0
  186. data/sig/_private/stoplight/wiring/light_factory/traffic_control_dsl.rbs +7 -0
  187. data/sig/_private/stoplight/wiring/light_factory/traffic_recovery_dsl.rbs +7 -0
  188. data/sig/_private/stoplight/wiring/light_factory.rbs +16 -0
  189. data/sig/_private/stoplight/wiring/memory/backend.rbs +26 -0
  190. data/sig/_private/stoplight/wiring/notifier_factory.rbs +10 -0
  191. data/sig/_private/stoplight/wiring/redis/backend.rbs +38 -0
  192. data/sig/_private/stoplight/wiring/storage_set.rbs +38 -0
  193. data/sig/_private/stoplight/wiring/storage_set_builder.rbs +15 -0
  194. data/sig/_private/stoplight/wiring/system.rbs +15 -0
  195. data/sig/_private/stoplight.rbs +48 -0
  196. data/sig/stoplight/color.rbs +7 -0
  197. data/sig/stoplight/data_store.rbs +19 -0
  198. data/sig/stoplight/error.rbs +20 -0
  199. data/sig/stoplight/notifier.rbs +11 -0
  200. data/sig/stoplight/ports/configuration.rbs +19 -0
  201. data/sig/stoplight/ports/exception_matcher.rbs +8 -0
  202. data/sig/stoplight/ports/light.rbs +12 -0
  203. data/sig/stoplight/ports/light_info.rbs +5 -0
  204. data/sig/stoplight/ports/state_transition_notifier.rbs +15 -0
  205. data/sig/stoplight/ports/system.rbs +21 -0
  206. data/sig/stoplight/state.rbs +7 -0
  207. data/sig/stoplight/undefined.rbs +9 -0
  208. data/sig/stoplight/version.rbs +3 -0
  209. data/sig/stoplight.rbs +66 -0
  210. metadata +199 -36
  211. data/lib/stoplight/domain/color.rb +0 -11
  212. data/lib/stoplight/domain/data_store.rb +0 -130
  213. data/lib/stoplight/domain/error.rb +0 -42
  214. data/lib/stoplight/domain/metrics.rb +0 -85
  215. data/lib/stoplight/domain/state.rb +0 -11
  216. data/lib/stoplight/domain/state_transition_notifier.rb +0 -25
  217. data/lib/stoplight/domain/strategies/run_strategy.rb +0 -27
  218. data/lib/stoplight/domain/tracker/base.rb +0 -41
  219. data/lib/stoplight/domain/traffic_control/base.rb +0 -74
  220. data/lib/stoplight/domain/traffic_recovery/base.rb +0 -80
  221. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
  222. data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
  223. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
  224. data/lib/stoplight/wiring/container.rb +0 -80
  225. data/lib/stoplight/wiring/default_factory_builder.rb +0 -25
  226. data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -147
  227. data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
  228. data/lib/stoplight/wiring/light/default_config.rb +0 -18
  229. data/lib/stoplight/wiring/light/system_config.rb +0 -11
  230. data/lib/stoplight/wiring/public_api.rb +0 -28
  231. data/lib/stoplight/wiring/system_container.rb +0 -9
  232. data/lib/stoplight/wiring/system_light_factory.rb +0 -17
  233. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/get_metrics.lua +0 -0
  234. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/record_failure.lua +0 -0
  235. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/record_success.lua +0 -0
  236. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_green.lua +0 -0
  237. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_red.lua +0 -0
  238. /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_yellow.lua +0 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ class NotifierFactory
6
+ class << self
7
+ # Wraps a notifier with fail-safe mechanisms.
8
+ #
9
+ # @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
10
+ # @param error_notifier [Proc] called when wrapped data store fails
11
+ # @return [Stoplight::Notifier::FailSafe] The original notifier if it is already
12
+ # a +FailSafe+ instance, otherwise a new +FailSafe+ instance.
13
+ def create(notifier:, error_notifier:)
14
+ case notifier
15
+ in Infrastructure::Notifier::FailSafe if notifier.error_notifier == error_notifier
16
+ notifier
17
+ in Infrastructure::Notifier::FailSafe
18
+ Infrastructure::Notifier::FailSafe.new(notifier: notifier.notifier, error_notifier:)
19
+ else
20
+ Infrastructure::Notifier::FailSafe.new(notifier:, error_notifier:)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module Redis
6
+ # Redis storage backend with automatic failover to in-memory storage.
7
+ #
8
+ # Every storage component is wrapped in a FailSafe decorator that catches
9
+ # Redis connection errors and falls back to a Memory backend. This ensures
10
+ # circuit breakers remain functional even when Redis is unavailable.
11
+ #
12
+ # The failover behavior is coordinated through a dedicated circuit breaker
13
+ # (`failover_light`) that prevents repeated Redis connection attempts during
14
+ # an outage.
15
+ #
16
+ # @example
17
+ # backend = Redis::Backend.new(
18
+ # redis: redis_connection,
19
+ # scripting: Scripting.new(redis:),
20
+ # key_space: KeySpace.build(system_name: "payments", light_name: "stripe"),
21
+ # config: light_config,
22
+ # error_notifier: ->(e) { Logger.error(e) },
23
+ # failover_light: Stoplight("redis-failover"),
24
+ # clock: SystemClock.new
25
+ # )
26
+ #
27
+ # backend.state_store #=> FailSafe::State wrapping Redis::State
28
+ #
29
+ # @api private
30
+ class Backend < DataStoreBackend
31
+ def initialize(redis:, scripting:, key_space:, config:, error_notifier:, failover_light:, clock:)
32
+ @redis = redis
33
+ @scripting = scripting
34
+ @key_space = key_space
35
+ @clock = clock
36
+ @config = config
37
+ @error_notifier = error_notifier
38
+ @failover_light = failover_light
39
+ @memory_fallback = Memory::Backend.new(clock:, config:)
40
+ end
41
+
42
+ def state_store
43
+ @state_store ||= Infrastructure::FailSafe::Storage::State.new(
44
+ primary_store: Infrastructure::Redis::Storage::State.new(
45
+ redis: @redis,
46
+ scripting: @scripting,
47
+ key_space: @key_space,
48
+ cool_off_time: @config.cool_off_time,
49
+ clock: @clock
50
+ ),
51
+ error_notifier: @error_notifier,
52
+ failover_store: @memory_fallback.state_store,
53
+ circuit_breaker: @failover_light
54
+ )
55
+ end
56
+
57
+ def recovery_lock_store
58
+ @recovery_lock_store ||= Infrastructure::FailSafe::Storage::RecoveryLock.new(
59
+ primary_store: Infrastructure::Redis::Storage::RecoveryLock.new(
60
+ config: @config, # TODO: pass cool_off_time directly
61
+ redis: @redis,
62
+ scripting: @scripting,
63
+ key_space: @key_space
64
+ ),
65
+ error_notifier: @error_notifier,
66
+ failover_store: @memory_fallback.recovery_lock_store,
67
+ circuit_breaker: @failover_light
68
+ )
69
+ end
70
+
71
+ def recovery_metrics_store
72
+ @recovery_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
73
+ error_notifier: @error_notifier,
74
+ primary_store: Infrastructure::Redis::Storage::RecoveryMetrics.new(
75
+ clock: @clock,
76
+ redis: @redis,
77
+ scripting: @scripting,
78
+ key_space: @key_space
79
+ ),
80
+ failover_store: @memory_fallback.recovery_metrics_store,
81
+ circuit_breaker: @failover_light
82
+ )
83
+ end
84
+
85
+ def windowed_metrics_store
86
+ @windowed_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
87
+ error_notifier: @error_notifier,
88
+ primary_store: Infrastructure::Redis::Storage::WindowMetrics.new(
89
+ config: @config, # TODO: pass window size directly
90
+ redis: @redis,
91
+ scripting: @scripting,
92
+ clock: @clock,
93
+ key_space: @key_space
94
+ ),
95
+ failover_store: @memory_fallback.windowed_metrics_store,
96
+ circuit_breaker: @failover_light
97
+ )
98
+ end
99
+
100
+ def unbounded_metrics_store
101
+ @unbounded_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
102
+ error_notifier: @error_notifier,
103
+ primary_store: Infrastructure::Redis::Storage::UnboundedMetrics.new(
104
+ clock: @clock,
105
+ redis: @redis,
106
+ scripting: @scripting,
107
+ key_space: @key_space
108
+ ),
109
+ failover_store: @memory_fallback.unbounded_metrics_store,
110
+ circuit_breaker: @failover_light
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ StorageSet = Data.define(
6
+ :metrics_store,
7
+ :recovery_metrics_store,
8
+ :state_store,
9
+ :recovery_lock_store
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ # Assembles a StorageSet from a backend, selecting windowed or unbounded metrics.
6
+ #
7
+ # StorageSetBuilder is the single point where the windowed/unbounded decision
8
+ # is made. All other storage components (state, recovery lock, recovery metrics)
9
+ # are backend-specific but mode-independent.
10
+ #
11
+ # @example Windowed metrics (error rate tracking)
12
+ # builder = StorageSetBuilder.new(backend: redis_backend, windowed: true)
13
+ # storage = builder.build
14
+ # storage.metrics_store #=> FailSafe<WindowMetrics>
15
+ #
16
+ # @example Unbounded metrics (consecutive error tracking)
17
+ # builder = StorageSetBuilder.new(backend: memory_backend, windowed: false)
18
+ # storage = builder.build
19
+ # storage.metrics_store #=> UnboundedMetrics
20
+ #
21
+ # @see DataStoreBackend
22
+ # @see StorageSet
23
+ # @api private
24
+ class StorageSetBuilder
25
+ attr_reader :backend
26
+ attr_reader :windowed
27
+
28
+ def initialize(backend:, windowed:)
29
+ @backend = backend
30
+ @windowed = windowed
31
+ end
32
+
33
+ def build
34
+ StorageSet.new(
35
+ metrics_store:,
36
+ recovery_metrics_store: backend.recovery_metrics_store,
37
+ state_store: backend.state_store,
38
+ recovery_lock_store: backend.recovery_lock_store
39
+ )
40
+ end
41
+
42
+ private def metrics_store
43
+ if windowed
44
+ backend.windowed_metrics_store
45
+ else
46
+ backend.unbounded_metrics_store
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ class System
6
+ class LightBuilder < Wiring::LightBuilder
7
+ def initialize(system:, config:, factory:)
8
+ @system = system
9
+
10
+ super(config:, factory:)
11
+ end
12
+
13
+ def key_space = @key_space ||= Infrastructure::Redis::Storage::KeySpace.build(
14
+ system_name: system.name,
15
+ light_name: config.name
16
+ )
17
+
18
+ def storage_set
19
+ @storage_set ||= StorageSetBuilder.new(backend: build_backend, windowed: !config.window_size.nil?).build
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :system
25
+
26
+ def state_store = storage_set.state_store
27
+ def recovery_lock_store = storage_set.recovery_lock_store
28
+ def recovery_metrics_store = storage_set.recovery_metrics_store
29
+ def metrics_store = storage_set.metrics_store
30
+ def storage_scripting = Infrastructure::Redis::Storage::Scripting.new(redis:)
31
+ def failover_system = @failover_system ||= Stoplight.__stoplight__system("failover-#{system.name}")
32
+
33
+ def build_backend
34
+ case data_store_config
35
+ in DataStore::Memory
36
+ Memory::Backend.new(clock:, config:)
37
+ in DataStore::Redis
38
+ Redis::Backend.new(
39
+ redis:, scripting: storage_scripting, key_space:, clock:, config:, error_notifier:,
40
+ failover_light: failover_system.light("redis")
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ class System
6
+ class LightFactory < Wiring::LightFactory
7
+ attr_reader :system
8
+
9
+ def initialize(system:, config:)
10
+ @system = system
11
+
12
+ super(config:)
13
+ end
14
+
15
+ def with(
16
+ name: T.undefined,
17
+ cool_off_time: T.undefined,
18
+ threshold: T.undefined,
19
+ recovery_threshold: T.undefined,
20
+ window_size: T.undefined,
21
+ tracked_errors: T.undefined,
22
+ skipped_errors: T.undefined,
23
+ data_store: T.undefined,
24
+ error_notifier: T.undefined,
25
+ notifiers: T.undefined,
26
+ traffic_control: T.undefined,
27
+ traffic_recovery: T.undefined
28
+ )
29
+ self.class.new(
30
+ system:,
31
+ config: ConfigurationDsl.new(
32
+ cool_off_time:,
33
+ threshold:,
34
+ recovery_threshold:,
35
+ window_size:,
36
+ tracked_errors:,
37
+ skipped_errors:,
38
+ traffic_control:,
39
+ traffic_recovery:,
40
+ error_notifier:,
41
+ data_store:,
42
+ notifiers:
43
+ ).configure!(config)
44
+ )
45
+ end
46
+
47
+ class InternalLightFactory < Wiring::LightFactory
48
+ def initialize
49
+ end
50
+
51
+ def with(**untyped) # steep:ignore
52
+ raise NotImplementedError, "You're not allowed to extend system lights"
53
+ end
54
+ end
55
+
56
+ private def light_builder(config:)
57
+ System::LightBuilder.new(system:, factory: light_factory, config:)
58
+ end
59
+
60
+ private def light_factory = InternalLightFactory.new
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module Stoplight
6
+ module Wiring
7
+ # 🚧UNDER CONSTRUCTION 🚧
8
+ # System provides namespace isolation and shared configuration for related circuits.
9
+ #
10
+ # Systems enforce configuration consistency within their scope - creating the same
11
+ # circuit name with different settings raises +Stoplight::Error::ConfigurationError+.
12
+ #
13
+ # This prevents subtle bugs where circuits silently interfere with each other.
14
+ #
15
+ # @example Basic usage
16
+ # billing = Stoplight.system(:billing,
17
+ # data_store: billing_redis,
18
+ # threshold: 5,
19
+ # window_size: 300
20
+ # )
21
+ #
22
+ # billing.light("stripe")
23
+ # billing.light("paypal")
24
+ #
25
+ # @example Multi-tenancy
26
+ # tenant_a = Stoplight.system(:tenant_a, data_store: tenant_a_redis)
27
+ # tenant_b = Stoplight.system(:tenant_b, data_store: tenant_b_redis)
28
+ #
29
+ # # Same circuit name, completely isolated
30
+ # tenant_a.light("api")
31
+ # tenant_b.light("api")
32
+ #
33
+ # @example Configuration inheritance
34
+ # system = Stoplight.system(:payments, threshold: 3, cool_off_time: 600)
35
+ #
36
+ # system.light("stripe") # Inherits threshold: 3
37
+ # system.light("paypal", threshold: 5) # Overrides threshold
38
+ #
39
+ # @note System configuration objects (data_store, notifiers) should be defined
40
+ # as constants and reused, not created inline. This ensures configuration
41
+ # matching works correctly across multiple system references.
42
+ #
43
+ # @note Light instances are cached within the system. Calling {#light} with
44
+ # the same name returns the cached instance.
45
+ #
46
+ # @api private
47
+ class System
48
+ attr_reader :name
49
+ # @!attribute system_config
50
+ # @api private
51
+ attr_reader :system_config
52
+
53
+ def initialize(config:)
54
+ @name = config.name
55
+ @system_config = config
56
+ @lights = Concurrent::Map.new
57
+ end
58
+
59
+ # Creates or retrieves a light.
60
+ #
61
+ # If a light with this name already exists, returns the cached instance.
62
+ # If settings differ from the existing light, raises +Stoplight::Error::ConfigurationError+.
63
+ #
64
+ #
65
+ # @raise [Stoplight::Error::ConfigurationError] if light exists with different settings
66
+ #
67
+ # @example Create a light
68
+ # light = system.light("stripe", threshold: 5, window_size: 60)
69
+ #
70
+ # @example Retrieve existing light - both return cached light
71
+ # light = system.light("stripe", threshold: 5, window_size: 60)
72
+ # light = system.light("stripe")
73
+ #
74
+ # @example Configuration conflict
75
+ # system.light("api", threshold: 5)
76
+ # system.light("api", threshold: 10) # Raises ConfigurationError
77
+ #
78
+ # @note Thread-safe: multiple threads can safely call this method concurrently
79
+ #
80
+ def light(
81
+ name,
82
+ cool_off_time: T.undefined,
83
+ threshold: T.undefined,
84
+ recovery_threshold: T.undefined,
85
+ window_size: T.undefined,
86
+ tracked_errors: T.undefined,
87
+ skipped_errors: T.undefined,
88
+ traffic_control: T.undefined,
89
+ traffic_recovery: T.undefined
90
+ )
91
+ light_config = ConfigurationDsl.new(
92
+ name:,
93
+ cool_off_time:,
94
+ threshold:,
95
+ recovery_threshold:,
96
+ window_size:,
97
+ tracked_errors:,
98
+ skipped_errors:,
99
+ traffic_control:,
100
+ traffic_recovery:
101
+ ).configure!(system_config)
102
+
103
+ light, _ = lights.compute(name) do |existing|
104
+ if existing
105
+ existing_light, existing_config = existing
106
+ if light_config == existing_config
107
+ [existing_light, existing_config]
108
+ else
109
+ raise Stoplight::Error::ConfigurationError, <<~MSG
110
+ Light name `#{name}` reused with different settings:
111
+ existing settings: #{existing_config}
112
+ new settings: #{light_config}
113
+
114
+ You cannot use the same light name with different settings.
115
+ MSG
116
+ end
117
+ else
118
+ [LightFactory.new(system: self, config: light_config).build, light_config]
119
+ end
120
+ end
121
+ light
122
+ end
123
+
124
+ private
125
+
126
+ attr_reader :lights
127
+ end
128
+ end
129
+ end