stoplight 5.4.0 → 5.5.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/views/layout.erb +3 -3
- data/lib/stoplight/admin.rb +4 -4
- data/lib/stoplight/domain/color.rb +11 -0
- data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
- data/lib/stoplight/domain/config.rb +55 -0
- data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
- data/lib/stoplight/domain/error.rb +42 -0
- data/lib/stoplight/domain/failure.rb +42 -0
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
- data/lib/stoplight/domain/light.rb +198 -0
- data/lib/stoplight/domain/light_factory.rb +75 -0
- data/lib/stoplight/domain/metadata.rb +65 -0
- data/lib/stoplight/domain/state.rb +11 -0
- data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
- data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
- data/lib/stoplight/domain/tracker/base.rb +41 -0
- data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
- data/lib/stoplight/domain/tracker/request.rb +67 -0
- data/lib/stoplight/domain/traffic_control/base.rb +74 -0
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
- data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
- data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
- data/lib/stoplight/domain/traffic_recovery.rb +13 -0
- data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
- data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
- data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
- data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
- data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
- data/lib/stoplight/rspec/generic_notifier.rb +1 -1
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/container.rb +80 -0
- data/lib/stoplight/wiring/default.rb +28 -0
- data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
- data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
- data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +22 -11
- data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
- data/lib/stoplight/wiring/light/default_config.rb +18 -0
- data/lib/stoplight/wiring/light/system_config.rb +11 -0
- data/lib/stoplight/wiring/light_factory.rb +188 -0
- data/lib/stoplight/wiring/public_api.rb +28 -0
- data/lib/stoplight/wiring/system_container.rb +9 -0
- data/lib/stoplight/wiring/system_light_factory.rb +17 -0
- data/lib/stoplight.rb +38 -28
- metadata +53 -43
- data/lib/stoplight/color.rb +0 -9
- data/lib/stoplight/config/dsl.rb +0 -97
- data/lib/stoplight/config/library_default_config.rb +0 -21
- data/lib/stoplight/config/system_config.rb +0 -10
- data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
- data/lib/stoplight/data_store/memory.rb +0 -285
- data/lib/stoplight/data_store/redis/lua.rb +0 -23
- data/lib/stoplight/data_store/redis.rb +0 -446
- data/lib/stoplight/data_store.rb +0 -6
- data/lib/stoplight/default.rb +0 -30
- data/lib/stoplight/error.rb +0 -39
- data/lib/stoplight/failure.rb +0 -71
- data/lib/stoplight/light/config.rb +0 -112
- data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
- data/lib/stoplight/light/green_run_strategy.rb +0 -54
- data/lib/stoplight/light/red_run_strategy.rb +0 -31
- data/lib/stoplight/light/run_strategy.rb +0 -32
- data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
- data/lib/stoplight/light.rb +0 -191
- data/lib/stoplight/metadata.rb +0 -99
- data/lib/stoplight/notifier/generic.rb +0 -79
- data/lib/stoplight/notifier/io.rb +0 -21
- data/lib/stoplight/notifier/logger.rb +0 -19
- data/lib/stoplight/state.rb +0 -9
- data/lib/stoplight/traffic_control/base.rb +0 -70
- data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
- data/lib/stoplight/traffic_control/error_rate.rb +0 -49
- data/lib/stoplight/traffic_recovery/base.rb +0 -75
- data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
- data/lib/stoplight/traffic_recovery.rb +0 -11
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
|
@@ -3,32 +3,41 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module Stoplight
|
|
6
|
-
module
|
|
6
|
+
module Wiring
|
|
7
7
|
# A wrapper around a data store that provides fail-safe mechanisms using a
|
|
8
8
|
# circuit breaker. It ensures that operations on the data store can gracefully
|
|
9
9
|
# handle failures by falling back to default values when necessary.
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
|
-
class
|
|
12
|
+
class FailSafeDataStore < Domain::DataStore
|
|
13
13
|
class << self
|
|
14
14
|
# Wraps a data store with fail-safe mechanisms.
|
|
15
15
|
#
|
|
16
16
|
# @param data_store [Stoplight::DataStore::Base] The data store to wrap.
|
|
17
|
+
# @param error_notifier [Proc] called when wrapped data store fails
|
|
17
18
|
# @return [Stoplight::DataStore::Base, FailSafe] The original data store if it is already
|
|
18
19
|
# a +Memory+ or +FailSafe+ instance, otherwise a new +FailSafe+ instance.
|
|
19
|
-
def wrap(data_store)
|
|
20
|
+
def wrap(data_store:, error_notifier:)
|
|
20
21
|
case data_store
|
|
21
|
-
|
|
22
|
+
in Infrastructure::DataStore::Memory
|
|
22
23
|
data_store
|
|
24
|
+
in self if data_store.error_notifier == error_notifier
|
|
25
|
+
data_store
|
|
26
|
+
in self
|
|
27
|
+
new(data_store: data_store.data_store, error_notifier:)
|
|
23
28
|
else
|
|
24
|
-
new(data_store)
|
|
29
|
+
new(data_store:, error_notifier:)
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
# @!attribute data_store
|
|
30
35
|
# @return [Stoplight::DataStore::Base] The underlying primary data store being used
|
|
31
|
-
|
|
36
|
+
attr_reader :data_store
|
|
37
|
+
|
|
38
|
+
# @!attribute error_notifier
|
|
39
|
+
# @return [Proc]
|
|
40
|
+
attr_reader :error_notifier
|
|
32
41
|
|
|
33
42
|
# @!attribute failover_data_store
|
|
34
43
|
# @return [Stoplight::DataStore::Base] The fallback data store used when the primary fails.
|
|
@@ -38,11 +47,13 @@ module Stoplight
|
|
|
38
47
|
# @return [Stoplight::Light] The circuit breaker used to handle data store failures.
|
|
39
48
|
private attr_reader :circuit_breaker
|
|
40
49
|
|
|
41
|
-
# @param data_store [Stoplight::DataStore
|
|
42
|
-
|
|
50
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
51
|
+
# @param error_notifier [Proc]
|
|
52
|
+
def initialize(data_store:, error_notifier:, failover_data_store: Wiring::Default::DATA_STORE)
|
|
43
53
|
@data_store = data_store
|
|
54
|
+
@error_notifier = error_notifier
|
|
44
55
|
@failover_data_store = failover_data_store
|
|
45
|
-
@circuit_breaker = Stoplight.system_light("
|
|
56
|
+
@circuit_breaker = Stoplight.system_light("data_store:fail_safe:#{data_store.class.name}")
|
|
46
57
|
end
|
|
47
58
|
|
|
48
59
|
def names
|
|
@@ -94,14 +105,14 @@ module Stoplight
|
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
def ==(other)
|
|
97
|
-
other.is_a?(self.class) && other.data_store == data_store
|
|
108
|
+
other.is_a?(self.class) && other.data_store == data_store && other.error_notifier == error_notifier
|
|
98
109
|
end
|
|
99
110
|
|
|
100
111
|
# @param method_name [Symbol] protected method name
|
|
101
112
|
private def with_fallback(method_name, *args, **kwargs, &code)
|
|
102
113
|
fallback = proc do |error|
|
|
103
114
|
config = args.first
|
|
104
|
-
|
|
115
|
+
error_notifier.call(error) if config && error
|
|
105
116
|
@failover_data_store.public_send(method_name, *args, **kwargs)
|
|
106
117
|
end
|
|
107
118
|
|
|
@@ -1,50 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stoplight
|
|
4
|
-
module
|
|
4
|
+
module Wiring
|
|
5
5
|
# A wrapper around a notifier that provides fail-safe mechanisms using a
|
|
6
6
|
# circuit breaker. It ensures that a notification can gracefully
|
|
7
7
|
# handle failures.
|
|
8
8
|
#
|
|
9
9
|
# @api private
|
|
10
|
-
class
|
|
10
|
+
class FailSafeNotifier < Domain::StateTransitionNotifier
|
|
11
11
|
# @!attribute [r] notifier
|
|
12
|
-
# @return [Stoplight::
|
|
13
|
-
|
|
12
|
+
# @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
|
|
13
|
+
attr_reader :notifier
|
|
14
|
+
|
|
15
|
+
# @!attribute [r] error_notifier
|
|
16
|
+
# @return [Stoplight::Domain::StateTransitionNotifier] The underlying notifier being wrapped.
|
|
17
|
+
attr_reader :error_notifier
|
|
14
18
|
|
|
15
19
|
class << self
|
|
16
20
|
# Wraps a notifier with fail-safe mechanisms.
|
|
17
21
|
#
|
|
18
|
-
# @param notifier [Stoplight::
|
|
22
|
+
# @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
|
|
23
|
+
# @param error_notifier [Proc] called when wrapped data store fails
|
|
19
24
|
# @return [Stoplight::Notifier::FailSafe] The original notifier if it is already
|
|
20
25
|
# a +FailSafe+ instance, otherwise a new +FailSafe+ instance.
|
|
21
|
-
def wrap(notifier)
|
|
26
|
+
def wrap(notifier:, error_notifier:)
|
|
22
27
|
case notifier
|
|
23
|
-
|
|
28
|
+
in self if notifier.error_notifier == error_notifier
|
|
24
29
|
notifier
|
|
30
|
+
in self
|
|
31
|
+
new(notifier: notifier.notifier, error_notifier:)
|
|
25
32
|
else
|
|
26
|
-
new(notifier)
|
|
33
|
+
new(notifier:, error_notifier:)
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
# Initializes a new instance of the +FailSafe+ class.
|
|
32
39
|
#
|
|
33
|
-
# @param notifier [Stoplight::
|
|
34
|
-
|
|
40
|
+
# @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
|
|
41
|
+
# @param error_notifier [Proc] called when wrapped data store fails
|
|
42
|
+
def initialize(notifier:, error_notifier:)
|
|
35
43
|
@notifier = notifier
|
|
44
|
+
@error_notifier = error_notifier
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
# Sends a notification using the wrapped notifier with fail-safe mechanisms.
|
|
39
48
|
#
|
|
40
|
-
# @param config [Stoplight::
|
|
49
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
41
50
|
# @param from_color [String] The initial color of the light.
|
|
42
51
|
# @param to_color [String] The target color of the light.
|
|
43
52
|
# @param error [Exception, nil] An optional error to include in the notification.
|
|
44
53
|
# @return [void]
|
|
45
54
|
def notify(config, from_color, to_color, error = nil)
|
|
46
55
|
fallback = proc do |exception|
|
|
47
|
-
|
|
56
|
+
error_notifier.call(exception) if exception
|
|
48
57
|
nil
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -55,7 +64,7 @@ module Stoplight
|
|
|
55
64
|
|
|
56
65
|
# @return [Boolean]
|
|
57
66
|
def ==(other)
|
|
58
|
-
other.is_a?(
|
|
67
|
+
other.is_a?(self.class) && notifier == other.notifier
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
# @return [Stoplight::Light] The circuit breaker used to handle failures.
|
|
@@ -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,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
# Concrete factory for building +Stoplight::Light++ instances with full dependency wiring.
|
|
6
|
+
#
|
|
7
|
+
# This factory implements the +Stoplight::Domain::LightFactory+ protocol. It knows how to:
|
|
8
|
+
# 1. Parse and transform user-provided settings
|
|
9
|
+
# 2. Wire together all Light dependencies using a DI container
|
|
10
|
+
# 3. Validate configuration compatibility
|
|
11
|
+
# 4. Construct fully-functional Light instances
|
|
12
|
+
#
|
|
13
|
+
# @see Stoplight::Domain::LightFactory
|
|
14
|
+
# @see Stoplight()
|
|
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
|
|
27
|
+
end
|
|
28
|
+
|
|
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)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Builds a fully-configured Light instance.
|
|
47
|
+
#
|
|
48
|
+
# The method resolves all dependencies from the container and constructs a Light that's
|
|
49
|
+
# ready to use. The Light is injected with a reference to this factory, allowing the
|
|
50
|
+
# +Stoplight::Light#with+ method to work for reconfiguration.
|
|
51
|
+
#
|
|
52
|
+
# @return [Stoplight::Light] Configured circuit breaker
|
|
53
|
+
# @raise [Stoplight::Error::ConfigurationError] If configuration is invalid
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# factory = Stoplight::Wiring::LightFactory.new(container)
|
|
57
|
+
# light = factory.build
|
|
58
|
+
#
|
|
59
|
+
# # Light is ready to use
|
|
60
|
+
# light.run { api_call }
|
|
61
|
+
|
|
62
|
+
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
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
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)
|
|
85
|
+
end
|
|
86
|
+
|
|
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(", ")}"
|
|
93
|
+
end
|
|
94
|
+
|
|
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
|
|
100
|
+
end
|
|
101
|
+
|
|
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
|
|
110
|
+
|
|
111
|
+
if settings.key?(:cool_off_time)
|
|
112
|
+
settings[:cool_off_time] = normalize_cool_off_time(settings[:cool_off_time])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
|
120
|
+
|
|
121
|
+
if settings.key?(:traffic_recovery)
|
|
122
|
+
settings[:traffic_recovery] = apply_traffic_recovery_dsl(settings[:traffic_recovery])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
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
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
# Public API facade for backward compatibility and convenience
|
|
6
|
+
# @api public
|
|
7
|
+
module PublicApi
|
|
8
|
+
# Aliases for domain concepts
|
|
9
|
+
Color = Domain::Color
|
|
10
|
+
Error = Domain::Error
|
|
11
|
+
State = Domain::State
|
|
12
|
+
|
|
13
|
+
# Namespace aliases for data stores
|
|
14
|
+
module DataStore
|
|
15
|
+
Redis = Infrastructure::DataStore::Redis
|
|
16
|
+
Memory = Infrastructure::DataStore::Memory
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Namespace aliases for notifiers
|
|
20
|
+
module Notifier
|
|
21
|
+
Base = Domain::StateTransitionNotifier
|
|
22
|
+
Generic = Infrastructure::Notifier::Generic
|
|
23
|
+
IO = Infrastructure::Notifier::IO
|
|
24
|
+
Logger = Infrastructure::Notifier::Logger
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozon_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
# Factory for internal system lights used by the Stoplight itself.
|
|
6
|
+
#
|
|
7
|
+
# System lights are isolated from user configuration to prevent
|
|
8
|
+
# user settings from breaking the library's own circuit breakers.
|
|
9
|
+
# For example, the FailSafe data store wrapper uses a system light
|
|
10
|
+
# to protect against data store failures.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
SystemLightFactory = Wiring::LightFactory.new(
|
|
14
|
+
Wiring::SystemContainer.with(config: Light::SystemConfig)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/stoplight.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "zeitwerk"
|
|
4
4
|
|
|
5
5
|
loader = Zeitwerk::Loader.for_gem
|
|
6
|
-
loader.inflector.inflect("io" => "IO"
|
|
6
|
+
loader.inflector.inflect("io" => "IO")
|
|
7
7
|
loader.do_not_eager_load(
|
|
8
8
|
"#{__dir__}/stoplight/data_store",
|
|
9
9
|
"#{__dir__}/stoplight/admin",
|
|
@@ -14,24 +14,19 @@ loader.ignore("#{__dir__}/stoplight/rspec.rb", "#{__dir__}/stoplight/rspec")
|
|
|
14
14
|
loader.setup
|
|
15
15
|
|
|
16
16
|
module Stoplight # rubocop:disable Style/Documentation
|
|
17
|
-
|
|
18
|
-
private_constant :CONFIG_DSL
|
|
17
|
+
include Wiring::PublicApi
|
|
19
18
|
|
|
20
19
|
CONFIG_MUTEX = Mutex.new
|
|
21
20
|
private_constant :CONFIG_MUTEX
|
|
22
21
|
|
|
23
22
|
class << self
|
|
24
|
-
ALREADY_CONFIGURED_WARNING = "Stoplight must be configured only once"
|
|
25
|
-
private_constant :ALREADY_CONFIGURED_WARNING
|
|
26
|
-
|
|
27
23
|
# Configures the Stoplight library.
|
|
28
24
|
#
|
|
29
25
|
# This method allows you to set up the library's configuration using a block.
|
|
30
26
|
# It raises an error if called more than once.
|
|
31
27
|
#
|
|
32
28
|
# @yield [config] Provides a configuration object to the block.
|
|
33
|
-
# @yieldparam config [Stoplight::
|
|
34
|
-
# @raise [Stoplight::Error::ConfigurationError] If the library is already configured.
|
|
29
|
+
# @yieldparam config [Stoplight::Wiring::DefaultConfiguration] The configuration object.
|
|
35
30
|
# @return [void]
|
|
36
31
|
#
|
|
37
32
|
# @example
|
|
@@ -56,18 +51,12 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
|
56
51
|
# suppress this warning, which could be useful in test environments.
|
|
57
52
|
#
|
|
58
53
|
def configure(trust_me_im_an_engineer: false)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
reconfigured = !@default_config.nil?
|
|
54
|
+
warn_if_reconfiguring(trust_me_im_an_engineer) do
|
|
55
|
+
factory_builder = Wiring::DefaultFactoryBuilder.new
|
|
56
|
+
yield factory_builder.configuration if block_given?
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
warn(
|
|
67
|
-
"Stoplight reconfigured. Existing circuit breakers will not see new configuration. " \
|
|
68
|
-
"New configuration: #{@default_config.inspect}"
|
|
69
|
-
)
|
|
70
|
-
end
|
|
58
|
+
@default_configuration = factory_builder.configuration
|
|
59
|
+
@default_light_factory = factory_builder.build
|
|
71
60
|
end
|
|
72
61
|
end
|
|
73
62
|
|
|
@@ -78,8 +67,7 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
|
78
67
|
# @return [Stoplight::Light]
|
|
79
68
|
# @api private
|
|
80
69
|
def system_light(name, **settings)
|
|
81
|
-
|
|
82
|
-
Stoplight::Light.new(config)
|
|
70
|
+
Wiring::SystemLightFactory.build_with(name: "__stoplight__#{name}", **settings)
|
|
83
71
|
end
|
|
84
72
|
|
|
85
73
|
# Create a Light with the user default configuration.
|
|
@@ -89,18 +77,40 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
|
89
77
|
# @return [Stoplight::Light]
|
|
90
78
|
# @api private
|
|
91
79
|
def light(name, **settings)
|
|
92
|
-
|
|
93
|
-
Stoplight::Light.new(config)
|
|
80
|
+
__stoplight__default_light_factory.build_with(name:, **settings)
|
|
94
81
|
end
|
|
95
82
|
|
|
96
|
-
# Retrieves the current
|
|
83
|
+
# Retrieves the current default dependencies.
|
|
97
84
|
#
|
|
98
|
-
# @return [Stoplight::
|
|
85
|
+
# @return [Stoplight::Domain::LightFactory]
|
|
86
|
+
# @api private
|
|
87
|
+
def __stoplight__default_light_factory
|
|
88
|
+
ensure_configured
|
|
89
|
+
@default_light_factory
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def __stoplight__default_configuration
|
|
93
|
+
ensure_configured
|
|
94
|
+
@default_configuration
|
|
95
|
+
end
|
|
96
|
+
|
|
99
97
|
# @api private
|
|
100
|
-
def
|
|
98
|
+
private def ensure_configured
|
|
101
99
|
CONFIG_MUTEX.synchronize do
|
|
102
|
-
|
|
100
|
+
configure unless configured?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def warn_if_reconfiguring(trust_me_im_an_engineer)
|
|
105
|
+
if configured? && !trust_me_im_an_engineer
|
|
106
|
+
warn "Stoplight reconfigured. Existing circuit breakers will not see new configuration"
|
|
103
107
|
end
|
|
108
|
+
yield
|
|
109
|
+
@configured = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private def configured?
|
|
113
|
+
@configured == true
|
|
104
114
|
end
|
|
105
115
|
end
|
|
106
116
|
end
|
|
@@ -117,7 +127,7 @@ end
|
|
|
117
127
|
# @option settings [Numeric] :window_size The size of the rolling window for failure tracking.
|
|
118
128
|
# @option settings [Array<StandardError>] :tracked_errors A list of errors to track.
|
|
119
129
|
# @option settings [Array<Exception>] :skipped_errors A list of errors to skip.
|
|
120
|
-
# @option settings [
|
|
130
|
+
# @option settings [Symbol, {Symbol, Hash{Symbol, any}}] :traffic_control The
|
|
121
131
|
# traffic control strategy to use.
|
|
122
132
|
#
|
|
123
133
|
# @return [Stoplight::Light] A new circuit breaker instance.
|