stoplight 5.6.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/lib/stoplight/admin/dependencies.rb +1 -1
- data/lib/stoplight/admin/helpers.rb +10 -5
- data/lib/stoplight/admin/lights_repository.rb +18 -15
- data/lib/stoplight/admin.rb +2 -1
- data/lib/stoplight/common/deprecations.rb +11 -0
- data/lib/stoplight/domain/config.rb +5 -1
- data/lib/stoplight/domain/data_store.rb +17 -1
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
- data/lib/stoplight/domain/light.rb +31 -20
- data/lib/stoplight/domain/metrics.rb +6 -27
- data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
- 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/run_strategy.rb +0 -5
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +58 -32
- data/lib/stoplight/domain/tracker/base.rb +0 -29
- data/lib/stoplight/domain/tracker/recovery_probe.rb +23 -22
- data/lib/stoplight/domain/tracker/request.rb +23 -19
- data/lib/stoplight/domain/traffic_recovery/base.rb +1 -2
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +2 -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/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.rb +61 -32
- 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 +133 -162
- 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 +50 -15
- 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 -147
- 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/{get_metrics.lua → lua_scripts/get_metrics.lua} +0 -0
- /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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Storage
|
|
6
|
+
# Encapsulates recovery lock management for coordinating recovery probes.
|
|
7
|
+
#
|
|
8
|
+
# When a circuit enters YELLOW state (half-open), it begins sending
|
|
9
|
+
# "recovery probes" - test requests to check if the protected service
|
|
10
|
+
# has recovered. In distributed deployments with multiple instances,
|
|
11
|
+
# recovery locks ensure only ONE instance sends probes at a time.
|
|
12
|
+
#
|
|
13
|
+
# Without coordination, all instances would simultaneously:
|
|
14
|
+
# 1. Detect the circuit is YELLOW
|
|
15
|
+
# 2. Send recovery probes to the struggling service
|
|
16
|
+
# 3. Potentially overwhelm it with "test" traffic
|
|
17
|
+
#
|
|
18
|
+
# Lock Lifecycle:
|
|
19
|
+
#
|
|
20
|
+
# Instance A: acquire_lock -> probe -> release_lock
|
|
21
|
+
# Instance B: acquire_lock -> nil (already held) -> skip probe
|
|
22
|
+
# Instance C: acquire_lock -> nil (already held) -> skip probe
|
|
23
|
+
#
|
|
24
|
+
# Lock Semantics:
|
|
25
|
+
# - Returns +nil+ if lock is already held. Never blocks waiting for lock availability
|
|
26
|
+
# - Locks must automatically expire when persisted storage is used
|
|
27
|
+
# - Failed releases are acceptable (timeout provides safety)
|
|
28
|
+
#
|
|
29
|
+
# @abstract
|
|
30
|
+
# @see Stoplight::Domain::Strategies::YellowRunStrategy
|
|
31
|
+
class RecoveryLock
|
|
32
|
+
# Attempts to acquire recovery lock for exclusive probe execution.
|
|
33
|
+
#
|
|
34
|
+
# This method tries to acquire a lock that serializes recovery probe
|
|
35
|
+
# execution across multiple instances. If the lock is already held by
|
|
36
|
+
# another instance, returns +nil+ immediately without blocking.
|
|
37
|
+
#
|
|
38
|
+
# @return [Stoplight::Domain::RecoveryLockToken, nil]
|
|
39
|
+
# - +RecoveryLockToken+: Lock acquired, caller should send probe
|
|
40
|
+
# - +nil+: Lock unavailable, another instance is probing
|
|
41
|
+
#
|
|
42
|
+
def acquire_lock = raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
# Releases a previously acquired lock.
|
|
45
|
+
#
|
|
46
|
+
# This method releases the lock token returned by +#acquire_lock+,
|
|
47
|
+
# allowing other instances to acquire it. Release should be called
|
|
48
|
+
# in an ensure block to guarantee cleanup even if probe fails.
|
|
49
|
+
#
|
|
50
|
+
# @param lock [Stoplight::Domain::RecoveryLockToken] The token returned by +#acquire_lock+
|
|
51
|
+
# @return [void]
|
|
52
|
+
def release_lock(lock) = raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Storage
|
|
6
|
+
# Encapsulates circuit breaker state storage.
|
|
7
|
+
#
|
|
8
|
+
# State management handles the current operational mode of a circuit breaker:
|
|
9
|
+
# - Color (GREEN/YELLOW/RED) - whether the circuit is open or closed
|
|
10
|
+
# - Lock state (LOCKED_GREEN/LOCKED_RED/UNLOCKED) - manual overrides
|
|
11
|
+
# - State transitions - tracking color changes for notifications #
|
|
12
|
+
#
|
|
13
|
+
# State requires stronger consistency than metrics because:
|
|
14
|
+
# - Multiple instances must agree on circuit color
|
|
15
|
+
# - Race conditions during transitions must be handled
|
|
16
|
+
# - Lock states must be immediately visible across instances
|
|
17
|
+
#
|
|
18
|
+
# @abstract
|
|
19
|
+
# @see Stoplight::Domain::Storage::Metrics
|
|
20
|
+
class State
|
|
21
|
+
# Retrieves current state snapshot for decision-making.
|
|
22
|
+
#
|
|
23
|
+
# The snapshot is an immutable view of the circuit's current state,
|
|
24
|
+
# including its color and lock status. This method is called on every
|
|
25
|
+
# circuit breaker invocation to determine whether to allow traffic.
|
|
26
|
+
#
|
|
27
|
+
# This is called on every request, so implementations should be fast.
|
|
28
|
+
#
|
|
29
|
+
# @return [Stoplight::Domain::StateSnapshot]
|
|
30
|
+
def state_snapshot = raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
# Sets the lock state of the circuit.
|
|
33
|
+
#
|
|
34
|
+
# Locks allow manual override of circuit behavior:
|
|
35
|
+
# - LOCKED_GREEN: Force circuit closed (allow all traffic)
|
|
36
|
+
# - LOCKED_RED: Force circuit open (block all traffic)
|
|
37
|
+
# - UNLOCKED: Follow normal circuit breaker rules
|
|
38
|
+
#
|
|
39
|
+
# Lock states take precedence over color states. A locked circuit
|
|
40
|
+
# ignores failure thresholds and stays in the locked state until
|
|
41
|
+
# explicitly unlocked.
|
|
42
|
+
#
|
|
43
|
+
# Use Cases:
|
|
44
|
+
# - Emergency traffic control during incidents
|
|
45
|
+
# - Maintenance windows (lock RED to prevent traffic)
|
|
46
|
+
# - Gradual rollout (lock GREEN during testing)
|
|
47
|
+
#
|
|
48
|
+
# @param state [String] The new state to set.
|
|
49
|
+
# @return [String] The state that was set.
|
|
50
|
+
def set_state(state) = raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
# Transitions the Stoplight to the specified color.
|
|
53
|
+
#
|
|
54
|
+
# This method performs a color transition operation that works across distributed instances
|
|
55
|
+
# of the light. It ensures that in a multi-instance environment, only one instance
|
|
56
|
+
# is considered the "first" to perform the transition (and therefore responsible for
|
|
57
|
+
# triggering notifications).
|
|
58
|
+
#
|
|
59
|
+
# @param color [String] The target color/state to transition to.
|
|
60
|
+
# Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] Returns +true+ if this instance was the first to perform this specific transition
|
|
63
|
+
# (and should therefore trigger notifications). Returns +false+ if another instance already
|
|
64
|
+
# initiated this transition.
|
|
65
|
+
#
|
|
66
|
+
# @note In distributed environments with multiple instances, race conditions can occur when instances
|
|
67
|
+
# attempt conflicting transitions simultaneously (e.g., one instance tries to transition from
|
|
68
|
+
# YELLOW to GREEN while another tries YELLOW to RED). The implementation handles this, but
|
|
69
|
+
# be aware that the last operation may determine the final color of the light.
|
|
70
|
+
#
|
|
71
|
+
def transition_to_color(color) = raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
# Clears all state data for this circuit.
|
|
74
|
+
#
|
|
75
|
+
# This removes the circuit from storage entirely, resetting it to
|
|
76
|
+
# default (unlocked, green) state. The next invocation will start
|
|
77
|
+
# with fresh state.
|
|
78
|
+
#
|
|
79
|
+
# @note This does NOT clear metrics. If you want to fully
|
|
80
|
+
# reset a circuit, clear both state and metrics stores.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
def clear = raise NotImplementedError
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -15,9 +15,17 @@ module Stoplight
|
|
|
15
15
|
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
16
16
|
protected attr_reader :config
|
|
17
17
|
|
|
18
|
-
# @!attribute [r]
|
|
19
|
-
# @return [Stoplight::
|
|
20
|
-
protected attr_reader :
|
|
18
|
+
# @!attribute [r] stare_store
|
|
19
|
+
# @return [Stoplight::Domain::Storage::State]
|
|
20
|
+
protected attr_reader :state_store
|
|
21
|
+
|
|
22
|
+
# @!attribute [r] metrics_store
|
|
23
|
+
# @return [Stoplight::Domain::Storage::Metrics]
|
|
24
|
+
protected attr_reader :metrics_store
|
|
25
|
+
|
|
26
|
+
# @!attribute [r] recovery_lock_store
|
|
27
|
+
# @return [Stoplight::Domain::Storage::RecoveryLock]
|
|
28
|
+
protected attr_reader :recovery_lock_store
|
|
21
29
|
|
|
22
30
|
# @!attribute [r] notifiers
|
|
23
31
|
# @return [Stoplight::Domain::StateTransitionNotifier]
|
|
@@ -27,15 +35,23 @@ module Stoplight
|
|
|
27
35
|
# @return [Stoplight::Domain::RecoveryProbeRequestRecorder]
|
|
28
36
|
protected attr_reader :request_tracker
|
|
29
37
|
|
|
38
|
+
# @!attribute [r] red_run_strategy
|
|
39
|
+
# @return [Stoplight::Domain::Strategies::RedRunStrategy]
|
|
40
|
+
protected attr_reader :red_run_strategy
|
|
41
|
+
|
|
30
42
|
# @param config [Stoplight::Domain::Config]
|
|
31
|
-
# @param data_store [Stoplight::DataStore::Base]
|
|
32
43
|
# @param notifiers [Array<Stoplight::Domain::StateTransitionNotifier>]
|
|
33
44
|
# @param request_tracker [Stoplight::Domain::Tracker::RecoveryProbe]
|
|
34
|
-
|
|
45
|
+
# @param red_run_strategy [Stoplight::Domain::Strategies::RedRunStrategy]
|
|
46
|
+
# @param recovery_lock_store [Stoplight::Domain::Storage::RecoveryLock]
|
|
47
|
+
def initialize(config:, notifiers:, request_tracker:, red_run_strategy:, state_store:, metrics_store:, recovery_lock_store:)
|
|
35
48
|
@config = config
|
|
36
|
-
@data_store = data_store
|
|
37
49
|
@notifiers = notifiers
|
|
38
50
|
@request_tracker = request_tracker
|
|
51
|
+
@red_run_strategy = red_run_strategy
|
|
52
|
+
@state_store = state_store
|
|
53
|
+
@metrics_store = metrics_store
|
|
54
|
+
@recovery_lock_store = recovery_lock_store
|
|
39
55
|
end
|
|
40
56
|
|
|
41
57
|
# Executes the provided code block when the light is in the yellow state.
|
|
@@ -46,21 +62,41 @@ module Stoplight
|
|
|
46
62
|
# @return [Object] The result of the code block if successful.
|
|
47
63
|
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
48
64
|
def execute(fallback, state_snapshot:, &code)
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
# Everything withing this block executed exclusively:
|
|
66
|
+
# - enter recovery
|
|
67
|
+
# - execute user's code
|
|
68
|
+
# - record outcome
|
|
69
|
+
# - transition to green or red if needed
|
|
70
|
+
with_recovery_lock(fallback:, state_snapshot:) do
|
|
71
|
+
enter_recovery(state_snapshot)
|
|
72
|
+
|
|
73
|
+
code.call.tap { record_recovery_probe_success }
|
|
74
|
+
rescue => error
|
|
75
|
+
if config.track_error?(error)
|
|
76
|
+
record_recovery_probe_failure(error)
|
|
77
|
+
|
|
78
|
+
if fallback
|
|
79
|
+
fallback.call(error)
|
|
80
|
+
else
|
|
81
|
+
raise
|
|
82
|
+
end
|
|
58
83
|
else
|
|
84
|
+
record_recovery_probe_success
|
|
59
85
|
raise
|
|
60
86
|
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def with_recovery_lock(fallback:, state_snapshot:)
|
|
91
|
+
recovery_lock_token = recovery_lock_store.acquire_lock
|
|
92
|
+
if recovery_lock_token.nil?
|
|
93
|
+
return red_run_strategy.execute(fallback, state_snapshot:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
yield
|
|
98
|
+
ensure
|
|
99
|
+
recovery_lock_store.release_lock(recovery_lock_token)
|
|
64
100
|
end
|
|
65
101
|
end
|
|
66
102
|
|
|
@@ -77,22 +113,12 @@ module Stoplight
|
|
|
77
113
|
private def enter_recovery(state_snapshot)
|
|
78
114
|
return if state_snapshot.recovery_started?
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
116
|
+
state_store.transition_to_color(Color::YELLOW)
|
|
117
|
+
metrics_store.clear
|
|
118
|
+
notifiers.each do |notifier|
|
|
119
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
|
85
120
|
end
|
|
86
121
|
end
|
|
87
|
-
|
|
88
|
-
# @return [Boolean]
|
|
89
|
-
def ==(other)
|
|
90
|
-
super &&
|
|
91
|
-
config == other.config &&
|
|
92
|
-
notifiers == other.notifiers &&
|
|
93
|
-
data_store == other.data_store &&
|
|
94
|
-
request_tracker == other.request_tracker
|
|
95
|
-
end
|
|
96
122
|
end
|
|
97
123
|
end
|
|
98
124
|
end
|
|
@@ -6,35 +6,6 @@ module Stoplight
|
|
|
6
6
|
# @api private
|
|
7
7
|
# @abstract
|
|
8
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
9
|
end
|
|
39
10
|
end
|
|
40
11
|
end
|
|
@@ -4,10 +4,6 @@ module Stoplight
|
|
|
4
4
|
module Domain
|
|
5
5
|
module Tracker
|
|
6
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
7
|
# @!attribute [r] traffic_recovery
|
|
12
8
|
# @return [Stoplight::Domain::TrafficRecovery::Base]
|
|
13
9
|
protected attr_reader :traffic_recovery
|
|
@@ -20,54 +16,59 @@ module Stoplight
|
|
|
20
16
|
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
21
17
|
protected attr_reader :config
|
|
22
18
|
|
|
23
|
-
#
|
|
19
|
+
# @!attribute [r] metrics_store
|
|
20
|
+
# @return [Stoplight::Domain::Storage::Metrics]
|
|
21
|
+
protected attr_reader :metrics_store
|
|
22
|
+
|
|
23
|
+
# @!attribute [r] state_store
|
|
24
|
+
# @return [Stoplight::Domain::Storage::State]
|
|
25
|
+
protected attr_reader :state_store
|
|
26
|
+
|
|
24
27
|
# @param traffic_recovery [Stoplight::Domain::TrafficRecovery::Base]
|
|
25
28
|
# @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
|
|
26
29
|
# @param config [Stoplight::Domain::Config]
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
# @param metrics_store [Stoplight::Domain::Storage::Metrics]
|
|
31
|
+
# @param state_store [Stoplight::Domain::Storage::State]
|
|
32
|
+
def initialize(traffic_recovery:, notifiers:, config:, metrics_store:, state_store:)
|
|
29
33
|
@traffic_recovery = traffic_recovery
|
|
30
34
|
@notifiers = notifiers
|
|
31
35
|
@config = config
|
|
36
|
+
@metrics_store = metrics_store
|
|
37
|
+
@state_store = state_store
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
# @param exception [Exception]
|
|
35
41
|
def record_failure(exception)
|
|
36
|
-
|
|
42
|
+
metrics_store.record_failure(exception)
|
|
37
43
|
|
|
38
44
|
recover
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def record_success
|
|
42
|
-
|
|
48
|
+
metrics_store.record_success
|
|
43
49
|
|
|
44
50
|
recover
|
|
45
51
|
end
|
|
46
52
|
RECOVERY_TRANSITIONS = {
|
|
47
53
|
TrafficRecovery::GREEN => [Color::YELLOW, Color::GREEN],
|
|
48
|
-
TrafficRecovery::YELLOW => [Color::RED, Color::YELLOW],
|
|
49
54
|
TrafficRecovery::RED => [Color::YELLOW, Color::RED]
|
|
50
55
|
}.freeze
|
|
51
56
|
|
|
52
57
|
private def recover
|
|
53
|
-
recovery_metrics =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
recovery_result = traffic_recovery.determine_color(config, recovery_metrics, state_snapshot)
|
|
58
|
+
recovery_metrics = metrics_store.metrics_snapshot
|
|
59
|
+
recovery_result = traffic_recovery.determine_color(config, recovery_metrics)
|
|
57
60
|
|
|
58
|
-
return if recovery_result == TrafficRecovery::
|
|
61
|
+
return if recovery_result == TrafficRecovery::YELLOW
|
|
59
62
|
|
|
60
63
|
from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
|
|
61
64
|
raise "recovery strategy returned unexpected color: #{recovery_result}"
|
|
62
65
|
end
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def ==(other)
|
|
70
|
-
super && traffic_recovery == other.traffic_recovery
|
|
67
|
+
state_store.transition_to_color(to_color)
|
|
68
|
+
metrics_store.clear
|
|
69
|
+
notifiers.each do |notifier|
|
|
70
|
+
notifier.notify(config, from_color, to_color, nil)
|
|
71
|
+
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -10,10 +10,6 @@ module Stoplight
|
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
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
13
|
# @!attribute [r] traffic_control
|
|
18
14
|
# @return [Stoplight::Domain::TrafficControl::Base]
|
|
19
15
|
protected attr_reader :traffic_control
|
|
@@ -26,42 +22,50 @@ module Stoplight
|
|
|
26
22
|
# @return [Stoplight::Domain::Config] The configuration for the light.
|
|
27
23
|
protected attr_reader :config
|
|
28
24
|
|
|
29
|
-
#
|
|
25
|
+
# @!attribute metrics_store
|
|
26
|
+
# @return [Stoplight::Storage::Metrics]
|
|
27
|
+
protected attr_reader :metrics_store
|
|
28
|
+
|
|
29
|
+
# @!attribute [r] state_store
|
|
30
|
+
# @return [Stoplight::Domain::Storage::State]
|
|
31
|
+
protected attr_reader :state_store
|
|
32
|
+
|
|
30
33
|
# @param traffic_control [Stoplight::Domain::TrafficControl::Base]
|
|
31
34
|
# @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
|
|
32
35
|
# @param config [Stoplight::Domain::Config]
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
# @param metrics_store [Stoplight::Storage::Metrics]
|
|
37
|
+
# @param state_store [Stoplight::Domain::Storage::State]
|
|
38
|
+
def initialize(traffic_control:, notifiers:, config:, metrics_store:, state_store:)
|
|
35
39
|
@traffic_control = traffic_control
|
|
36
40
|
@notifiers = notifiers
|
|
37
41
|
@config = config
|
|
42
|
+
@metrics_store = metrics_store
|
|
43
|
+
@state_store = state_store
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# @param exception [Exception]
|
|
41
47
|
# @return [void]
|
|
42
48
|
def record_failure(exception)
|
|
43
|
-
|
|
44
|
-
metrics =
|
|
49
|
+
metrics_store.record_failure(exception)
|
|
50
|
+
metrics = metrics_store.metrics_snapshot
|
|
45
51
|
|
|
46
52
|
transition_to_red(exception, metrics:)
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
# @return [void]
|
|
50
|
-
def record_success
|
|
51
|
-
data_store.record_success(config)
|
|
52
|
-
end
|
|
56
|
+
def record_success = metrics_store.record_success
|
|
53
57
|
|
|
54
58
|
private def transition_to_red(exception, metrics:)
|
|
55
59
|
if traffic_control.stop_traffic?(config, metrics)
|
|
56
|
-
|
|
60
|
+
# Returns true only if not yet in red therefore preventing
|
|
61
|
+
# duplicate notifications
|
|
62
|
+
if state_store.transition_to_color(Color::RED)
|
|
63
|
+
notifiers.each do |notifier|
|
|
64
|
+
notifier.notify(config, Color::GREEN, Color::RED, exception)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
|
-
|
|
60
|
-
# @param other [any]
|
|
61
|
-
# @return [bool]
|
|
62
|
-
def ==(other)
|
|
63
|
-
super && traffic_control == other.traffic_control
|
|
64
|
-
end
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|
|
@@ -50,10 +50,9 @@ module Stoplight
|
|
|
50
50
|
#
|
|
51
51
|
# @param config [Stoplight::Domain::Config]
|
|
52
52
|
# @param metrics [Stoplight::Domain::Metrics]
|
|
53
|
-
# @param state_snapshot [Stoplight::Domain::StateSnapshot]
|
|
54
53
|
# @return [TrafficRecovery::Decision]
|
|
55
54
|
# :nocov:
|
|
56
|
-
def determine_color(config, metrics
|
|
55
|
+
def determine_color(config, metrics)
|
|
57
56
|
raise NotImplementedError
|
|
58
57
|
end
|
|
59
58
|
# :nocov:
|
|
@@ -50,15 +50,9 @@ module Stoplight
|
|
|
50
50
|
#
|
|
51
51
|
# @param config [Stoplight::Domain::Config]
|
|
52
52
|
# @param recovery_metrics [Stoplight::Domain::Metrics]
|
|
53
|
-
# @param state_snapshot [Stoplight::Domain::StateSnapshot]
|
|
54
53
|
# @return [TrafficRecovery::Decision]
|
|
55
|
-
def determine_color(config, recovery_metrics
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
recovery_started_at = state_snapshot.recovery_started_at || state_snapshot.recovery_scheduled_after
|
|
59
|
-
|
|
60
|
-
# TODO: Need to add metrics cleanup and we can just use recovery_metrics.errors > 0
|
|
61
|
-
if recovery_metrics.last_error_at && recovery_metrics.last_error_at >= recovery_started_at
|
|
54
|
+
def determine_color(config, recovery_metrics)
|
|
55
|
+
if recovery_metrics.consecutive_errors > 0
|
|
62
56
|
TrafficRecovery::RED
|
|
63
57
|
elsif recovery_metrics.consecutive_successes >= config.recovery_threshold
|
|
64
58
|
TrafficRecovery::GREEN
|