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
@@ -2,76 +2,93 @@
2
2
 
3
3
  module Stoplight
4
4
  module Wiring
5
- # User-facing configuration interface
5
+ # User-facing configuration interface for setting global Stoplight defaults.
6
+ #
7
+ # This class serves as the configuration DSL yielded to users when calling
8
+ # +Stoplight.configure+. It provides a clean interface for setting default
9
+ # values while internally tracking whether each setting was explicitly
10
+ # configured or should fall back to library defaults.
11
+ #
12
+ # @example Configuring Stoplight defaults
13
+ # Stoplight.configure do |config|
14
+ # config.data_store = Redis.new
15
+ # config.threshold = 5
16
+ # # window_size not set - will use library default
17
+ # end
18
+ #
19
+ # == Option-Based Configuration Tracking
20
+ #
21
+ # Internally, each setting is wrapped in an Option type (+Some+ or +None+).
22
+ # This design allows the class to distinguish between three states:
23
+ #
24
+ # 1. Not configured: Stored as +None+, getter returns library default
25
+ # 2. Explicitly configured: Stored as +Some(value)+, getter returns that value
26
+ # 3. Explicitly set to nil: Stored as +Some(nil)+, getter returns +nil+
27
+ #
28
+ # This distinction is critical when building {Settings} and {Dependencies}
29
+ # objects, which need to know whether a value was user-specified (and should
30
+ # be enforced) or inherited (and can be overridden per-circuit).
31
+ #
32
+ # == Dual Interface
33
+ #
34
+ # The class exposes two interfaces for each setting:
35
+ #
36
+ # - *Setters* (+attr_writer+): Accept raw values, wrap them in +Some+
37
+ # - *Getters* (custom methods): Unwrap the Option, returning the value
38
+ # or falling back to library defaults from {Default}
39
+ #
40
+ # The {#settings} and {#dependencies} methods preserve the raw Option
41
+ # values, allowing downstream code to detect explicit configuration.
42
+ #
43
+ # @see Settings Value object for circuit behavior configuration
44
+ # @see Dependencies Value object for infrastructure dependencies
45
+ # @see Default Library default constants
46
+ #
6
47
  class DefaultConfiguration
7
- # @!attribute [w] cool_off_time
8
- # @return [Integer, nil] The default cool-off time in seconds.
9
- attr_writer :cool_off_time
48
+ def initialize
49
+ @config = DefaultConfig.with
50
+ @cool_off_time = T.undefined
51
+ @threshold = T.undefined
52
+ @recovery_threshold = T.undefined
53
+ @window_size = T.undefined
54
+ @tracked_errors = T.undefined
55
+ @skipped_errors = T.undefined
56
+ @traffic_control = T.undefined
57
+ @traffic_recovery = T.undefined
58
+ @error_notifier = T.undefined
59
+ @data_store = T.undefined
60
+ @notifiers = T.undefined
61
+ end
10
62
 
11
- # @!attribute [w] threshold
12
- # @return [Integer, Float, nil] The default failure threshold to trip the circuit breaker.
13
- attr_writer :threshold
63
+ def notifiers = @config.notifiers
14
64
 
15
- # @!attribute [w] recovery_threshold
16
- # @return [Integer, nil] The default recovery threshold for the circuit breaker.
65
+ attr_writer :cool_off_time
66
+ attr_writer :threshold
17
67
  attr_writer :recovery_threshold
18
-
19
- # @!attribute [w] window_size
20
- # @return [Integer, nil] The default size of the rolling window for failure tracking.
21
68
  attr_writer :window_size
22
-
23
- # @!attribute [w] tracked_errors
24
- # @return [Array<Class>, nil] The default list of errors to track.
25
69
  attr_writer :tracked_errors
26
-
27
- # @!attribute [w] skipped_errors
28
- # @return [Array<Class>, nil] The default list of errors to skip.
29
70
  attr_writer :skipped_errors
30
-
31
- # @!attribute [w] error_notifier
32
- # @return [Proc, nil] The default error notifier (callable object).
33
- attr_writer :error_notifier
34
-
35
- # @!attribute [rw] notifiers
36
- # @return [Array<Stoplight::Domain::StateTransitionNotifier>] The default list of notifiers.
37
- attr_accessor :notifiers
38
-
39
- # @!attribute [rw] data_store
40
- # @return [Stoplight::Domain::DataStore] The default data store instance.
41
- attr_accessor :data_store
42
-
43
- # @!attribute [w] traffic_control
44
- # @return [Stoplight::Domain::TrafficControl::Base] The traffic control strategy.
45
71
  attr_writer :traffic_control
46
-
47
- # @!attribute [w] traffic_recovery
48
- # @return [Stoplight::Domain::TrafficRecovery::Base] The traffic recovery strategy.
49
72
  attr_writer :traffic_recovery
73
+ attr_writer :error_notifier
74
+ attr_writer :data_store
75
+ attr_writer :notifiers
50
76
 
51
- def initialize
52
- # This allows users appending notifiers to the default list,
53
- # while still allowing them to override the default list.
54
- @notifiers = Default::NOTIFIERS
55
- end
56
-
57
- # Converts the user-defined configuration to a hash.
58
- #
59
- # @return [Hash] A hash representation of the configuration, excluding nil values.
60
- # @api private
61
- def to_h
62
- {
77
+ # Builds and validates configuration
78
+ def to_config!
79
+ ConfigurationDsl.new(
63
80
  cool_off_time: @cool_off_time,
64
81
  threshold: @threshold,
65
82
  recovery_threshold: @recovery_threshold,
66
83
  window_size: @window_size,
67
84
  tracked_errors: @tracked_errors,
68
85
  skipped_errors: @skipped_errors,
69
- data_store: @data_store,
70
- error_notifier: @error_notifier,
71
- notifiers: @notifiers,
72
86
  traffic_control: @traffic_control,
73
- traffic_recovery: @traffic_recovery
74
- }.compact
87
+ traffic_recovery: @traffic_recovery,
88
+ error_notifier: @error_notifier,
89
+ data_store: @data_store,
90
+ notifiers: @notifiers
91
+ ).configure!(@config)
75
92
  end
76
93
  end
77
94
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module Stoplight
6
+ module Wiring
7
+ # Constructs a fully-wired Light instance from validated configuration.
8
+ #
9
+ # LightBuilder is the final assembly step in the Light creation pipeline.
10
+ # It receives validated config and dependencies from ConfigurationPipeline
11
+ # and wires together all infrastructure components (data stores, trackers,
12
+ # strategies) needed for a functioning circuit breaker.
13
+ #
14
+ # LightBuilder maintains a global registry (MEMORY_REGISTRY) that ensures
15
+ # the same Memory data store config object always produces the same
16
+ # data store instance:
17
+ #
18
+ # data_store = Stoplight::DataStore::Memory.new
19
+ # light1 = Stoplight("foo", data_store: data_store)
20
+ # light2 = Stoplight("bar", data_store: data_store)
21
+ # # light1 and light2 share the same underlying memory store
22
+ #
23
+ # light3 = Stoplight("baz", data_store: Stoplight::DataStore::Memory.new)
24
+ # # light3 has its own independent store
25
+ #
26
+ # This singleton behavior is keyed by config object identity (object_id),
27
+ # not by value equality.
28
+ #
29
+ # @api private
30
+ class LightBuilder
31
+ FAILOVER_DATA_STORE_CONFIG = Stoplight::DataStore::Memory.new
32
+ private_constant :FAILOVER_DATA_STORE_CONFIG
33
+
34
+ MEMORY_REGISTRY = Concurrent::Map.new
35
+ private_constant :MEMORY_REGISTRY
36
+
37
+ def initialize(config:, factory:)
38
+ @clock = Infrastructure::SystemClock.new
39
+ @config = config
40
+ @name = T.must(config.name)
41
+ @cool_off_time = config.cool_off_time
42
+
43
+ @data_store_config = config.data_store
44
+ @error_notifier = config.error_notifier
45
+ @factory = factory
46
+ @notifiers = config.notifiers
47
+ @traffic_recovery = config.traffic_recovery
48
+ @traffic_control = config.traffic_control
49
+
50
+ @error_tracking_policy = Domain::ErrorTrackingPolicy.new(
51
+ tracked: config.tracked_errors,
52
+ skipped: config.skipped_errors
53
+ )
54
+ end
55
+
56
+ def build
57
+ Stoplight::Domain::Light.new(
58
+ @name,
59
+ state_store:,
60
+ green_run_strategy:,
61
+ yellow_run_strategy:,
62
+ red_run_strategy:,
63
+ factory:
64
+ )
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :data_store_config
70
+ attr_reader :error_notifier
71
+ attr_reader :factory
72
+ attr_reader :clock
73
+ attr_reader :traffic_control
74
+ attr_reader :traffic_recovery
75
+ attr_reader :config
76
+
77
+ def state_store = Stoplight::Infrastructure::Storage::CompatibilityState.new(config:, data_store:)
78
+
79
+ # @return [<Stoplight::Notifier::Base>]
80
+ def notifiers
81
+ Array(@notifiers).map do |notifier|
82
+ Infrastructure::Notifier::FailSafe.new(notifier:, error_notifier:)
83
+ end
84
+ end
85
+
86
+ def redis_recovery_lock_store
87
+ Infrastructure::Redis::DataStore::RecoveryLockStore.new(
88
+ redis:,
89
+ lock_timeout: config.cool_off_time_in_milliseconds,
90
+ scripting:
91
+ )
92
+ end
93
+
94
+ def scripting = Infrastructure::Redis::DataStore::Scripting.new(redis:)
95
+
96
+ def memory_recovery_lock_store
97
+ Infrastructure::Memory::DataStore::RecoveryLockStore.new
98
+ end
99
+
100
+ def failover_data_store
101
+ create_data_store(FAILOVER_DATA_STORE_CONFIG)
102
+ end
103
+
104
+ def data_store
105
+ create_data_store(data_store_config)
106
+ end
107
+
108
+ def metrics_store
109
+ Stoplight::Infrastructure::Storage::CompatibilityMetrics.new(config:, data_store:)
110
+ end
111
+
112
+ def recovery_lock_store
113
+ Stoplight::Infrastructure::Storage::CompatibilityRecoveryLock.new(config:, data_store:)
114
+ end
115
+
116
+ def request_tracker
117
+ Domain::Tracker::Request.new(traffic_control:, notifiers:, config:, metrics_store:, state_store:)
118
+ end
119
+
120
+ def recovery_probe_tracker
121
+ Domain::Tracker::RecoveryProbe.new(
122
+ traffic_recovery:,
123
+ notifiers:,
124
+ config:,
125
+ metrics_store: recovery_metrics_store,
126
+ state_store:
127
+ )
128
+ end
129
+
130
+ def recovery_metrics_store
131
+ Stoplight::Infrastructure::Storage::CompatibilityRecoveryMetrics.new(config:, data_store:)
132
+ end
133
+
134
+ def green_run_strategy
135
+ Domain::Strategies::GreenRunStrategy.new(
136
+ error_tracking_policy: @error_tracking_policy,
137
+ request_tracker:
138
+ )
139
+ end
140
+
141
+ def yellow_run_strategy
142
+ Domain::Strategies::YellowRunStrategy.new(
143
+ name: @name,
144
+ error_tracking_policy: @error_tracking_policy,
145
+ notifiers:,
146
+ request_tracker: recovery_probe_tracker,
147
+ red_run_strategy:,
148
+ state_store:,
149
+ metrics_store:,
150
+ recovery_lock_store:,
151
+ config: @config
152
+ )
153
+ end
154
+
155
+ def red_run_strategy
156
+ Domain::Strategies::RedRunStrategy.new(name: @name, cool_off_time: @cool_off_time)
157
+ end
158
+
159
+ def create_data_store(data_store_config)
160
+ case data_store_config
161
+ when Stoplight::DataStore::Memory
162
+ memory_registry.compute_if_absent(data_store_config.object_id) do
163
+ Infrastructure::Memory::DataStore.new(
164
+ recovery_lock_store: memory_recovery_lock_store,
165
+ clock:
166
+ )
167
+ end
168
+ when Stoplight::DataStore::Redis
169
+ Infrastructure::FailSafe::DataStore.new(
170
+ data_store: Stoplight::Infrastructure::Redis::DataStore.new(
171
+ clock:,
172
+ redis: data_store_config.redis,
173
+ warn_on_clock_skew: data_store_config.warn_on_clock_skew,
174
+ recovery_lock_store: redis_recovery_lock_store,
175
+ scripting:
176
+ ),
177
+ error_notifier:,
178
+ failover_data_store:,
179
+ circuit_breaker: Stoplight.system_light("data_store:fail_safe:redis")
180
+ )
181
+ else
182
+ raise NoMatchingPatternError, data_store_config
183
+ end
184
+ end
185
+
186
+ def redis
187
+ case data_store_config
188
+ when DataStore::Redis
189
+ data_store_config.redis
190
+ else
191
+ raise TypeError, "Expected Stoplight::DataStore::Redis, got #{data_store_config.class}"
192
+ end
193
+ end
194
+
195
+ def memory_registry = MEMORY_REGISTRY
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ class LightFactory
6
+ TrafficControlDsl = ->(value) {
7
+ case value
8
+ in _ if value.respond_to?(:stop_traffic?) # TODO: can be removed in 6.0
9
+ value
10
+ in :consecutive_errors
11
+ Domain::TrafficControl::ConsecutiveErrors.new
12
+ in :error_rate
13
+ Domain::TrafficControl::ErrorRate.new
14
+ in {error_rate: error_rate_settings}
15
+ Domain::TrafficControl::ErrorRate.new(**error_rate_settings)
16
+ else
17
+ raise Stoplight::Error::ConfigurationError, <<~ERROR
18
+ unsupported traffic_control strategy provided (`#{value}`). Supported options:
19
+ * :consecutive_errors
20
+ * :error_rate
21
+ ERROR
22
+ end
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ class LightFactory
6
+ TrafficRecoveryDsl = ->(value) {
7
+ case value
8
+ in _ if value.respond_to?(:determine_color) # TODO: remove in 6.0
9
+ value
10
+ in :consecutive_successes
11
+ Domain::TrafficRecovery::ConsecutiveSuccesses.new
12
+ else
13
+ raise Error::ConfigurationError, <<~ERROR
14
+ unsupported traffic_recovery strategy provided (`#{value}`). Supported options:
15
+ * :consecutive_successes
16
+ ERROR
17
+ end
18
+ }
19
+ end
20
+ end
21
+ end
@@ -13,34 +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
- # @!attribute [r] container
19
- # The dependency injection container holding all component configurations.
20
- # Contains config, data_store, notifiers, strategies, etc.
21
- # @return [Stoplight::Wiring::Container]
22
- protected attr_reader :container
23
-
24
- # @param container [Stoplight::Wiring::Container]
25
- def initialize(container)
26
- @container = container
16
+ #
17
+ class LightFactory
18
+ def initialize(config:)
19
+ @config = config
27
20
  end
28
21
 
29
- # @param settings [Hash] Settings to override in the new factory
30
- # @see Stoplight()
31
- # @return [Stoplight::Wiring::LightFactory]
32
- # @see Stoplight()
33
- def with(**settings)
34
- transformed_settings = transform_settings(settings)
35
- config_settings = extract_config_settings(transformed_settings)
36
- dependency_settings = extract_dependency_settings(transformed_settings)
37
-
38
- validate_settings!(transformed_settings, config_settings, dependency_settings)
39
-
40
- new_config = container.resolve(:config).with(**config_settings)
41
- new_container = container.with(config: new_config, **dependency_settings)
42
-
43
- self.class.new(new_container)
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
+ )
44
52
  end
45
53
 
46
54
  # Builds a fully-configured Light instance.
@@ -60,129 +68,60 @@ module Stoplight
60
68
  # light.run { api_call }
61
69
 
62
70
  def build
63
- validate!
64
-
65
- Stoplight::Domain::Light.new(
66
- container.resolve(:config),
67
- data_store: container.resolve(:data_store),
68
- green_run_strategy: container.resolve(:green_run_strategy),
69
- yellow_run_strategy: container.resolve(:yellow_run_strategy),
70
- red_run_strategy: container.resolve(:red_run_strategy),
71
- factory: self
72
- )
71
+ light_builder(config:).build
73
72
  end
74
73
 
75
74
  def ==(other)
76
- other.is_a?(self.class) && other.container == container
77
- end
78
-
79
- private def extract_config_settings(settings)
80
- settings.slice(*container.resolve(:config).members)
81
- end
82
-
83
- private def extract_dependency_settings(settings)
84
- settings.slice(*container.keys)
75
+ other.is_a?(self.class) && other.config == config
85
76
  end
86
77
 
87
- private def validate_settings!(settings, config_settings, dependency_settings)
88
- recognized_keys = config_settings.keys + dependency_settings.keys
89
- unexpected_keys = settings.keys - recognized_keys
90
-
91
- return if unexpected_keys.empty?
92
- raise ArgumentError, "Unknown settings: #{unexpected_keys.join(", ")}"
78
+ alias_method :eql?, :==
79
+
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
93
108
  end
94
109
 
95
- private def transform_settings(settings)
96
- settings.dup.tap do |transformed_settings|
97
- transform_config_settings!(transformed_settings)
98
- transform_dependencies_settings!(transformed_settings)
99
- end
110
+ def hash
111
+ [self.class, config].hash
100
112
  end
101
113
 
102
- private def transform_config_settings!(settings)
103
- if settings.key?(:tracked_errors)
104
- settings[:tracked_errors] = normalize_array(settings[:tracked_errors])
105
- end
106
-
107
- if settings.key?(:skipped_errors)
108
- settings[:skipped_errors] = normalize_array(settings[:skipped_errors])
109
- end
114
+ protected
110
115
 
111
- if settings.key?(:cool_off_time)
112
- settings[:cool_off_time] = normalize_cool_off_time(settings[:cool_off_time])
113
- end
114
- end
116
+ attr_reader :config
115
117
 
116
- private def transform_dependencies_settings!(settings)
117
- if settings.key?(:traffic_control)
118
- settings[:traffic_control] = apply_traffic_control_dsl(settings[:traffic_control])
119
- end
118
+ private
120
119
 
121
- if settings.key?(:traffic_recovery)
122
- settings[:traffic_recovery] = apply_traffic_recovery_dsl(settings[:traffic_recovery])
123
- end
120
+ def light_builder(config:)
121
+ LightBuilder.new(config:, factory: light_factory)
124
122
  end
125
123
 
126
- private def normalize_array(value) = Array(value)
127
- private def normalize_cool_off_time(value) = value.to_i
128
-
129
- private def apply_traffic_control_dsl(traffic_control)
130
- case traffic_control
131
- in Domain::TrafficControl::Base
132
- traffic_control
133
- in :consecutive_errors
134
- Domain::TrafficControl::ConsecutiveErrors.new
135
- in :error_rate
136
- Domain::TrafficControl::ErrorRate.new
137
- in {error_rate: error_rate_settings}
138
- Domain::TrafficControl::ErrorRate.new(**error_rate_settings)
139
- else
140
- raise Domain::Error::ConfigurationError, <<~ERROR
141
- unsupported traffic_control strategy provided (`#{traffic_control}`). Supported options:
142
- * :consecutive_errors
143
- * :error_rate
144
- ERROR
145
- end
146
- end
147
-
148
- def apply_traffic_recovery_dsl(traffic_recovery)
149
- case traffic_recovery
150
- in Domain::TrafficRecovery::Base
151
- traffic_recovery
152
- in :consecutive_successes
153
- Domain::TrafficRecovery::ConsecutiveSuccesses.new
154
- else
155
- raise Domain::Error::ConfigurationError, <<~ERROR
156
- unsupported traffic_recovery strategy provided (`#{traffic_recovery}`). Supported options:
157
- * :consecutive_successes
158
- ERROR
159
- end
160
- end
161
-
162
- private def validate!
163
- validate_traffic_control!(container.resolve(:traffic_control), container.resolve(:config))
164
- validate_traffic_recovery!(container.resolve(:traffic_recovery), container.resolve(:config))
165
- end
166
-
167
- private def validate_traffic_control!(traffic_control, config)
168
- traffic_control.check_compatibility(config).then do |compatibility_result|
169
- if compatibility_result.incompatible?
170
- raise Domain::Error::ConfigurationError.new(
171
- "#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
172
- )
173
- end
174
- end
175
- end
176
-
177
- private def validate_traffic_recovery!(traffic_recovery, config)
178
- traffic_recovery.check_compatibility(config).then do |compatibility_result|
179
- if compatibility_result.incompatible?
180
- raise Domain::Error::ConfigurationError.new(
181
- "#{traffic_recovery.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
182
- )
183
- end
184
- end
185
- end
124
+ def light_factory = self
186
125
  end
187
126
  end
188
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