stoplight 5.3.8 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -2
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/actions/remove.rb +23 -0
  5. data/lib/stoplight/admin/dependencies.rb +6 -1
  6. data/lib/stoplight/admin/helpers.rb +10 -5
  7. data/lib/stoplight/admin/lights_repository.rb +26 -14
  8. data/lib/stoplight/admin/views/_card.erb +13 -1
  9. data/lib/stoplight/admin/views/layout.erb +3 -3
  10. data/lib/stoplight/admin.rb +13 -4
  11. data/lib/stoplight/common/deprecations.rb +11 -0
  12. data/lib/stoplight/domain/color.rb +11 -0
  13. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  14. data/lib/stoplight/domain/config.rb +59 -0
  15. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +71 -17
  16. data/lib/stoplight/domain/error.rb +42 -0
  17. data/lib/stoplight/domain/failure.rb +44 -0
  18. data/lib/stoplight/domain/light/configuration_builder_interface.rb +234 -0
  19. data/lib/stoplight/domain/light.rb +208 -0
  20. data/lib/stoplight/domain/light_factory.rb +75 -0
  21. data/lib/stoplight/domain/metrics.rb +64 -0
  22. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  23. data/lib/stoplight/domain/state.rb +11 -0
  24. data/lib/stoplight/domain/state_snapshot.rb +57 -0
  25. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  26. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  27. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  28. data/lib/stoplight/domain/storage/state.rb +87 -0
  29. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  30. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  31. data/lib/stoplight/domain/strategies/run_strategy.rb +22 -0
  32. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +125 -0
  33. data/lib/stoplight/domain/tracker/base.rb +12 -0
  34. data/lib/stoplight/domain/tracker/recovery_probe.rb +76 -0
  35. data/lib/stoplight/domain/tracker/request.rb +72 -0
  36. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  37. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +53 -0
  38. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  39. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  40. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +66 -0
  41. data/lib/stoplight/domain/traffic_recovery.rb +12 -0
  42. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  43. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  44. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  45. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  46. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  47. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  48. data/lib/stoplight/infrastructure/data_store/memory.rb +338 -0
  49. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/get_metrics.lua +26 -0
  50. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  51. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  52. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  53. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  54. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  55. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  56. data/lib/stoplight/infrastructure/data_store/redis.rb +524 -0
  57. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  58. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  59. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  60. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  61. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  62. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  63. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  64. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  65. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  66. data/lib/stoplight/version.rb +1 -1
  67. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  68. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  69. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  70. data/lib/stoplight/wiring/default.rb +28 -0
  71. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  72. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  73. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  74. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  75. data/lib/stoplight/wiring/light_builder.rb +185 -0
  76. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  77. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  78. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  79. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  80. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  81. data/lib/stoplight/wiring/light_factory.rb +101 -0
  82. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  83. data/lib/stoplight/wiring/public_api.rb +29 -0
  84. data/lib/stoplight.rb +55 -30
  85. metadata +92 -42
  86. data/lib/stoplight/color.rb +0 -9
  87. data/lib/stoplight/config/dsl.rb +0 -97
  88. data/lib/stoplight/config/library_default_config.rb +0 -21
  89. data/lib/stoplight/config/system_config.rb +0 -7
  90. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  91. data/lib/stoplight/data_store/memory.rb +0 -311
  92. data/lib/stoplight/data_store/redis/get_metadata.lua +0 -38
  93. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  94. data/lib/stoplight/data_store/redis.rb +0 -449
  95. data/lib/stoplight/data_store.rb +0 -6
  96. data/lib/stoplight/default.rb +0 -30
  97. data/lib/stoplight/error.rb +0 -10
  98. data/lib/stoplight/failure.rb +0 -71
  99. data/lib/stoplight/light/config.rb +0 -111
  100. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  101. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  102. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  103. data/lib/stoplight/light/run_strategy.rb +0 -32
  104. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  105. data/lib/stoplight/light.rb +0 -191
  106. data/lib/stoplight/metadata.rb +0 -99
  107. data/lib/stoplight/notifier/fail_safe.rb +0 -70
  108. data/lib/stoplight/notifier/generic.rb +0 -79
  109. data/lib/stoplight/notifier/io.rb +0 -21
  110. data/lib/stoplight/notifier/logger.rb +0 -19
  111. data/lib/stoplight/state.rb +0 -9
  112. data/lib/stoplight/traffic_control/base.rb +0 -70
  113. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  114. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  115. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  116. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  117. data/lib/stoplight/traffic_recovery.rb +0 -11
  118. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_failure.lua +0 -0
  119. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_success.lua +0 -0
  120. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_green.lua +0 -0
  121. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_red.lua +0 -0
  122. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_yellow.lua +0 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Notifier
6
+ # @see Base
7
+ class Logger < Domain::StateTransitionNotifier
8
+ include Generic
9
+
10
+ # @return [::Logger]
11
+ def logger
12
+ @object
13
+ end
14
+
15
+ def put(message)
16
+ logger.warn(message)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges Domain::Storage::Metrics to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the metrics abstraction to be introduced
9
+ # without breaking existing data store implementations. It delegates all
10
+ # operations to the data store's original methods.
11
+ #
12
+ # This class will be removed in a future versions once all data stores
13
+ # have native metrics implementations.
14
+ #
15
+ # @example Creating metrics for a circuit
16
+ # metrics = CompatibilityMetrics.new(
17
+ # data_store: redis_store,
18
+ # config: config
19
+ # )
20
+ # metrics.record_success
21
+ #
22
+ # @see Stoplight::Domain::Storage::Metrics
23
+ class CompatibilityMetrics < Domain::Storage::Metrics
24
+ private attr_reader :data_store
25
+ private attr_reader :config
26
+
27
+ # @param data_store [Stoplight::Domain::DataStore]
28
+ # @param config [Stoplight::Domain::Config]
29
+ def initialize(data_store:, config:)
30
+ @data_store = data_store
31
+ @config = config
32
+ end
33
+
34
+ def metrics_snapshot = data_store.get_metrics(config)
35
+
36
+ # @return [void]
37
+ def record_success = data_store.record_success(config)
38
+
39
+ # @param error [StandardError]
40
+ # @return [void]
41
+ def record_failure(error) = data_store.record_failure(config, error)
42
+
43
+ # @return [void]
44
+ def clear = data_store.clear_metrics(config)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges +Domain::Storage::RecoveryLock+ to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the recovery lock abstraction to be
9
+ # introduced without breaking existing data store implementations. It
10
+ # delegates all lock operations to the data store's original methods.
11
+ #
12
+ # This adapter will be removed in a future versions once all
13
+ # data stores have native recovery lock implementations.
14
+ #
15
+ # @see Stoplight::Domain::Storage::RecoveryLock
16
+ class CompatibilityRecoveryLock < Domain::Storage::RecoveryLock
17
+ private attr_reader :data_store
18
+ private attr_reader :config
19
+
20
+ # @param data_store [Stoplight::Domain::DataStore]
21
+ # @param config [Stoplight::Domain::Config]
22
+ def initialize(data_store:, config:)
23
+ @data_store = data_store
24
+ @config = config
25
+ end
26
+
27
+ # @return [Stoplight::Domain::RecoveryLockToken, nil]
28
+ def acquire_lock = data_store.acquire_recovery_lock(config)
29
+
30
+ # @param lock [Stoplight::Domain::LockToken]
31
+ # @return [void]
32
+ def release_lock(lock) = data_store.release_recovery_lock(lock)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # When a circuit is RED (open), Stoplight periodically sends "recovery probes"
7
+ # to test whether the protected service has recovered. These test requests have
8
+ # different semantics than normal requests and their metrics are tracked separately.
9
+ #
10
+ # Like +CompatibilityMetrics+, this adapter will be replaced with purpose-built
11
+ # recovery metrics implementations (e.g., +ConsecutiveSuccessMetrics+) once the
12
+ # metrics extraction is complete.
13
+ #
14
+ # @example Recovery probe flow
15
+ # # Circuit is RED, start probing
16
+ # recovery_metrics = CompatibilityRecoveryMetrics.new(
17
+ # data_store: redis_store,
18
+ # config: circuit_config
19
+ # )
20
+ #
21
+ # recovery_metrics.record_success
22
+ # recovery_metrics.metrics_snapshot # => 1 success, 0 failures
23
+ #
24
+ # @see Stoplight::Domain::Storage::Metrics
25
+ class CompatibilityRecoveryMetrics < Domain::Storage::Metrics
26
+ private attr_reader :data_store
27
+ private attr_reader :config
28
+
29
+ # @param data_store [Stoplight::Domain::DataStore]
30
+ # @param config [Stoplight::Domain::Config]
31
+ def initialize(data_store:, config:)
32
+ @data_store = data_store
33
+ @config = config
34
+ end
35
+
36
+ def metrics_snapshot = data_store.get_recovery_metrics(config)
37
+
38
+ # Tracks successful circuit breaker execution
39
+ #
40
+ # @return [void]
41
+ def record_success = data_store.record_recovery_probe_success(config)
42
+
43
+ # Tracks failed circuit breaker execution
44
+ #
45
+ # @param error [StandardError]
46
+ # @return [void]
47
+ def record_failure(error) = data_store.record_recovery_probe_failure(config, error)
48
+
49
+ # Clears metrics
50
+ # @return [void]
51
+ def clear = data_store.clear_recovery_metrics(config)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module Storage
6
+ # Temporary adapter that bridges Domain::Storage::State to existing DataStore.
7
+ #
8
+ # This compatibility layer allows the state abstraction to be introduced
9
+ # without breaking existing data store implementations. It delegates all
10
+ # state operations to the data store's original methods.
11
+ #
12
+ # This adapter will be removed in a future versions once all
13
+ # data stores have native state storage implementations.
14
+ #
15
+ # @example Creating state storage for a circuit
16
+ # state = CompatibilityState.new(
17
+ # data_store: redis_store,
18
+ # config: circuit_config
19
+ # )
20
+ # state.set_state(State::LOCKED_RED)
21
+ # snapshot = state.state_snapshot
22
+ #
23
+ class CompatibilityState < Domain::Storage::State
24
+ # @!attribute data_store
25
+ # @return [Stoplight::Domain::DataStore]
26
+ private attr_reader :data_store
27
+
28
+ # @!attribute config
29
+ # @return [Stoplight::Domain::Config]
30
+ private attr_reader :config
31
+
32
+ # @param data_store [Stoplight::Domain::DataStore]
33
+ # @param config [Stoplight::Domain::Config]
34
+ def initialize(data_store:, config:)
35
+ @data_store = data_store
36
+ @config = config
37
+ end
38
+
39
+ # @return [Stoplight::Domain::StateSnapshot]
40
+ def state_snapshot = data_store.get_state_snapshot(config)
41
+
42
+ # @param state [String]
43
+ # @return [String]
44
+ def set_state(state) = data_store.set_state(config, state)
45
+
46
+ # @param color [String]
47
+ # @return [Boolean]
48
+ def transition_to_color(color) = data_store.transition_to_color(config, color)
49
+
50
+ # @return [void]
51
+ def clear = data_store.delete_light(config)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -9,7 +9,7 @@ RSpec.shared_examples "a generic notifier" do
9
9
  it "is initially the default" do
10
10
  formatter = nil
11
11
  expect(described_class.new(nil, formatter).formatter)
12
- .to eql(Stoplight::Default::FORMATTER)
12
+ .to eql(Stoplight::Wiring::Default::FORMATTER)
13
13
  end
14
14
 
15
15
  it "reads the formatter" do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- VERSION = Gem::Version.new("5.3.8")
4
+ VERSION = Gem::Version.new("5.7.0")
5
5
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module DataStore
6
+ # @abstract
7
+ class Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module DataStore
6
+ class Memory < Base
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module DataStore
6
+ class Redis < Base
7
+ # @!attribute redis
8
+ # @return [::Redis, ConnectionPool<::Redis>]
9
+ attr_reader :redis
10
+
11
+ # @!attribute warn_on_clock_skew
12
+ # @return [Boolean]
13
+ attr_reader :warn_on_clock_skew
14
+
15
+ # @param redis [::Redis, ConnectionPool<::Redis>]
16
+ # @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
17
+ # the application server
18
+ def initialize(redis, warn_on_clock_skew: true)
19
+ @warn_on_clock_skew = warn_on_clock_skew
20
+ @redis = redis
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module Default
6
+ COOL_OFF_TIME = 60.0
7
+
8
+ DATA_STORE = Stoplight::DataStore::Memory.new
9
+
10
+ ERROR_NOTIFIER = ->(error) { warn error }
11
+
12
+ FORMATTER = Infrastructure::Notifier::Generic::DEFAULT_FORMATTER
13
+
14
+ NOTIFIERS = [Infrastructure::Notifier::IO.new($stderr)].freeze
15
+
16
+ THRESHOLD = 3
17
+ RECOVERY_THRESHOLD = 1
18
+
19
+ WINDOW_SIZE = nil
20
+
21
+ TRACKED_ERRORS = [StandardError].freeze
22
+ SKIPPED_ERRORS = [].freeze
23
+
24
+ TRAFFIC_CONTROL = Domain::TrafficControl::ConsecutiveErrors.new
25
+ TRAFFIC_RECOVERY = Domain::TrafficRecovery::ConsecutiveSuccesses.new
26
+ end
27
+ end
28
+ end
@@ -1,29 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module Stoplight
6
- module Config
7
- # Represents user-defined default configuration for Stoplight.
8
- #
9
- # This class allows users to define default settings for various Stoplight
10
- # parameters, such as cool-off time, data store, error notifier, and more.
11
- # TODO: add evaluation/recovery strategy support
12
- class UserDefaultConfig
13
- extend Forwardable
14
-
4
+ module Wiring
5
+ # User-facing configuration interface
6
+ class DefaultConfiguration
15
7
  # @!attribute [w] cool_off_time
16
8
  # @return [Integer, nil] The default cool-off time in seconds.
17
9
  attr_writer :cool_off_time
18
10
 
19
- # @!attribute [w] error_notifier
20
- # @return [Proc, nil] The default error notifier (callable object).
21
- attr_writer :error_notifier
22
-
23
- # @!attribute [rw] notifiers
24
- # @return [Array<Stoplight::Notifier::Base>] The default list of notifiers.
25
- attr_accessor :notifiers
26
-
27
11
  # @!attribute [w] threshold
28
12
  # @return [Integer, Float, nil] The default failure threshold to trip the circuit breaker.
29
13
  attr_writer :threshold
@@ -44,14 +28,26 @@ module Stoplight
44
28
  # @return [Array<Class>, nil] The default list of errors to skip.
45
29
  attr_writer :skipped_errors
46
30
 
47
- # @!attribute [w] data_store
48
- # @return [Stoplight::DataStore::Base] The default data store instance.
49
- attr_writer :data_store
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::Wiring::DataStore::Base] The default data store instance.
41
+ attr_accessor :data_store
50
42
 
51
43
  # @!attribute [w] traffic_control
52
- # @return [Stoplight::TrafficControl::Base, Symbol, Hash] The traffic control strategy.
44
+ # @return [Stoplight::Domain::TrafficControl::Base] The traffic control strategy.
53
45
  attr_writer :traffic_control
54
46
 
47
+ # @!attribute [w] traffic_recovery
48
+ # @return [Stoplight::Domain::TrafficRecovery::Base] The traffic recovery strategy.
49
+ attr_writer :traffic_recovery
50
+
55
51
  def initialize
56
52
  # This allows users appending notifiers to the default list,
57
53
  # while still allowing them to override the default list.
@@ -65,21 +61,18 @@ module Stoplight
65
61
  def to_h
66
62
  {
67
63
  cool_off_time: @cool_off_time,
68
- data_store: @data_store,
69
- error_notifier: @error_notifier,
70
- notifiers: @notifiers,
71
64
  threshold: @threshold,
72
65
  recovery_threshold: @recovery_threshold,
73
66
  window_size: @window_size,
74
67
  tracked_errors: @tracked_errors,
75
68
  skipped_errors: @skipped_errors,
76
- traffic_control: @traffic_control
69
+ data_store: @data_store,
70
+ error_notifier: @error_notifier,
71
+ notifiers: @notifiers,
72
+ traffic_control: @traffic_control,
73
+ traffic_recovery: @traffic_recovery
77
74
  }.compact
78
75
  end
79
-
80
- # @return [Boolean] True if the configuration hash is not empty, false otherwise.
81
- # @api private
82
- def_delegator :to_h, :any?
83
76
  end
84
77
  end
85
78
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ # Builds the default LightFactory from user-provided configuration which is
6
+ # used as the basis for all circuit breakers.
7
+ #
8
+ class DefaultFactoryBuilder
9
+ # @!attribute [r] configuration
10
+ # @return [Stoplight::Wiring::DefaultConfiguration]
11
+ #
12
+ attr_reader :configuration
13
+
14
+ def initialize
15
+ @configuration = DefaultConfiguration.new
16
+ end
17
+
18
+ # @return [Stoplight::Wiring::LightFactory]
19
+ # @api private the method is used internally by Stoplight
20
+ def build
21
+ LightFactory.new(configuration.to_h)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module Light
6
+ # Provides default settings for the Stoplight library.
7
+ # @api private
8
+ DefaultConfig = Domain::Config.empty.with(
9
+ cool_off_time: Default::COOL_OFF_TIME,
10
+ threshold: Default::THRESHOLD,
11
+ recovery_threshold: Default::RECOVERY_THRESHOLD,
12
+ window_size: Default::WINDOW_SIZE,
13
+ tracked_errors: Default::TRACKED_ERRORS,
14
+ skipped_errors: Default::SKIPPED_ERRORS
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Wiring
5
+ module Light
6
+ SystemConfig = DefaultConfig.with(
7
+ recovery_threshold: 3
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,185 @@
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
+ # @!attribute data_store_config
38
+ # @return [Stoplight::DataStore::Bose]
39
+ private attr_reader :data_store_config
40
+
41
+ # @!attribute error_notifier
42
+ # @return [Proc]
43
+ private attr_reader :error_notifier
44
+
45
+ # @!attribute traffic_recovery
46
+ # @return [Stoplight::Domain::TrafficRecovery::Base]
47
+ private attr_reader :traffic_recovery
48
+
49
+ # @!attribute traffic_control
50
+ # @return [Stoplight::Domain::TrafficControl::Base]
51
+ private attr_reader :traffic_control
52
+
53
+ # @!attribute config
54
+ # @return [Stoplight::Domain::Config]
55
+ private attr_reader :config
56
+
57
+ # @!attribute factory
58
+ # @return [Stoplight::Domain::LightFactory]
59
+ private attr_reader :factory
60
+
61
+ def initialize(settings)
62
+ @notifiers = settings[:notifiers]
63
+ @data_store_config = settings[:data_store]
64
+ @error_notifier = settings[:error_notifier]
65
+ @traffic_recovery = settings[:traffic_recovery]
66
+ @traffic_control = settings[:traffic_control]
67
+ @config = settings[:config]
68
+ @factory = settings[:factory]
69
+ end
70
+
71
+ def build
72
+ Stoplight::Domain::Light.new(
73
+ config,
74
+ state_store:,
75
+ green_run_strategy:,
76
+ yellow_run_strategy:,
77
+ red_run_strategy:,
78
+ factory:
79
+ )
80
+ end
81
+
82
+ private def state_store = Stoplight::Infrastructure::Storage::CompatibilityState.new(config:, data_store:)
83
+
84
+ # @return [<Stoplight::Notifier::Base>]
85
+ private def notifiers
86
+ Array(@notifiers).map do |notifier|
87
+ Infrastructure::Notifier::FailSafe.new(notifier:, error_notifier:)
88
+ end
89
+ end
90
+
91
+ private def redis_recovery_lock_store
92
+ Infrastructure::DataStore::Redis::RecoveryLockStore.new(
93
+ redis: data_store_config.redis,
94
+ lock_timeout: config.cool_off_time_in_milliseconds,
95
+ scripting:
96
+ )
97
+ end
98
+
99
+ private def scripting = Infrastructure::DataStore::Redis::Scripting.new(redis: data_store_config.redis)
100
+
101
+ private def memory_recovery_lock_store
102
+ Infrastructure::DataStore::Memory::RecoveryLockStore.new
103
+ end
104
+
105
+ private def failover_data_store
106
+ create_data_store(FAILOVER_DATA_STORE_CONFIG)
107
+ end
108
+
109
+ private def data_store
110
+ create_data_store(data_store_config)
111
+ end
112
+
113
+ private def metrics_store
114
+ Stoplight::Infrastructure::Storage::CompatibilityMetrics.new(config:, data_store:)
115
+ end
116
+
117
+ private def recovery_lock_store
118
+ Stoplight::Infrastructure::Storage::CompatibilityRecoveryLock.new(config:, data_store:)
119
+ end
120
+
121
+ private def request_tracker
122
+ Domain::Tracker::Request.new(traffic_control:, notifiers:, config:, metrics_store:, state_store:)
123
+ end
124
+
125
+ private def recovery_probe_tracker
126
+ Domain::Tracker::RecoveryProbe.new(
127
+ traffic_recovery:,
128
+ notifiers:,
129
+ config:,
130
+ metrics_store: recovery_metrics_store,
131
+ state_store:
132
+ )
133
+ end
134
+
135
+ private def recovery_metrics_store
136
+ Stoplight::Infrastructure::Storage::CompatibilityRecoveryMetrics.new(config:, data_store:)
137
+ end
138
+
139
+ private def green_run_strategy
140
+ Domain::Strategies::GreenRunStrategy.new(config:, request_tracker:)
141
+ end
142
+
143
+ private def yellow_run_strategy
144
+ Domain::Strategies::YellowRunStrategy.new(
145
+ config:,
146
+ notifiers:,
147
+ request_tracker: recovery_probe_tracker,
148
+ red_run_strategy:,
149
+ state_store:,
150
+ metrics_store:,
151
+ recovery_lock_store:
152
+ )
153
+ end
154
+
155
+ private def red_run_strategy
156
+ Domain::Strategies::RedRunStrategy.new(config:)
157
+ end
158
+
159
+ private def create_data_store(data_store_config)
160
+ case data_store_config
161
+ in Stoplight::DataStore::Memory
162
+ memory_registry.compute_if_absent(data_store_config.object_id) do
163
+ Infrastructure::DataStore::Memory.new(
164
+ recovery_lock_store: memory_recovery_lock_store
165
+ )
166
+ end
167
+ in Stoplight::DataStore::Redis
168
+ Infrastructure::DataStore::FailSafe.new(
169
+ data_store: Stoplight::Infrastructure::DataStore::Redis.new(
170
+ redis: data_store_config.redis,
171
+ warn_on_clock_skew: data_store_config.warn_on_clock_skew,
172
+ recovery_lock_store: redis_recovery_lock_store,
173
+ scripting:
174
+ ),
175
+ error_notifier:,
176
+ failover_data_store:,
177
+ circuit_breaker: Stoplight.system_light("data_store:fail_safe:redis")
178
+ )
179
+ end
180
+ end
181
+
182
+ private def memory_registry = MEMORY_REGISTRY
183
+ end
184
+ end
185
+ end