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
|
@@ -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,40 +35,68 @@ 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.
|
|
42
58
|
#
|
|
43
59
|
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
44
|
-
# @param
|
|
60
|
+
# @param state_snapshot [Stoplight::Domain::StateSnapshot]
|
|
45
61
|
# @yield The code block to execute.
|
|
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
|
-
def execute(fallback,
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
def execute(fallback, state_snapshot:, &code)
|
|
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
|
|
|
@@ -72,26 +108,17 @@ module Stoplight
|
|
|
72
108
|
request_tracker.record_failure(error)
|
|
73
109
|
end
|
|
74
110
|
|
|
75
|
-
# @param
|
|
111
|
+
# @param state_snapshot [Stoplight::Domain::StateSnapshot]
|
|
76
112
|
# @return [void]
|
|
77
|
-
private def enter_recovery(
|
|
78
|
-
return if
|
|
113
|
+
private def enter_recovery(state_snapshot)
|
|
114
|
+
return if state_snapshot.recovery_started?
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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)
|
|
84
120
|
end
|
|
85
121
|
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
122
|
end
|
|
96
123
|
end
|
|
97
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,51 +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
|
-
recover
|
|
44
|
+
recover
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def record_success
|
|
42
|
-
|
|
48
|
+
metrics_store.record_success
|
|
43
49
|
|
|
44
|
-
recover
|
|
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
|
-
private def recover
|
|
53
|
-
|
|
57
|
+
private def recover
|
|
58
|
+
recovery_metrics = metrics_store.metrics_snapshot
|
|
59
|
+
recovery_result = traffic_recovery.determine_color(config, recovery_metrics)
|
|
54
60
|
|
|
55
|
-
return if recovery_result == TrafficRecovery::
|
|
61
|
+
return if recovery_result == TrafficRecovery::YELLOW
|
|
56
62
|
|
|
57
63
|
from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
|
|
58
64
|
raise "recovery strategy returned unexpected color: #{recovery_result}"
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def ==(other)
|
|
67
|
-
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
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
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,41 +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
|
-
|
|
49
|
+
metrics_store.record_failure(exception)
|
|
50
|
+
metrics = metrics_store.metrics_snapshot
|
|
44
51
|
|
|
45
|
-
transition_to_red(exception,
|
|
52
|
+
transition_to_red(exception, metrics:)
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
# @return [void]
|
|
49
|
-
def record_success
|
|
50
|
-
data_store.record_success(config)
|
|
51
|
-
end
|
|
56
|
+
def record_success = metrics_store.record_success
|
|
52
57
|
|
|
53
|
-
private def transition_to_red(exception,
|
|
54
|
-
if traffic_control.stop_traffic?(config,
|
|
55
|
-
|
|
58
|
+
private def transition_to_red(exception, metrics:)
|
|
59
|
+
if traffic_control.stop_traffic?(config, metrics)
|
|
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
|
|
56
67
|
end
|
|
57
68
|
end
|
|
58
|
-
|
|
59
|
-
# @param other [any]
|
|
60
|
-
# @return [bool]
|
|
61
|
-
def ==(other)
|
|
62
|
-
super && traffic_control == other.traffic_control
|
|
63
|
-
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
66
71
|
end
|
|
@@ -18,11 +18,11 @@ module Stoplight
|
|
|
18
18
|
# end
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
|
-
# def stop_traffic?(config,
|
|
22
|
-
# total =
|
|
21
|
+
# def stop_traffic?(config, metrics)
|
|
22
|
+
# total = metrics.successes + metrics.failures
|
|
23
23
|
# return false if total < 10 # Minimum sample size
|
|
24
24
|
#
|
|
25
|
-
# error_rate =
|
|
25
|
+
# error_rate = metrics.failures.fdiv(total)
|
|
26
26
|
# error_rate >= 0.5 # Stop traffic when error rate reaches 50%
|
|
27
27
|
# end
|
|
28
28
|
# end
|
|
@@ -44,10 +44,10 @@ module Stoplight
|
|
|
44
44
|
# current state and metrics.
|
|
45
45
|
#
|
|
46
46
|
# @param config [Stoplight::Domain::Config]
|
|
47
|
-
# @param
|
|
47
|
+
# @param metrics [Stoplight::Domain::Metrics]
|
|
48
48
|
# @return [Boolean] true if traffic should be stopped (rec), false otherwise (green)
|
|
49
49
|
# :nocov:
|
|
50
|
-
def stop_traffic?(config,
|
|
50
|
+
def stop_traffic?(config, metrics)
|
|
51
51
|
raise NotImplementedError
|
|
52
52
|
end
|
|
53
53
|
# :nocov:
|
|
@@ -42,14 +42,10 @@ module Stoplight
|
|
|
42
42
|
# Determines if traffic should be stopped based on failure counts.
|
|
43
43
|
#
|
|
44
44
|
# @param config [Stoplight::Domain::Config]
|
|
45
|
-
# @param
|
|
45
|
+
# @param metrics [Stoplight::Domain::Metrics]
|
|
46
46
|
# @return [Boolean] true if failures have reached the threshold, false otherwise
|
|
47
|
-
def stop_traffic?(config,
|
|
48
|
-
|
|
49
|
-
[metadata.consecutive_errors, metadata.errors].min >= config.threshold
|
|
50
|
-
else
|
|
51
|
-
metadata.consecutive_errors >= config.threshold
|
|
52
|
-
end
|
|
47
|
+
def stop_traffic?(config, metrics)
|
|
48
|
+
metrics.consecutive_errors >= config.threshold
|
|
53
49
|
end
|
|
54
50
|
end
|
|
55
51
|
end
|
|
@@ -40,10 +40,10 @@ module Stoplight
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# @param config [Stoplight::Domain::Config]
|
|
43
|
-
# @param
|
|
43
|
+
# @param metrics [Stoplight::Domain::Metrics]
|
|
44
44
|
# @return [Boolean]
|
|
45
|
-
def stop_traffic?(config,
|
|
46
|
-
|
|
45
|
+
def stop_traffic?(config, metrics)
|
|
46
|
+
metrics.requests >= min_requests && metrics.error_rate >= config.threshold
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -14,14 +14,14 @@ module Stoplight
|
|
|
14
14
|
# @min_samples = min_samples
|
|
15
15
|
# end
|
|
16
16
|
#
|
|
17
|
-
# def determine_color(config,
|
|
18
|
-
# total_probes =
|
|
17
|
+
# def determine_color(config, metrics)
|
|
18
|
+
# total_probes = metrics.recovery_probe_successes + metrics.recovery_probe_errors
|
|
19
19
|
#
|
|
20
20
|
# if total_probes < @min_samples
|
|
21
21
|
# return Color::YELLOW # Keep recovering, not enough samples
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
|
-
# success_rate =
|
|
24
|
+
# success_rate = metrics.recovery_probe_successes.fdiv(total_probes)
|
|
25
25
|
# if success_rate >= @min_success_rate
|
|
26
26
|
# Color::GREEN # Recovery successful
|
|
27
27
|
# elsif success_rate <= 0.2
|
|
@@ -49,10 +49,10 @@ module Stoplight
|
|
|
49
49
|
# current metrics and recovery progress.
|
|
50
50
|
#
|
|
51
51
|
# @param config [Stoplight::Domain::Config]
|
|
52
|
-
# @param
|
|
52
|
+
# @param metrics [Stoplight::Domain::Metrics]
|
|
53
53
|
# @return [TrafficRecovery::Decision]
|
|
54
54
|
# :nocov:
|
|
55
|
-
def determine_color(config,
|
|
55
|
+
def determine_color(config, metrics)
|
|
56
56
|
raise NotImplementedError
|
|
57
57
|
end
|
|
58
58
|
# :nocov:
|
|
@@ -49,16 +49,12 @@ module Stoplight
|
|
|
49
49
|
# Determines if traffic should be resumed based on successes counts.
|
|
50
50
|
#
|
|
51
51
|
# @param config [Stoplight::Domain::Config]
|
|
52
|
-
# @param
|
|
52
|
+
# @param recovery_metrics [Stoplight::Domain::Metrics]
|
|
53
53
|
# @return [TrafficRecovery::Decision]
|
|
54
|
-
def determine_color(config,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
recovery_started_at = metadata.recovery_started_at || metadata.recovery_scheduled_after
|
|
58
|
-
|
|
59
|
-
if metadata.last_error_at && metadata.last_error_at >= recovery_started_at
|
|
54
|
+
def determine_color(config, recovery_metrics)
|
|
55
|
+
if recovery_metrics.consecutive_errors > 0
|
|
60
56
|
TrafficRecovery::RED
|
|
61
|
-
elsif
|
|
57
|
+
elsif recovery_metrics.consecutive_successes >= config.recovery_threshold
|
|
62
58
|
TrafficRecovery::GREEN
|
|
63
59
|
else
|
|
64
60
|
TrafficRecovery::YELLOW
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module DataStore
|
|
8
|
+
# A wrapper around a data store that provides fail-safe mechanisms using a
|
|
9
|
+
# circuit breaker. It ensures that operations on the data store can gracefully
|
|
10
|
+
# handle failures by falling back to default values when necessary.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class FailSafe < Domain::DataStore
|
|
14
|
+
# @!attribute data_store
|
|
15
|
+
# @return [Stoplight::DataStore::Base] The underlying primary data store being used
|
|
16
|
+
attr_reader :data_store
|
|
17
|
+
|
|
18
|
+
# @!attribute error_notifier
|
|
19
|
+
# @return [Proc]
|
|
20
|
+
attr_reader :error_notifier
|
|
21
|
+
|
|
22
|
+
# @!attribute failover_data_store
|
|
23
|
+
# @return [Stoplight::DataStore::Base] The fallback data store used when the primary fails.
|
|
24
|
+
attr_reader :failover_data_store
|
|
25
|
+
|
|
26
|
+
# @!attribute circuit_breaker
|
|
27
|
+
# @return [Stoplight::Light] The circuit breaker used to handle data store failures.
|
|
28
|
+
private attr_reader :circuit_breaker
|
|
29
|
+
|
|
30
|
+
# @param data_store [Stoplight::Domain::DataStore]
|
|
31
|
+
# @param error_notifier [Proc]
|
|
32
|
+
# @param failover_data_store [Stoplight::Domain::DataStore]
|
|
33
|
+
# @param circuit_breaker [Stoplight::Domain::Light]
|
|
34
|
+
def initialize(data_store:, error_notifier:, failover_data_store:, circuit_breaker:)
|
|
35
|
+
@data_store = data_store
|
|
36
|
+
@error_notifier = error_notifier
|
|
37
|
+
@failover_data_store = failover_data_store
|
|
38
|
+
@circuit_breaker = circuit_breaker
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def names
|
|
42
|
+
with_fallback(:names) do
|
|
43
|
+
data_store.names
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_metrics(config, *args, **kwargs)
|
|
48
|
+
with_fallback(:get_metrics, config, *args, **kwargs) do
|
|
49
|
+
data_store.get_metrics(config, *args, **kwargs)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_recovery_metrics(config, *args, **kwargs)
|
|
54
|
+
with_fallback(:get_recovery_metrics, config, *args, **kwargs) do
|
|
55
|
+
data_store.get_recovery_metrics(config, *args, **kwargs)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_state_snapshot(config)
|
|
60
|
+
with_fallback(:get_state_snapshot, config) do
|
|
61
|
+
data_store.get_state_snapshot(config)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def clear_metrics(config)
|
|
66
|
+
with_fallback(:clear_metrics, config) do
|
|
67
|
+
data_store.clear_metrics(config)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def clear_recovery_metrics(config)
|
|
72
|
+
with_fallback(:clear_recovery_metrics, config) do
|
|
73
|
+
data_store.clear_recovery_metrics(config)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def record_failure(config, *args, **kwargs)
|
|
78
|
+
with_fallback(:record_failure, config, *args, **kwargs) do
|
|
79
|
+
data_store.record_failure(config, *args, **kwargs)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def record_success(config, *args, **kwargs)
|
|
84
|
+
with_fallback(:record_success, config, *args, **kwargs) do
|
|
85
|
+
data_store.record_success(config, *args, **kwargs)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def record_recovery_probe_success(config, *args, **kwargs)
|
|
90
|
+
with_fallback(:record_recovery_probe_success, config, *args, **kwargs) do
|
|
91
|
+
data_store.record_recovery_probe_success(config, *args, **kwargs)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def record_recovery_probe_failure(config, *args, **kwargs)
|
|
96
|
+
with_fallback(:record_recovery_probe_failure, config, *args, **kwargs) do
|
|
97
|
+
data_store.record_recovery_probe_failure(config, *args, **kwargs)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set_state(config, *args, **kwargs)
|
|
102
|
+
with_fallback(:set_state, config, *args, **kwargs) do
|
|
103
|
+
data_store.set_state(config, *args, **kwargs)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def transition_to_color(config, *args, **kwargs)
|
|
108
|
+
with_fallback(:transition_to_color, config, *args, **kwargs) do
|
|
109
|
+
data_store.transition_to_color(config, *args, **kwargs)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def delete_light(config, *args, **kwargs)
|
|
114
|
+
with_fallback(:delete_light, config, *args, **kwargs) do
|
|
115
|
+
data_store.delete_light(config, *args, **kwargs)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param config [Stoplight::Domain::Config]
|
|
120
|
+
def acquire_recovery_lock(config)
|
|
121
|
+
with_fallback(:acquire_recovery_lock, config) do
|
|
122
|
+
data_store.acquire_recovery_lock(config)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Routes release to correct store based on token type.
|
|
127
|
+
# Redis tokens release via primary (with error notification on failure).
|
|
128
|
+
# Memory tokens release via failover directly.
|
|
129
|
+
#
|
|
130
|
+
# @param recovery_lock_token [Stoplight::Domain::RecoveryLockToken]
|
|
131
|
+
def release_recovery_lock(recovery_lock_token)
|
|
132
|
+
case recovery_lock_token
|
|
133
|
+
in Redis::RecoveryLockToken
|
|
134
|
+
fallback = proc do |error|
|
|
135
|
+
error_notifier.call(error) if error
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
circuit_breaker.run(fallback) do
|
|
139
|
+
data_store.release_recovery_lock(recovery_lock_token)
|
|
140
|
+
end
|
|
141
|
+
in Memory::RecoveryLockToken
|
|
142
|
+
failover_data_store.release_recovery_lock(recovery_lock_token)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ==(other)
|
|
147
|
+
other.is_a?(self.class) && other.data_store == data_store && other.error_notifier == error_notifier &&
|
|
148
|
+
other.failover_data_store == failover_data_store
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @param method_name [Symbol] protected method name
|
|
152
|
+
private def with_fallback(method_name, *args, **kwargs, &code)
|
|
153
|
+
fallback = proc do |error|
|
|
154
|
+
config = args.first
|
|
155
|
+
error_notifier.call(error) if config && error
|
|
156
|
+
@failover_data_store.public_send(method_name, *args, **kwargs)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
circuit_breaker.run(fallback, &code)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module DataStore
|
|
6
|
+
class Memory
|
|
7
|
+
class Metrics
|
|
8
|
+
attr_accessor :consecutive_errors
|
|
9
|
+
attr_accessor :consecutive_successes
|
|
10
|
+
attr_accessor :last_error
|
|
11
|
+
attr_accessor :last_success_at
|
|
12
|
+
|
|
13
|
+
def initialize(consecutive_errors: 0, consecutive_successes: 0, last_error: nil, last_success_at: nil)
|
|
14
|
+
@consecutive_errors = consecutive_errors
|
|
15
|
+
@consecutive_successes = consecutive_successes
|
|
16
|
+
@last_error = last_error
|
|
17
|
+
@last_success_at = last_success_at
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def last_error_at
|
|
21
|
+
@last_error&.time
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|