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
@@ -13,39 +13,42 @@ module Stoplight
13
13
  # @see Stoplight::Domain::LightFactory
14
14
  # @see Stoplight()
15
15
  # @api private
16
-
17
- class LightFactory < Domain::LightFactory
18
- DEPENDENCY_KEYS = %i[data_store traffic_recovery traffic_control notifiers error_notifier].freeze
19
- private_constant :DEPENDENCY_KEYS
20
-
21
- CONFIG_KEYS = Domain::Config.members.freeze
22
- private_constant :CONFIG_KEYS
23
-
24
- # @!attribute [r] settings
25
- # @return [Hash]
26
- protected attr_reader :settings
27
-
28
- def initialize(settings = {})
29
- @settings = settings
30
-
31
- validate_settings!
32
- end
33
-
34
- private def validate_settings!
35
- recognized = CONFIG_KEYS + DEPENDENCY_KEYS
36
- unknown = settings.keys - recognized
37
-
38
- return if unknown.empty?
39
-
40
- raise ArgumentError, "Unknown settings: #{unknown.join(", ")}", caller(2)
16
+ #
17
+ class LightFactory
18
+ def initialize(config:)
19
+ @config = config
41
20
  end
42
21
 
43
- # @param settings [Hash] Settings to override in the new factory
44
- # @see Stoplight()
45
- # @return [Stoplight::Wiring::LightFactory]
46
- # @see Stoplight()
47
- def with(**settings)
48
- self.class.new(self.settings.merge(settings))
22
+ def with(
23
+ name: T.undefined,
24
+ cool_off_time: T.undefined,
25
+ threshold: T.undefined,
26
+ recovery_threshold: T.undefined,
27
+ window_size: T.undefined,
28
+ tracked_errors: T.undefined,
29
+ skipped_errors: T.undefined,
30
+ data_store: T.undefined,
31
+ error_notifier: T.undefined,
32
+ notifiers: T.undefined,
33
+ traffic_control: T.undefined,
34
+ traffic_recovery: T.undefined
35
+ )
36
+ self.class.new(
37
+ config: ConfigurationDsl.new(
38
+ name:,
39
+ cool_off_time:,
40
+ threshold:,
41
+ recovery_threshold:,
42
+ window_size:,
43
+ tracked_errors:,
44
+ skipped_errors:,
45
+ traffic_control:,
46
+ traffic_recovery:,
47
+ error_notifier:,
48
+ data_store:,
49
+ notifiers:
50
+ ).configure!(config)
51
+ )
49
52
  end
50
53
 
51
54
  # Builds a fully-configured Light instance.
@@ -65,37 +68,60 @@ module Stoplight
65
68
  # light.run { api_call }
66
69
 
67
70
  def build
68
- config_settings = settings.slice(*CONFIG_KEYS)
69
- dependency_settings = settings.slice(*DEPENDENCY_KEYS)
71
+ light_builder(config:).build
72
+ end
70
73
 
71
- config, dependencies = ConfigurationPipeline.process(
72
- config_settings,
73
- dependency_settings
74
- )
75
- LightBuilder.new(factory: self, config:, **dependencies).build
74
+ def ==(other)
75
+ other.is_a?(self.class) && other.config == config
76
76
  end
77
77
 
78
- # @return [Stoplight::Error::ConfigurationError]
79
- def validate_configuration!
80
- config_settings = settings.slice(*CONFIG_KEYS)
81
- dependency_settings = settings.slice(*DEPENDENCY_KEYS)
78
+ alias_method :eql?, :==
82
79
 
83
- ConfigurationPipeline.process(
84
- config_settings,
85
- dependency_settings
86
- )
87
- nil
80
+ def build_with(
81
+ name: T.undefined,
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
+ data_store: T.undefined,
89
+ error_notifier: T.undefined,
90
+ notifiers: T.undefined,
91
+ traffic_control: T.undefined,
92
+ traffic_recovery: T.undefined
93
+ )
94
+ with(
95
+ name:,
96
+ cool_off_time:,
97
+ threshold:,
98
+ recovery_threshold:,
99
+ window_size:,
100
+ tracked_errors:,
101
+ skipped_errors:,
102
+ data_store:,
103
+ error_notifier:,
104
+ notifiers:,
105
+ traffic_control:,
106
+ traffic_recovery:
107
+ ).build
88
108
  end
89
109
 
90
- def ==(other)
91
- other.is_a?(self.class) && other.settings == settings
110
+ def hash
111
+ [self.class, config].hash
92
112
  end
93
113
 
94
- alias_method :eql?, :==
114
+ protected
95
115
 
96
- def hash
97
- [self.class, settings].hash
116
+ attr_reader :config
117
+
118
+ private
119
+
120
+ def light_builder(config:)
121
+ LightBuilder.new(config:, factory: light_factory)
98
122
  end
123
+
124
+ def light_factory = self
99
125
  end
100
126
  end
101
127
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module Memory
6
+ # In-memory storage backend for single-process deployments.
7
+ #
8
+ # All storage components use thread-safe in-memory data structures.
9
+ # State is not shared across processes and is lost on restart.
10
+ #
11
+ # Memory backend is also used as the fallback layer for Redis backend
12
+ # when Redis is unavailable.
13
+ #
14
+ # @example
15
+ # backend = Memory::Backend.new(clock: SystemClock.new, config:)
16
+ # backend.state_store #=> Memory::Storage::State
17
+ #
18
+ # @api private
19
+ class Backend < DataStoreBackend
20
+ def initialize(clock:, config:)
21
+ @clock = clock
22
+ @config = config
23
+ end
24
+
25
+ def state_store
26
+ @state_store ||= Infrastructure::Memory::Storage::State.new(
27
+ clock: @clock,
28
+ cool_off_time: @config.cool_off_time
29
+ )
30
+ end
31
+
32
+ def recovery_lock_store
33
+ @recovery_lock_store ||= Infrastructure::Memory::Storage::RecoveryLock.new
34
+ end
35
+
36
+ def recovery_metrics_store
37
+ @recovery_metrics_store ||= Infrastructure::Memory::Storage::RecoveryMetrics.new(
38
+ clock: @clock
39
+ )
40
+ end
41
+
42
+ def windowed_metrics_store
43
+ @windowed_metrics_store ||= Infrastructure::Memory::Storage::WindowMetrics.new(
44
+ window_size: T.must(@config.window_size),
45
+ clock: @clock
46
+ )
47
+ end
48
+
49
+ def unbounded_metrics_store
50
+ @unbounded_metrics_store ||= Infrastructure::Memory::Storage::UnboundedMetrics.new(
51
+ clock: @clock
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ 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