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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
# Abstract factory protocol for building +Stoplight::Light+ instances.
|
|
6
|
+
#
|
|
7
|
+
# This defines the interface that any Light factory must implement,
|
|
8
|
+
# regardless of the underlying implementation details.
|
|
9
|
+
#
|
|
10
|
+
# This uses the Abstract Factory pattern to decouple the domain (Light)
|
|
11
|
+
# from the application layer (concrete factory implementation). Light
|
|
12
|
+
# doesn't know HOW it's built, only that it needs something that can
|
|
13
|
+
# build variants of itself.
|
|
14
|
+
#
|
|
15
|
+
# By defining this interface in the domain layer:
|
|
16
|
+
# - Light can request reconfiguration without knowing about containers
|
|
17
|
+
# - Different factory implementations can be swapped (code, database-configured, etc.)
|
|
18
|
+
# - Dependency direction is preserved (Application → Domain, not Domain → Application)
|
|
19
|
+
#
|
|
20
|
+
# @abstract Subclasses must implement +#with+ and +#build+
|
|
21
|
+
# @api private
|
|
22
|
+
class LightFactory
|
|
23
|
+
# Creates a new factory with modified settings.
|
|
24
|
+
#
|
|
25
|
+
# This method must return a NEW factory instance - it should not
|
|
26
|
+
# modify the current factory. The new factory should inherit all
|
|
27
|
+
# configuration from the current factory, with the provided
|
|
28
|
+
# settings overriding specific values.
|
|
29
|
+
#
|
|
30
|
+
# @param settings [Hash] Configuration and dependency overrides
|
|
31
|
+
# @return [Stoplight::Domain::LightFactory] New factory with updated settings
|
|
32
|
+
# @raise [NotImplementedError] Must be implemented by subclass
|
|
33
|
+
# @abstract
|
|
34
|
+
# :nocov:
|
|
35
|
+
def with(**settings)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Builds a +Stoplight::Light+ instance with the current configuration.
|
|
40
|
+
#
|
|
41
|
+
# This method must construct a fully-wired Light with all required
|
|
42
|
+
# dependencies (strategies, data store, notifiers, etc.). The Light
|
|
43
|
+
# should be ready to use immediately after construction.
|
|
44
|
+
#
|
|
45
|
+
# @return [Stoplight::Light] Configured circuit breaker instance
|
|
46
|
+
# @raise [NotImplementedError] Must be implemented by subclass
|
|
47
|
+
# @raise [Stoplight::Error::ConfigurationError] If configuration is invalid
|
|
48
|
+
# @abstract
|
|
49
|
+
def build
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
end
|
|
52
|
+
# :nocov:
|
|
53
|
+
|
|
54
|
+
# Convenience method to configure and build in one operation.
|
|
55
|
+
#
|
|
56
|
+
# This combines +#with+ and +#build+ into a single call, which is
|
|
57
|
+
# useful when you want to create a customized Light without keeping
|
|
58
|
+
# a reference to the intermediate factory.
|
|
59
|
+
#
|
|
60
|
+
# @param settings [Hash] Settings to override before building
|
|
61
|
+
# @return [Stoplight::Light] Configured circuit breaker instance
|
|
62
|
+
#
|
|
63
|
+
# @example Usage
|
|
64
|
+
# # Instead of:
|
|
65
|
+
# new_factory = factory.with(threshold: 10)
|
|
66
|
+
# light = new_factory.build
|
|
67
|
+
#
|
|
68
|
+
# # You can do:
|
|
69
|
+
# light = factory.build_with(threshold: 10)
|
|
70
|
+
def build_with(**settings)
|
|
71
|
+
with(**settings).build
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
# @api private
|
|
6
|
+
Metadata = Data.define(
|
|
7
|
+
:successes,
|
|
8
|
+
:errors,
|
|
9
|
+
:recovery_probe_successes,
|
|
10
|
+
:recovery_probe_errors,
|
|
11
|
+
:last_error_at,
|
|
12
|
+
:last_success_at,
|
|
13
|
+
:consecutive_errors,
|
|
14
|
+
:consecutive_successes,
|
|
15
|
+
:last_error,
|
|
16
|
+
:breached_at,
|
|
17
|
+
:locked_state,
|
|
18
|
+
:recovery_scheduled_after,
|
|
19
|
+
:recovery_started_at,
|
|
20
|
+
:recovered_at,
|
|
21
|
+
:current_time
|
|
22
|
+
) do
|
|
23
|
+
# YELLOW color could be entered implicitly through a timeout
|
|
24
|
+
# and explicitly through a transition.
|
|
25
|
+
#
|
|
26
|
+
# This method indicates whether the recovery has already started explicitly
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def recovery_started?
|
|
30
|
+
recovery_started_at && recovery_started_at <= current_time
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
|
|
34
|
+
def color
|
|
35
|
+
if locked_state == State::LOCKED_GREEN
|
|
36
|
+
Color::GREEN
|
|
37
|
+
elsif locked_state == State::LOCKED_RED
|
|
38
|
+
Color::RED
|
|
39
|
+
elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
|
|
40
|
+
Color::YELLOW
|
|
41
|
+
elsif breached_at
|
|
42
|
+
Color::RED
|
|
43
|
+
else
|
|
44
|
+
Color::GREEN
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Calculates the error rate based on the number of successes and errors.
|
|
49
|
+
#
|
|
50
|
+
# @return [Float]
|
|
51
|
+
def error_rate
|
|
52
|
+
if (successes + errors).zero?
|
|
53
|
+
0.0
|
|
54
|
+
else
|
|
55
|
+
errors.fdiv(successes + errors)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Integer]
|
|
60
|
+
def requests
|
|
61
|
+
successes + errors
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stoplight
|
|
4
|
-
module
|
|
4
|
+
module Domain
|
|
5
5
|
# Base class for creating custom notifiers in Stoplight.
|
|
6
6
|
# This is an abstract class that defines the interface for notifiers.
|
|
7
7
|
#
|
|
8
8
|
# @abstract Subclasses must implement the `notify` method to define custom notification logic.
|
|
9
|
-
#
|
|
10
|
-
class
|
|
9
|
+
# :nocov:
|
|
10
|
+
class StateTransitionNotifier # ColorTransition?????
|
|
11
11
|
# Sends a notification when a Stoplight changes state.
|
|
12
12
|
#
|
|
13
|
-
# @param config [Stoplight::
|
|
13
|
+
# @param config [Stoplight::Domain::Config] The Stoplight instance triggering the notification.
|
|
14
14
|
# @param from_color [String] The previous state color of the Stoplight.
|
|
15
15
|
# @param to_color [String] The new state color of the Stoplight.
|
|
16
16
|
# @param error [Exception, nil] The error (if any) that caused the state change.
|
|
@@ -20,5 +20,6 @@ module Stoplight
|
|
|
20
20
|
raise NotImplementedError
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
+
# :nocov:
|
|
23
24
|
end
|
|
24
25
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Strategies
|
|
6
|
+
# Defines how the light executes when it is green.
|
|
7
|
+
#
|
|
8
|
+
# This strategy clears failures after successful execution and handles errors
|
|
9
|
+
# by either raising them or invoking a fallback if provided.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class GreenRunStrategy < RunStrategy
|
|
13
|
+
# @!attribute [r] request_tracker
|
|
14
|
+
# @return [Stoplight::Domain::Tracker::Request]
|
|
15
|
+
protected attr_reader :request_tracker
|
|
16
|
+
|
|
17
|
+
# @!attribute [r] config
|
|
18
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
19
|
+
protected attr_reader :config
|
|
20
|
+
|
|
21
|
+
# @param config [Stoplight::Domain::Config]
|
|
22
|
+
# @param request_tracker [Stoplight::Domain::Tracker::Request
|
|
23
|
+
def initialize(config:, request_tracker:)
|
|
24
|
+
@config = config
|
|
25
|
+
@request_tracker = request_tracker
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Executes the provided code block when the light is in the green state.
|
|
29
|
+
#
|
|
30
|
+
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
31
|
+
# @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
|
|
32
|
+
# @yield The code block to execute.
|
|
33
|
+
# @return [Object] The result of the code block if successful.
|
|
34
|
+
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
35
|
+
def execute(fallback, metadata:, &code)
|
|
36
|
+
# TODO: Consider implementing sampling rate to limit the memory footprint
|
|
37
|
+
code.call.tap { record_success }
|
|
38
|
+
rescue => error
|
|
39
|
+
if config.track_error?(error)
|
|
40
|
+
record_error(error)
|
|
41
|
+
|
|
42
|
+
if fallback
|
|
43
|
+
fallback.call(error)
|
|
44
|
+
else
|
|
45
|
+
raise
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
# User chose to not track the error, so we record it as a success
|
|
49
|
+
record_success
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private def record_error(error)
|
|
55
|
+
request_tracker.record_failure(error)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def record_success
|
|
59
|
+
request_tracker.record_success
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def ==(other)
|
|
64
|
+
super && config == other.config && request_tracker == other.request_tracker
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Strategies
|
|
6
|
+
# Defines how the light executes when it is red.
|
|
7
|
+
#
|
|
8
|
+
# This strategy prevents execution of the code block and either raises an error
|
|
9
|
+
# or invokes a fallback if provided.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class RedRunStrategy < RunStrategy
|
|
13
|
+
# @!attribute [r] config
|
|
14
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
15
|
+
protected attr_reader :config
|
|
16
|
+
|
|
17
|
+
def initialize(config:)
|
|
18
|
+
@config = config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Executes the fallback proc when the light is in the red state.
|
|
22
|
+
#
|
|
23
|
+
# @param fallback [Proc, nil] A fallback proc to execute instead of the code block.
|
|
24
|
+
# @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
|
|
25
|
+
# @return [Object, nil] The result of the fallback proc if provided.
|
|
26
|
+
# @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
|
|
27
|
+
def execute(fallback, metadata:)
|
|
28
|
+
if fallback
|
|
29
|
+
fallback.call(nil)
|
|
30
|
+
else
|
|
31
|
+
raise Error::RedLight.new(
|
|
32
|
+
config.name,
|
|
33
|
+
cool_off_time: config.cool_off_time,
|
|
34
|
+
retry_after: metadata.recovery_scheduled_after
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Strategies
|
|
6
|
+
# Represents an abstract strategy for running a light's operations.
|
|
7
|
+
# Every new strategy should be a child of this class.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
# @abstract
|
|
11
|
+
class RunStrategy
|
|
12
|
+
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
13
|
+
# @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
|
|
14
|
+
# :nocov:
|
|
15
|
+
def execute(fallback, metadata:, &code)
|
|
16
|
+
raise NotImplementedError, "Subclasses must implement the execute method"
|
|
17
|
+
end
|
|
18
|
+
# :nocov:
|
|
19
|
+
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def ==(other)
|
|
22
|
+
other.is_a?(self.class)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Strategies
|
|
6
|
+
# Defines how the light executes when it is yellow.
|
|
7
|
+
#
|
|
8
|
+
# This strategy clears failures after successful execution and notifies
|
|
9
|
+
# about color switch from Red to Green. It also handles errors by either
|
|
10
|
+
# raising them or invoking a fallback if provided.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class YellowRunStrategy < RunStrategy
|
|
14
|
+
# @!attribute [r] config
|
|
15
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
16
|
+
protected attr_reader :config
|
|
17
|
+
|
|
18
|
+
# @!attribute [r] data_store
|
|
19
|
+
# @return [Stoplight::DataStore::Base] The data store associated with the light.
|
|
20
|
+
protected attr_reader :data_store
|
|
21
|
+
|
|
22
|
+
# @!attribute [r] notifiers
|
|
23
|
+
# @return [Stoplight::Domain::StateTransitionNotifier]
|
|
24
|
+
protected attr_reader :notifiers
|
|
25
|
+
|
|
26
|
+
# @!attribute [r] request_tracker
|
|
27
|
+
# @return [Stoplight::Domain::RecoveryProbeRequestRecorder]
|
|
28
|
+
protected attr_reader :request_tracker
|
|
29
|
+
|
|
30
|
+
# @param config [Stoplight::Domain::Config]
|
|
31
|
+
# @param data_store [Stoplight::DataStore::Base]
|
|
32
|
+
# @param notifiers [Array<Stoplight::Domain::StateTransitionNotifier>]
|
|
33
|
+
# @param request_tracker [Stoplight::Domain::Tracker::RecoveryProbe]
|
|
34
|
+
def initialize(config:, data_store:, notifiers:, request_tracker:)
|
|
35
|
+
@config = config
|
|
36
|
+
@data_store = data_store
|
|
37
|
+
@notifiers = notifiers
|
|
38
|
+
@request_tracker = request_tracker
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Executes the provided code block when the light is in the yellow state.
|
|
42
|
+
#
|
|
43
|
+
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
44
|
+
# @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
|
|
45
|
+
# @yield The code block to execute.
|
|
46
|
+
# @return [Object] The result of the code block if successful.
|
|
47
|
+
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
48
|
+
def execute(fallback, metadata:, &code)
|
|
49
|
+
enter_recovery(metadata)
|
|
50
|
+
# TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
|
|
51
|
+
code.call.tap { record_recovery_probe_success }
|
|
52
|
+
rescue => error
|
|
53
|
+
if config.track_error?(error)
|
|
54
|
+
record_recovery_probe_failure(error)
|
|
55
|
+
|
|
56
|
+
if fallback
|
|
57
|
+
fallback.call(error)
|
|
58
|
+
else
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
record_recovery_probe_success
|
|
63
|
+
raise
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private def record_recovery_probe_success
|
|
68
|
+
request_tracker.record_success
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def record_recovery_probe_failure(error)
|
|
72
|
+
request_tracker.record_failure(error)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param metadata [Stoplight::Domain::Metadata]
|
|
76
|
+
# @return [void]
|
|
77
|
+
private def enter_recovery(metadata)
|
|
78
|
+
return if metadata.recovery_started?
|
|
79
|
+
|
|
80
|
+
if data_store.transition_to_color(config, Color::YELLOW)
|
|
81
|
+
notifiers.each do |notifier|
|
|
82
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def ==(other)
|
|
89
|
+
super &&
|
|
90
|
+
config == other.config &&
|
|
91
|
+
notifiers == other.notifiers &&
|
|
92
|
+
data_store == other.data_store &&
|
|
93
|
+
request_tracker == other.request_tracker
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Tracker
|
|
6
|
+
# @api private
|
|
7
|
+
# @abstract
|
|
8
|
+
class Base
|
|
9
|
+
# @!attribute [r] data_store
|
|
10
|
+
# @return [Stoplight::DataStore::Base] The data store associated with the light.
|
|
11
|
+
protected attr_reader :data_store
|
|
12
|
+
|
|
13
|
+
# @!attribute [r] traffic_control
|
|
14
|
+
# @return [Stoplight::Domain::TrafficControl::Base]
|
|
15
|
+
protected attr_reader :notifiers
|
|
16
|
+
|
|
17
|
+
# @!attribute [r] config
|
|
18
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
19
|
+
protected attr_reader :config
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
other.is_a?(self.class) &&
|
|
23
|
+
config == other.config &&
|
|
24
|
+
data_store == other.data_store &&
|
|
25
|
+
notifiers == other.notifiers
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def transition_and_notify(from_color, to_color, error = nil)
|
|
29
|
+
if data_store.transition_to_color(config, to_color)
|
|
30
|
+
notifiers.each do |notifier|
|
|
31
|
+
notifier.notify(config, from_color, to_color, error)
|
|
32
|
+
end
|
|
33
|
+
true
|
|
34
|
+
else
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Tracker
|
|
6
|
+
class RecoveryProbe < Base
|
|
7
|
+
# @!attribute [r] data_store
|
|
8
|
+
# @return [Stoplight::DataStore::Base] The data store associated with the light.
|
|
9
|
+
protected attr_reader :data_store
|
|
10
|
+
|
|
11
|
+
# @!attribute [r] traffic_recovery
|
|
12
|
+
# @return [Stoplight::Domain::TrafficRecovery::Base]
|
|
13
|
+
protected attr_reader :traffic_recovery
|
|
14
|
+
|
|
15
|
+
# @!attribute [r] traffic_control
|
|
16
|
+
# @return [Stoplight::Domain::TrafficControl::Base]
|
|
17
|
+
protected attr_reader :notifiers
|
|
18
|
+
|
|
19
|
+
# @!attribute [r] config
|
|
20
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
21
|
+
protected attr_reader :config
|
|
22
|
+
|
|
23
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
24
|
+
# @param traffic_recovery [Stoplight::Domain::TrafficRecovery::Base]
|
|
25
|
+
# @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
|
|
26
|
+
# @param config [Stoplight::Domain::Config]
|
|
27
|
+
def initialize(data_store:, traffic_recovery:, notifiers:, config:)
|
|
28
|
+
@data_store = data_store
|
|
29
|
+
@traffic_recovery = traffic_recovery
|
|
30
|
+
@notifiers = notifiers
|
|
31
|
+
@config = config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param exception [Exception]
|
|
35
|
+
def record_failure(exception)
|
|
36
|
+
metadata = data_store.record_recovery_probe_failure(config, exception)
|
|
37
|
+
|
|
38
|
+
recover(metadata)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def record_success
|
|
42
|
+
metadata = data_store.record_recovery_probe_success(config)
|
|
43
|
+
|
|
44
|
+
recover(metadata)
|
|
45
|
+
end
|
|
46
|
+
RECOVERY_TRANSITIONS = {
|
|
47
|
+
TrafficRecovery::GREEN => [Color::YELLOW, Color::GREEN],
|
|
48
|
+
TrafficRecovery::YELLOW => [Color::RED, Color::YELLOW],
|
|
49
|
+
TrafficRecovery::RED => [Color::YELLOW, Color::RED]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
private def recover(metadata)
|
|
53
|
+
recovery_result = traffic_recovery.determine_color(config, metadata)
|
|
54
|
+
|
|
55
|
+
return if recovery_result == TrafficRecovery::PASS
|
|
56
|
+
|
|
57
|
+
from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
|
|
58
|
+
raise "recovery strategy returned unexpected color: #{recovery_result}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
transition_and_notify(from_color, to_color, nil)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param other [any]
|
|
65
|
+
# @return [bool]
|
|
66
|
+
def ==(other)
|
|
67
|
+
super && traffic_recovery == other.traffic_recovery
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Tracker
|
|
6
|
+
# Tracks request outcomes (success/failure) and manages state transitions
|
|
7
|
+
# for normal traffic.
|
|
8
|
+
#
|
|
9
|
+
# Used by +GreenRunStrategy+ to track failures and potentially open the circuit.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Request < Base
|
|
13
|
+
# @!attribute [r] data_store
|
|
14
|
+
# @return [Stoplight::DataStore::Base] The data store associated with the light.
|
|
15
|
+
protected attr_reader :data_store
|
|
16
|
+
|
|
17
|
+
# @!attribute [r] traffic_control
|
|
18
|
+
# @return [Stoplight::Domain::TrafficControl::Base]
|
|
19
|
+
protected attr_reader :traffic_control
|
|
20
|
+
|
|
21
|
+
# @!attribute [r] traffic_control
|
|
22
|
+
# @return [Stoplight::Domain::TrafficControl::Base]
|
|
23
|
+
protected attr_reader :notifiers
|
|
24
|
+
|
|
25
|
+
# @!attribute [r] config
|
|
26
|
+
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
27
|
+
protected attr_reader :config
|
|
28
|
+
|
|
29
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
30
|
+
# @param traffic_control [Stoplight::Domain::TrafficControl::Base]
|
|
31
|
+
# @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
|
|
32
|
+
# @param config [Stoplight::Domain::Config]
|
|
33
|
+
def initialize(data_store:, traffic_control:, notifiers:, config:)
|
|
34
|
+
@data_store = data_store
|
|
35
|
+
@traffic_control = traffic_control
|
|
36
|
+
@notifiers = notifiers
|
|
37
|
+
@config = config
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param exception [Exception]
|
|
41
|
+
# @return [void]
|
|
42
|
+
def record_failure(exception)
|
|
43
|
+
metadata = data_store.record_failure(config, exception)
|
|
44
|
+
|
|
45
|
+
transition_to_red(exception, metadata:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [void]
|
|
49
|
+
def record_success
|
|
50
|
+
data_store.record_success(config)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def transition_to_red(exception, metadata:)
|
|
54
|
+
if traffic_control.stop_traffic?(config, metadata)
|
|
55
|
+
transition_and_notify(Color::GREEN, Color::RED, exception)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param other [any]
|
|
60
|
+
# @return [bool]
|
|
61
|
+
def ==(other)
|
|
62
|
+
super && traffic_control == other.traffic_control
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module TrafficControl
|
|
6
|
+
# Strategies for determining when a Stoplight should change color to red.
|
|
7
|
+
#
|
|
8
|
+
# These strategies evaluate the current state and metrics of a Stoplight to decide
|
|
9
|
+
# if traffic should be stopped (i.e., if the light should turn RED).
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a custom strategy
|
|
12
|
+
# class ErrorRateStrategy < Stoplight::Domain::TrafficControl::Base
|
|
13
|
+
# def check_compatibility(config)
|
|
14
|
+
# if config.window_size.nil?
|
|
15
|
+
# incompatible("`window_size` should be set")
|
|
16
|
+
# else
|
|
17
|
+
# compatible
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# def stop_traffic?(config, metadata)
|
|
22
|
+
# total = metadata.successes + metadata.failures
|
|
23
|
+
# return false if total < 10 # Minimum sample size
|
|
24
|
+
#
|
|
25
|
+
# error_rate = metadata.failures.fdiv(total)
|
|
26
|
+
# error_rate >= 0.5 # Stop traffic when error rate reaches 50%
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @abstract
|
|
31
|
+
# @api private
|
|
32
|
+
class Base
|
|
33
|
+
# Checks if the strategy is compatible with the given Stoplight configuration.
|
|
34
|
+
#
|
|
35
|
+
# @param config [Stoplight::Domain::Config]
|
|
36
|
+
# @return [Stoplight::Domain::CompatibilityResult]
|
|
37
|
+
# :nocov:
|
|
38
|
+
def check_compatibility(config)
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
# :nocov:
|
|
42
|
+
|
|
43
|
+
# Determines whether traffic should be stopped based on the Stoplight's
|
|
44
|
+
# current state and metrics.
|
|
45
|
+
#
|
|
46
|
+
# @param config [Stoplight::Domain::Config]
|
|
47
|
+
# @param metadata [Stoplight::Domain::Metadata]
|
|
48
|
+
# @return [Boolean] true if traffic should be stopped (rec), false otherwise (green)
|
|
49
|
+
# :nocov:
|
|
50
|
+
def stop_traffic?(config, metadata)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
# :nocov:
|
|
54
|
+
|
|
55
|
+
# @param other [any]
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def ==(other)
|
|
58
|
+
other.is_a?(self.class)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns a compatibility result indicating the strategy is compatible.
|
|
62
|
+
#
|
|
63
|
+
# @return [Stoplight::Domain::CompatibilityResult] A compatible result.
|
|
64
|
+
private def compatible = CompatibilityResult.compatible
|
|
65
|
+
|
|
66
|
+
# Returns a compatibility result indicating the strategy is incompatible.
|
|
67
|
+
#
|
|
68
|
+
# @param errors [Array<String>] The list of error messages describing incompatibility.
|
|
69
|
+
# @return [Stoplight::Domain::CompatibilityResult] An incompatible result.
|
|
70
|
+
private def incompatible(*errors) = CompatibilityResult.incompatible(*errors)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|