stoplight 5.5.0 → 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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/stoplight/admin/actions/remove.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +6 -1
- data/lib/stoplight/admin/helpers.rb +10 -5
- data/lib/stoplight/admin/lights_repository.rb +26 -14
- data/lib/stoplight/admin/views/_card.erb +13 -1
- data/lib/stoplight/admin.rb +9 -0
- data/lib/stoplight/common/deprecations.rb +11 -0
- data/lib/stoplight/domain/config.rb +5 -1
- data/lib/stoplight/domain/data_store.rb +58 -6
- data/lib/stoplight/domain/failure.rb +2 -0
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
- data/lib/stoplight/domain/light.rb +34 -24
- data/lib/stoplight/domain/metrics.rb +64 -0
- data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
- data/lib/stoplight/domain/{metadata.rb → state_snapshot.rb} +29 -37
- data/lib/stoplight/domain/storage/metrics.rb +42 -0
- data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
- data/lib/stoplight/domain/storage/state.rb +87 -0
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +2 -2
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +3 -3
- data/lib/stoplight/domain/strategies/run_strategy.rb +2 -7
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +63 -36
- data/lib/stoplight/domain/tracker/base.rb +0 -29
- data/lib/stoplight/domain/tracker/recovery_probe.rb +26 -22
- data/lib/stoplight/domain/tracker/request.rb +26 -21
- data/lib/stoplight/domain/traffic_control/base.rb +5 -5
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +3 -7
- data/lib/stoplight/domain/traffic_control/error_rate.rb +3 -3
- data/lib/stoplight/domain/traffic_recovery/base.rb +5 -5
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +4 -8
- data/lib/stoplight/domain/traffic_recovery.rb +0 -1
- data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
- data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
- data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
- data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
- data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +163 -132
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/get_metrics.lua +26 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
- data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
- data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
- data/lib/stoplight/infrastructure/data_store/redis.rb +211 -165
- data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
- data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
- data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/data_store/base.rb +11 -0
- data/lib/stoplight/wiring/data_store/memory.rb +10 -0
- data/lib/stoplight/wiring/data_store/redis.rb +25 -0
- data/lib/stoplight/wiring/default.rb +1 -1
- data/lib/stoplight/wiring/default_configuration.rb +1 -1
- data/lib/stoplight/wiring/default_factory_builder.rb +1 -1
- data/lib/stoplight/wiring/light_builder.rb +185 -0
- data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
- data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
- data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
- data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
- data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
- data/lib/stoplight/wiring/light_factory.rb +45 -132
- data/lib/stoplight/wiring/notifier_factory.rb +26 -0
- data/lib/stoplight/wiring/public_api.rb +3 -2
- data/lib/stoplight.rb +18 -3
- metadata +55 -16
- data/lib/stoplight/infrastructure/data_store/redis/get_metadata.lua +0 -38
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
- data/lib/stoplight/wiring/container.rb +0 -80
- data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -123
- data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
- data/lib/stoplight/wiring/system_container.rb +0 -9
- data/lib/stoplight/wiring/system_light_factory.rb +0 -17
- /data/lib/stoplight/infrastructure/data_store/redis/{record_failure.lua → lua_scripts/record_failure.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{record_success.lua → lua_scripts/record_success.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_green.lua → lua_scripts/transition_to_green.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_red.lua → lua_scripts/transition_to_red.lua} +0 -0
- /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_yellow.lua → lua_scripts/transition_to_yellow.lua} +0 -0
|
@@ -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
|
data/lib/stoplight/version.rb
CHANGED
|
@@ -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
|
|
@@ -37,7 +37,7 @@ module Stoplight
|
|
|
37
37
|
attr_accessor :notifiers
|
|
38
38
|
|
|
39
39
|
# @!attribute [rw] data_store
|
|
40
|
-
# @return [Stoplight::
|
|
40
|
+
# @return [Stoplight::Wiring::DataStore::Base] The default data store instance.
|
|
41
41
|
attr_accessor :data_store
|
|
42
42
|
|
|
43
43
|
# @!attribute [w] traffic_control
|
|
@@ -18,7 +18,7 @@ module Stoplight
|
|
|
18
18
|
# @return [Stoplight::Wiring::LightFactory]
|
|
19
19
|
# @api private the method is used internally by Stoplight
|
|
20
20
|
def build
|
|
21
|
-
LightFactory.new(
|
|
21
|
+
LightFactory.new(configuration.to_h)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class LightFactory
|
|
6
|
+
# Validates that traffic control and recovery strategies are
|
|
7
|
+
# compatible with the provided configuration.
|
|
8
|
+
#
|
|
9
|
+
# Different strategies have different configuration requirements:
|
|
10
|
+
# - ErrorRate requires window_size and threshold ∈ [0,1]
|
|
11
|
+
# - ConsecutiveErrors requires threshold > 0
|
|
12
|
+
# - ConsecutiveSuccesses requires recovery_threshold > 0
|
|
13
|
+
#
|
|
14
|
+
# @raise [Stoplight::Error::ConfigurationError] if incompatible
|
|
15
|
+
class CompatibilityValidator
|
|
16
|
+
private attr_reader :dependencies
|
|
17
|
+
private attr_reader :config
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def call(config, dependencies) = new(config, dependencies).call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(config, dependencies)
|
|
24
|
+
@config = config
|
|
25
|
+
@dependencies = dependencies
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
validate_traffic_control!
|
|
30
|
+
validate_traffic_recovery!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def validate_traffic_control!
|
|
34
|
+
traffic_control = dependencies.fetch(:traffic_control)
|
|
35
|
+
traffic_control.check_compatibility(config).then do |compatibility_result|
|
|
36
|
+
if compatibility_result.incompatible?
|
|
37
|
+
raise Domain::Error::ConfigurationError,
|
|
38
|
+
"#{traffic_control.class.name} incompatible with config: #{compatibility_result.error_messages}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_traffic_recovery!
|
|
44
|
+
traffic_recovery = dependencies.fetch(:traffic_recovery)
|
|
45
|
+
traffic_recovery.check_compatibility(config).then do |compatibility_result|
|
|
46
|
+
if compatibility_result.incompatible?
|
|
47
|
+
raise Domain::Error::ConfigurationError,
|
|
48
|
+
"#{traffic_recovery.class.name} incompatible with config: #{compatibility_result.error_messages}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class LightFactory
|
|
6
|
+
# Normalizes user-provided configuration values into canonical forms.
|
|
7
|
+
#
|
|
8
|
+
# Handles common user convenience patterns:
|
|
9
|
+
# - Single error class → Array of error classes
|
|
10
|
+
# - Numeric cool_off_time → Integer
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# config = Config.empty.with(
|
|
14
|
+
# tracked_errors: StandardError, # Single class
|
|
15
|
+
# cool_off_time: 30.5 # Float
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# normalized = ConfigNormalizer.call(config)
|
|
19
|
+
# normalized.tracked_errors #=> [StandardError] # Array
|
|
20
|
+
# normalized.cool_off_time #=> 30 # Integer
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class ConfigNormalizer
|
|
24
|
+
class << self
|
|
25
|
+
def call(config) = new(config).call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @!attribute config
|
|
29
|
+
# @return [Stoplight::Domain::Config]
|
|
30
|
+
private attr_reader :config
|
|
31
|
+
|
|
32
|
+
# @param [Stoplight::Domain::Config]
|
|
33
|
+
def initialize(config)
|
|
34
|
+
@config = config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Stoplight::Domain::Config]
|
|
38
|
+
def call
|
|
39
|
+
config
|
|
40
|
+
.then { |c| normalize_tracked_errors(c) }
|
|
41
|
+
.then { |c| normalize_skipped_errors(c) }
|
|
42
|
+
.then { |c| normalize_cool_off_time(c) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def normalize_tracked_errors(config)
|
|
46
|
+
if config.tracked_errors.is_a?(Array)
|
|
47
|
+
config
|
|
48
|
+
else
|
|
49
|
+
config.with(tracked_errors: Array(config.tracked_errors))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def normalize_skipped_errors(config)
|
|
54
|
+
if config.skipped_errors.is_a?(Array)
|
|
55
|
+
config
|
|
56
|
+
else
|
|
57
|
+
config.with(skipped_errors: Array(config.skipped_errors))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private def normalize_cool_off_time(config)
|
|
62
|
+
if config.cool_off_time.is_a?(Integer)
|
|
63
|
+
config
|
|
64
|
+
else
|
|
65
|
+
config.with(cool_off_time: config.cool_off_time.to_i)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class LightFactory
|
|
6
|
+
# Orchestrates DSL interpretation, normalization, and validation.
|
|
7
|
+
#
|
|
8
|
+
# ConfigurationPipeline is the entry point for transforming raw user settings
|
|
9
|
+
# into validated domain objects. It coordinates three steps:
|
|
10
|
+
#
|
|
11
|
+
# 1. Normalization - Convert user-friendly values to canonical forms
|
|
12
|
+
# 2. DSL Interpretation - Transform symbols/hashes into strategy objects
|
|
13
|
+
# 3. Validation - Ensure strategies are compatible with configuration
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
class ConfigurationPipeline
|
|
17
|
+
BASE_DEPENDENCIES = {
|
|
18
|
+
data_store: Default::DATA_STORE,
|
|
19
|
+
traffic_recovery: Default::TRAFFIC_RECOVERY,
|
|
20
|
+
traffic_control: Default::TRAFFIC_CONTROL,
|
|
21
|
+
notifiers: Default::NOTIFIERS,
|
|
22
|
+
error_notifier: Default::ERROR_NOTIFIER
|
|
23
|
+
}.freeze
|
|
24
|
+
private_constant :BASE_DEPENDENCIES
|
|
25
|
+
|
|
26
|
+
private attr_reader :dependency_settings
|
|
27
|
+
private attr_reader :config_settings
|
|
28
|
+
|
|
29
|
+
def self.process(config_settings, dependency_settings)
|
|
30
|
+
new(config_settings, dependency_settings).process
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(config_settings, dependency_settings)
|
|
34
|
+
@config_settings = config_settings
|
|
35
|
+
@dependency_settings = dependency_settings
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process
|
|
39
|
+
config = build_config
|
|
40
|
+
dependencies = build_dependencies
|
|
41
|
+
|
|
42
|
+
CompatibilityValidator.call(config, dependencies)
|
|
43
|
+
|
|
44
|
+
[config, dependencies]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_config
|
|
48
|
+
base_config
|
|
49
|
+
.with(**config_settings)
|
|
50
|
+
.then { |cfg| ConfigNormalizer.call(cfg) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_dependencies
|
|
54
|
+
base_dependencies
|
|
55
|
+
.merge(dependency_settings)
|
|
56
|
+
.then { |deps| interpret_dsl(deps) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def interpret_dsl(dependencies)
|
|
60
|
+
dependencies.merge(
|
|
61
|
+
traffic_control: TrafficControlDsl.call(dependencies.fetch(:traffic_control)),
|
|
62
|
+
traffic_recovery: TrafficRecoveryDsl.call(dependencies.fetch(:traffic_recovery))
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def base_config = Light::DefaultConfig
|
|
67
|
+
|
|
68
|
+
def base_dependencies = BASE_DEPENDENCIES
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|