stoplight 5.7.0 → 5.8.2
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/UPGRADING.md +303 -0
- data/lib/generators/stoplight/install/install_generator.rb +6 -1
- data/lib/stoplight/admin/dependencies.rb +1 -1
- data/lib/stoplight/admin/helpers.rb +20 -4
- data/lib/stoplight/admin/lights_repository/light.rb +22 -6
- data/lib/stoplight/admin/lights_repository.rb +6 -5
- data/lib/stoplight/admin/views/_card.erb +8 -5
- data/lib/stoplight/color.rb +9 -0
- data/lib/stoplight/data_store.rb +28 -0
- data/lib/stoplight/domain/compatibility_result.rb +7 -7
- data/lib/stoplight/domain/config.rb +38 -39
- data/lib/stoplight/domain/error_tracking_policy.rb +27 -0
- data/lib/stoplight/domain/failure.rb +1 -1
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +2 -0
- data/lib/stoplight/domain/light.rb +15 -46
- data/lib/stoplight/domain/light_info.rb +7 -0
- data/lib/stoplight/domain/metrics_snapshot.rb +58 -0
- data/lib/stoplight/domain/state_snapshot.rb +29 -23
- data/lib/stoplight/domain/storage/recovery_lock_token.rb +15 -0
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +18 -26
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +9 -12
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +41 -51
- data/lib/stoplight/domain/tracker/recovery_probe.rb +16 -33
- data/lib/stoplight/domain/tracker/request.rb +12 -31
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +8 -11
- data/lib/stoplight/domain/traffic_control/error_rate.rb +19 -15
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +6 -10
- data/lib/stoplight/domain/traffic_recovery.rb +3 -4
- data/lib/stoplight/error.rb +46 -0
- data/lib/stoplight/infrastructure/{data_store/fail_safe.rb → fail_safe/data_store.rb} +39 -51
- data/lib/stoplight/infrastructure/fail_safe/storage/metrics.rb +65 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock.rb +69 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rb +19 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/state.rb +62 -0
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/metrics.rb +2 -2
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/recovery_lock_store.rb +10 -12
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/recovery_lock_token.rb +3 -6
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/sliding_window.rb +21 -26
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/state.rb +3 -3
- data/lib/stoplight/infrastructure/{data_store/memory.rb → memory/data_store.rb} +36 -32
- data/lib/stoplight/infrastructure/memory/storage/recovery_lock.rb +35 -0
- data/lib/stoplight/infrastructure/memory/storage/recovery_metrics.rb +16 -0
- data/lib/stoplight/infrastructure/memory/storage/state.rb +155 -0
- data/lib/stoplight/infrastructure/memory/storage/unbounded_metrics.rb +103 -0
- data/lib/stoplight/infrastructure/memory/storage/window_metrics.rb +101 -0
- data/lib/stoplight/infrastructure/notifier/fail_safe.rb +9 -21
- data/lib/stoplight/infrastructure/notifier/generic.rb +4 -14
- data/lib/stoplight/infrastructure/notifier/io.rb +1 -2
- data/lib/stoplight/infrastructure/notifier/logger.rb +1 -2
- data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/recovery_lock_store.rb +9 -22
- data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/recovery_lock_token.rb +7 -14
- data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/scripting.rb +22 -20
- data/lib/stoplight/infrastructure/{data_store/redis.rb → redis/data_store.rb} +47 -55
- data/lib/stoplight/infrastructure/redis/storage/key_space.rb +51 -0
- data/lib/stoplight/infrastructure/redis/storage/metrics.rb +40 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_lock/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_lock.rb +64 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_metrics.rb +20 -0
- data/lib/stoplight/infrastructure/redis/storage/scripting.rb +18 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua +10 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +10 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua +9 -0
- data/lib/stoplight/infrastructure/redis/storage/state.rb +141 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_failure.lua +28 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_success.lua +26 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb +123 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/metrics_snapshot.lua +26 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_failure.lua +36 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_success.lua +35 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics.rb +174 -0
- data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +3 -10
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +8 -11
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +6 -14
- data/lib/stoplight/infrastructure/storage/compatibility_state.rb +6 -17
- data/lib/stoplight/infrastructure/system_clock.rb +16 -0
- data/lib/stoplight/notifier.rb +11 -0
- data/lib/stoplight/state.rb +9 -0
- data/lib/stoplight/types.rb +29 -0
- data/lib/stoplight/undefined.rb +16 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/config_compatibility_validator.rb +54 -0
- data/lib/stoplight/wiring/configuration_dsl.rb +101 -0
- data/lib/stoplight/wiring/data_store_backend.rb +26 -0
- data/lib/stoplight/wiring/default.rb +1 -1
- data/lib/stoplight/wiring/default_config.rb +21 -0
- data/lib/stoplight/wiring/default_configuration.rb +70 -53
- data/lib/stoplight/wiring/light_builder.rb +76 -63
- data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +3 -3
- data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +4 -4
- data/lib/stoplight/wiring/light_factory.rb +78 -52
- data/lib/stoplight/wiring/memory/backend.rb +57 -0
- data/lib/stoplight/wiring/redis/backend.rb +116 -0
- data/lib/stoplight/wiring/storage_set.rb +12 -0
- data/lib/stoplight/wiring/storage_set_builder.rb +51 -0
- data/lib/stoplight/wiring/system/light_builder.rb +47 -0
- data/lib/stoplight/wiring/system/light_factory.rb +64 -0
- data/lib/stoplight/wiring/system.rb +129 -0
- data/lib/stoplight.rb +196 -25
- data/sig/_private/generators/stoplight/install/install_generator.rbs +22 -0
- data/sig/_private/stoplight/common/deprecations.rbs +9 -0
- data/sig/_private/stoplight/data_store.rbs +6 -0
- data/sig/_private/stoplight/domain/compatibility_result.rbs +18 -0
- data/sig/_private/stoplight/domain/config.rbs +65 -0
- data/sig/_private/stoplight/domain/error_tracking_policy.rbs +14 -0
- data/sig/_private/stoplight/domain/failure.rbs +16 -0
- data/sig/_private/stoplight/domain/light.rbs +25 -0
- data/sig/_private/stoplight/domain/light_info.rbs +19 -0
- data/sig/_private/stoplight/domain/metrics_snapshot.rbs +38 -0
- data/sig/_private/stoplight/domain/ports/clock.rbs +18 -0
- data/sig/_private/stoplight/domain/ports/data_store.rbs +76 -0
- data/{lib/stoplight/domain/light_factory.rb → sig/_private/stoplight/domain/ports/light_factory.rbs} +33 -28
- data/sig/_private/stoplight/domain/ports/metrics_store.rbs +29 -0
- data/sig/_private/stoplight/domain/ports/recovery_lock_store.rbs +52 -0
- data/sig/_private/stoplight/domain/ports/recovery_lock_token.rbs +6 -0
- data/sig/_private/stoplight/domain/ports/run_strategy.rbs +14 -0
- data/sig/_private/stoplight/domain/ports/state_store.rbs +79 -0
- data/sig/_private/stoplight/domain/ports/traffic_control.rbs +41 -0
- data/sig/_private/stoplight/domain/ports/traffic_recovery.rbs +47 -0
- data/sig/_private/stoplight/domain/state_snapshot.rbs +32 -0
- data/sig/_private/stoplight/domain/storage/recovery_lock_token.rbs +11 -0
- data/sig/_private/stoplight/domain/strategies/green_run_strategy.rbs +17 -0
- data/sig/_private/stoplight/domain/strategies/red_run_strategy.rbs +17 -0
- data/sig/_private/stoplight/domain/strategies/yellow_run_strategy.rbs +42 -0
- data/{lib/stoplight/domain/tracker/base.rb → sig/_private/stoplight/domain/tracker/base.rbs} +0 -4
- data/sig/_private/stoplight/domain/tracker/recovery_probe.rbs +25 -0
- data/sig/_private/stoplight/domain/tracker/request.rbs +26 -0
- data/sig/_private/stoplight/domain/traffic_control/consecutive_errors.rbs +9 -0
- data/sig/_private/stoplight/domain/traffic_control/error_rate.rbs +13 -0
- data/sig/_private/stoplight/domain/traffic_recovery/consecutive_successes.rbs +9 -0
- data/sig/_private/stoplight/domain/traffic_recovery.rbs +9 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/data_store.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock.rbs +29 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/state.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_store.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_token.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/sliding_window.rbs +27 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/state.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store.rbs +30 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/recovery_lock.rbs +15 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/recovery_metrics.rbs +10 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/state.rbs +28 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/unbounded_metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/window_metrics.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/notifier/fail_safe.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/notifier/generic.rbs +18 -0
- data/sig/_private/stoplight/infrastructure/notifier/io.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/notifier/logger.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_store.rbs +24 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_token.rbs +21 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/scripting.rbs +34 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store.rbs +67 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/key_space.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/metrics.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/recovery_lock.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/recovery_metrics.rbs +10 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/scripting.rbs +13 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/state.rbs +32 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/unbounded_metrics.rbs +21 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs +34 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_metrics.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_lock.rbs +13 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_metrics.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_state.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/system_clock.rbs +7 -0
- data/sig/_private/stoplight/system/light_builder.rbs +23 -0
- data/sig/_private/stoplight/system/light_factory.rbs +17 -0
- data/sig/_private/stoplight/types.rbs +6 -0
- data/sig/_private/stoplight/wiring/config_compatibility_validator.rbs +19 -0
- data/sig/_private/stoplight/wiring/configuration_dsl.rbs +43 -0
- data/sig/_private/stoplight/wiring/data_store_backend.rbs +11 -0
- data/sig/_private/stoplight/wiring/default.rbs +26 -0
- data/{lib/stoplight/wiring/data_store/memory.rb → sig/_private/stoplight/wiring/default_config.rbs} +1 -4
- data/sig/_private/stoplight/wiring/default_configuration.rbs +29 -0
- data/sig/_private/stoplight/wiring/light_builder.rbs +48 -0
- data/sig/_private/stoplight/wiring/light_factory/traffic_control_dsl.rbs +7 -0
- data/sig/_private/stoplight/wiring/light_factory/traffic_recovery_dsl.rbs +7 -0
- data/sig/_private/stoplight/wiring/light_factory.rbs +16 -0
- data/sig/_private/stoplight/wiring/memory/backend.rbs +26 -0
- data/sig/_private/stoplight/wiring/notifier_factory.rbs +10 -0
- data/sig/_private/stoplight/wiring/redis/backend.rbs +38 -0
- data/sig/_private/stoplight/wiring/storage_set.rbs +38 -0
- data/sig/_private/stoplight/wiring/storage_set_builder.rbs +15 -0
- data/sig/_private/stoplight/wiring/system.rbs +15 -0
- data/sig/_private/stoplight.rbs +48 -0
- data/sig/stoplight/color.rbs +7 -0
- data/sig/stoplight/data_store.rbs +19 -0
- data/sig/stoplight/error.rbs +20 -0
- data/sig/stoplight/notifier.rbs +11 -0
- data/sig/stoplight/ports/configuration.rbs +19 -0
- data/sig/stoplight/ports/exception_matcher.rbs +8 -0
- data/sig/stoplight/ports/light.rbs +12 -0
- data/sig/stoplight/ports/light_info.rbs +5 -0
- data/sig/stoplight/ports/state_transition_notifier.rbs +15 -0
- data/sig/stoplight/ports/system.rbs +21 -0
- data/sig/stoplight/state.rbs +7 -0
- data/sig/stoplight/undefined.rbs +9 -0
- data/sig/stoplight/version.rbs +3 -0
- data/sig/stoplight.rbs +66 -0
- metadata +175 -47
- data/lib/stoplight/domain/color.rb +0 -11
- data/lib/stoplight/domain/data_store.rb +0 -146
- data/lib/stoplight/domain/error.rb +0 -42
- data/lib/stoplight/domain/metrics.rb +0 -64
- data/lib/stoplight/domain/recovery_lock_token.rb +0 -15
- data/lib/stoplight/domain/state.rb +0 -11
- data/lib/stoplight/domain/state_transition_notifier.rb +0 -25
- data/lib/stoplight/domain/storage/metrics.rb +0 -42
- data/lib/stoplight/domain/storage/recovery_lock.rb +0 -56
- data/lib/stoplight/domain/storage/state.rb +0 -87
- data/lib/stoplight/domain/strategies/run_strategy.rb +0 -22
- data/lib/stoplight/domain/traffic_control/base.rb +0 -74
- data/lib/stoplight/domain/traffic_recovery/base.rb +0 -79
- data/lib/stoplight/wiring/data_store/base.rb +0 -11
- data/lib/stoplight/wiring/data_store/redis.rb +0 -25
- data/lib/stoplight/wiring/default_factory_builder.rb +0 -25
- data/lib/stoplight/wiring/light/default_config.rb +0 -18
- data/lib/stoplight/wiring/light/system_config.rb +0 -11
- data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +0 -55
- data/lib/stoplight/wiring/light_factory/config_normalizer.rb +0 -71
- data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +0 -72
- data/lib/stoplight/wiring/public_api.rb +0 -29
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/get_metrics.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_failure.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_recovery_probe_failure.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_recovery_probe_success.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/record_success.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/release_lock.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_green.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_red.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store}/lua_scripts/transition_to_yellow.lua +0 -0
|
@@ -4,7 +4,7 @@ require "forwardable"
|
|
|
4
4
|
|
|
5
5
|
module Stoplight
|
|
6
6
|
module Infrastructure
|
|
7
|
-
module
|
|
7
|
+
module Redis
|
|
8
8
|
# == Errors
|
|
9
9
|
# All errors are stored in the sorted set where keys are serialized errors and
|
|
10
10
|
# values (Redis uses "score" term) contain integer representations of the time
|
|
@@ -17,7 +17,8 @@ module Stoplight
|
|
|
17
17
|
# of errors happened within last +config.window_size+ seconds (by default infinity).
|
|
18
18
|
#
|
|
19
19
|
# @see Base
|
|
20
|
-
|
|
20
|
+
# steep:ignore:start
|
|
21
|
+
class DataStore
|
|
21
22
|
extend Forwardable
|
|
22
23
|
|
|
23
24
|
class << self
|
|
@@ -30,6 +31,8 @@ module Stoplight
|
|
|
30
31
|
[KEY_PREFIX, *pieces].join(KEY_SEPARATOR)
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
METRICS_RETENTION_TIME = 60 * 60 * 24 # 1 day
|
|
35
|
+
|
|
33
36
|
# Retrieves the list of Redis bucket keys required to cover a specific time window.
|
|
34
37
|
#
|
|
35
38
|
# @param light_name [String] The name of the light (used as part of the Redis key).
|
|
@@ -40,7 +43,7 @@ module Stoplight
|
|
|
40
43
|
# @api private
|
|
41
44
|
def buckets_for_window(light_name, metric:, window_end:, window_size:)
|
|
42
45
|
window_end_ts = window_end.to_i
|
|
43
|
-
window_start_ts = window_end_ts - [window_size,
|
|
46
|
+
window_start_ts = window_end_ts - [window_size, METRICS_RETENTION_TIME].compact.min.to_i
|
|
44
47
|
|
|
45
48
|
# Find bucket timestamps that contain any part of the window
|
|
46
49
|
start_bucket = (window_start_ts / bucket_size) * bucket_size
|
|
@@ -75,11 +78,11 @@ module Stoplight
|
|
|
75
78
|
KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
|
|
76
79
|
|
|
77
80
|
# @!attribute recovery_lock_store
|
|
78
|
-
# @return [Stoplight::Infrastructure::DataStore::
|
|
81
|
+
# @return [Stoplight::Infrastructure::Redis::DataStore::RecoveryLockStore]
|
|
79
82
|
protected attr_reader :recovery_lock_store
|
|
80
83
|
|
|
81
84
|
# @!attribute scripting
|
|
82
|
-
# @return [Stoplight::Infrastructure::DataStore::
|
|
85
|
+
# @return [Stoplight::Infrastructure::Redis::DataStore::Scripting]
|
|
83
86
|
protected attr_reader :scripting
|
|
84
87
|
|
|
85
88
|
# @!attribute redis
|
|
@@ -90,12 +93,18 @@ module Stoplight
|
|
|
90
93
|
# @return [Boolean]
|
|
91
94
|
protected attr_reader :warn_on_clock_skew
|
|
92
95
|
|
|
96
|
+
# @!attribute clock
|
|
97
|
+
# @return [Stoplight::Domain::_Clock]
|
|
98
|
+
private attr_reader :clock
|
|
99
|
+
|
|
93
100
|
# @param redis [::Redis, ConnectionPool<::Redis>]
|
|
94
|
-
# @param recovery_lock_store [Stoplight::Infrastructure::DataStore::
|
|
101
|
+
# @param recovery_lock_store [Stoplight::Infrastructure::Redis::DataStore::RecoveryLockStore]
|
|
95
102
|
# @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
|
|
96
|
-
# @param scripting [Stoplight::Infrastructure::DataStore::
|
|
103
|
+
# @param scripting [Stoplight::Infrastructure::Redis::DataStore::Scripting]
|
|
104
|
+
# @param clock [Stoplight::Domain::_Clock]
|
|
97
105
|
# the application server
|
|
98
|
-
def initialize(redis:, recovery_lock_store:, scripting:, warn_on_clock_skew: true)
|
|
106
|
+
def initialize(redis:, recovery_lock_store:, scripting:, clock:, warn_on_clock_skew: true)
|
|
107
|
+
@clock = clock
|
|
99
108
|
@warn_on_clock_skew = warn_on_clock_skew
|
|
100
109
|
@redis = redis
|
|
101
110
|
@recovery_lock_store = recovery_lock_store
|
|
@@ -117,11 +126,11 @@ module Stoplight
|
|
|
117
126
|
end
|
|
118
127
|
|
|
119
128
|
# @param config [Stoplight::Domain::Config]
|
|
120
|
-
# @return [Stoplight::Domain::
|
|
129
|
+
# @return [Stoplight::Domain::MetricsSnapshot]
|
|
121
130
|
def get_metrics(config)
|
|
122
131
|
config.name
|
|
123
132
|
|
|
124
|
-
window_end_ts = current_time.to_f
|
|
133
|
+
window_end_ts = clock.current_time.to_f
|
|
125
134
|
window_start_ts = window_end_ts - config.window_size.to_i
|
|
126
135
|
|
|
127
136
|
if config.window_size
|
|
@@ -149,18 +158,18 @@ module Stoplight
|
|
|
149
158
|
consecutive_errors = config.window_size ? [consecutive_errors.to_i, errors].min : consecutive_errors.to_i
|
|
150
159
|
consecutive_successes = config.window_size ? [consecutive_successes.to_i, successes].min : consecutive_successes.to_i
|
|
151
160
|
|
|
152
|
-
Domain::
|
|
161
|
+
Domain::MetricsSnapshot.new(
|
|
153
162
|
successes: (successes if config.window_size),
|
|
154
163
|
errors: (errors if config.window_size),
|
|
155
164
|
consecutive_errors:,
|
|
156
165
|
consecutive_successes:,
|
|
157
166
|
last_error: deserialize_failure(last_error_json),
|
|
158
|
-
last_success_at: (
|
|
167
|
+
last_success_at: (clock.at(last_success_at.to_f) if last_success_at)
|
|
159
168
|
)
|
|
160
169
|
end
|
|
161
170
|
|
|
162
171
|
# @param config [Stoplight::Domain::Config]
|
|
163
|
-
# @return [Stoplight::Domain::
|
|
172
|
+
# @return [Stoplight::Domain::MetricsSnapshot]
|
|
164
173
|
def get_recovery_metrics(config)
|
|
165
174
|
last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
|
|
166
175
|
client.hmget(
|
|
@@ -169,12 +178,12 @@ module Stoplight
|
|
|
169
178
|
)
|
|
170
179
|
end
|
|
171
180
|
|
|
172
|
-
Domain::
|
|
181
|
+
Domain::MetricsSnapshot.new(
|
|
173
182
|
successes: nil, errors: nil,
|
|
174
183
|
consecutive_errors: consecutive_errors.to_i,
|
|
175
184
|
consecutive_successes: consecutive_successes.to_i,
|
|
176
185
|
last_error: deserialize_failure(last_error_json),
|
|
177
|
-
last_success_at: (
|
|
186
|
+
last_success_at: (clock.at(last_success_at.to_f) if last_success_at)
|
|
178
187
|
)
|
|
179
188
|
end
|
|
180
189
|
|
|
@@ -190,17 +199,17 @@ module Stoplight
|
|
|
190
199
|
recovery_started_at = recovery_started_at_raw&.to_f
|
|
191
200
|
|
|
192
201
|
Domain::StateSnapshot.new(
|
|
193
|
-
breached_at: (
|
|
194
|
-
locked_state: locked_state ||
|
|
195
|
-
recovery_scheduled_after: (
|
|
196
|
-
recovery_started_at: (
|
|
197
|
-
time: current_time
|
|
202
|
+
breached_at: (clock.at(breached_at) if breached_at),
|
|
203
|
+
locked_state: locked_state || State::UNLOCKED,
|
|
204
|
+
recovery_scheduled_after: (clock.at(recovery_scheduled_after) if recovery_scheduled_after),
|
|
205
|
+
recovery_started_at: (clock.at(recovery_started_at) if recovery_started_at),
|
|
206
|
+
time: clock.current_time
|
|
198
207
|
)
|
|
199
208
|
end
|
|
200
209
|
|
|
201
210
|
def clear_metrics(config)
|
|
202
211
|
if config.window_size
|
|
203
|
-
window_end_ts = current_time.to_i
|
|
212
|
+
window_end_ts = clock.current_time.to_i
|
|
204
213
|
@redis.with do |client|
|
|
205
214
|
client.multi do |tx|
|
|
206
215
|
tx.unlink(
|
|
@@ -223,26 +232,12 @@ module Stoplight
|
|
|
223
232
|
end
|
|
224
233
|
end
|
|
225
234
|
|
|
226
|
-
private def state_snapshot_from_hash(data, time: current_time)
|
|
227
|
-
breached_at = data[:breached_at]&.to_f
|
|
228
|
-
recovery_scheduled_after = data[:recovery_scheduled_after]&.to_f
|
|
229
|
-
recovery_started_at = data[:recovery_started_at]&.to_f
|
|
230
|
-
|
|
231
|
-
Domain::StateSnapshot.new(
|
|
232
|
-
breached_at: (Time.at(breached_at) if breached_at),
|
|
233
|
-
locked_state: data[:locked_state] || Domain::State::UNLOCKED,
|
|
234
|
-
recovery_scheduled_after: (Time.at(recovery_scheduled_after) if recovery_scheduled_after),
|
|
235
|
-
recovery_started_at: (Time.at(recovery_started_at) if recovery_started_at),
|
|
236
|
-
time:
|
|
237
|
-
)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
235
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
241
236
|
# @param exception [Exception]
|
|
242
237
|
# @return [void]
|
|
243
238
|
def record_failure(config, exception)
|
|
244
|
-
current_time =
|
|
245
|
-
current_ts = current_time.to_f
|
|
239
|
+
current_time = clock.current_time
|
|
240
|
+
current_ts = clock.current_time.to_f
|
|
246
241
|
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
247
242
|
|
|
248
243
|
scripting.call(
|
|
@@ -256,7 +251,7 @@ module Stoplight
|
|
|
256
251
|
end
|
|
257
252
|
|
|
258
253
|
def record_success(config, request_id: SecureRandom.hex(12))
|
|
259
|
-
current_ts = current_time.to_f
|
|
254
|
+
current_ts = clock.current_time.to_f
|
|
260
255
|
|
|
261
256
|
scripting.call(
|
|
262
257
|
:record_success,
|
|
@@ -274,8 +269,8 @@ module Stoplight
|
|
|
274
269
|
# @param exception [Exception]
|
|
275
270
|
# @return [void]
|
|
276
271
|
def record_recovery_probe_failure(config, exception)
|
|
277
|
-
current_time =
|
|
278
|
-
current_ts = current_time.to_f
|
|
272
|
+
current_time = clock.current_time
|
|
273
|
+
current_ts = clock.current_time.to_f
|
|
279
274
|
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
280
275
|
|
|
281
276
|
scripting.call(
|
|
@@ -290,7 +285,7 @@ module Stoplight
|
|
|
290
285
|
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
291
286
|
# @return [void]
|
|
292
287
|
def record_recovery_probe_success(config)
|
|
293
|
-
current_ts = current_time.to_f
|
|
288
|
+
current_ts = clock.current_time.to_f
|
|
294
289
|
|
|
295
290
|
scripting.call(
|
|
296
291
|
:record_recovery_probe_success,
|
|
@@ -317,11 +312,11 @@ module Stoplight
|
|
|
317
312
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
318
313
|
def transition_to_color(config, color)
|
|
319
314
|
case color
|
|
320
|
-
when
|
|
315
|
+
when Color::GREEN
|
|
321
316
|
transition_to_green(config)
|
|
322
|
-
when
|
|
317
|
+
when Color::YELLOW
|
|
323
318
|
transition_to_yellow(config)
|
|
324
|
-
when
|
|
319
|
+
when Color::RED
|
|
325
320
|
transition_to_red(config)
|
|
326
321
|
else
|
|
327
322
|
raise ArgumentError, "Invalid color: #{color}"
|
|
@@ -329,12 +324,12 @@ module Stoplight
|
|
|
329
324
|
end
|
|
330
325
|
|
|
331
326
|
# @param config [Stoplight::Domain::Config]
|
|
332
|
-
# @return [Stoplight::Infrastructure::DataStore::
|
|
327
|
+
# @return [Stoplight::Infrastructure::Redis::DataStore::RecoveryLockToken, nil]
|
|
333
328
|
def acquire_recovery_lock(config)
|
|
334
329
|
recovery_lock_store.acquire_lock(config.name)
|
|
335
330
|
end
|
|
336
331
|
|
|
337
|
-
# @param lock [Stoplight::Infrastructure::DataStore::
|
|
332
|
+
# @param lock [Stoplight::Infrastructure::Redis::DataStore::RecoveryLockToken]
|
|
338
333
|
# @return [void]
|
|
339
334
|
def release_recovery_lock(lock)
|
|
340
335
|
recovery_lock_store.release_lock(lock)
|
|
@@ -345,7 +340,7 @@ module Stoplight
|
|
|
345
340
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
346
341
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
347
342
|
private def transition_to_green(config)
|
|
348
|
-
current_ts = current_time.to_f
|
|
343
|
+
current_ts = clock.current_time.to_f
|
|
349
344
|
meta_key = metadata_key(config)
|
|
350
345
|
|
|
351
346
|
became_green = scripting.call(
|
|
@@ -361,7 +356,7 @@ module Stoplight
|
|
|
361
356
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
362
357
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
363
358
|
private def transition_to_yellow(config)
|
|
364
|
-
current_ts = current_time.to_f
|
|
359
|
+
current_ts = clock.current_time.to_f
|
|
365
360
|
meta_key = metadata_key(config)
|
|
366
361
|
|
|
367
362
|
became_yellow = scripting.call(
|
|
@@ -377,7 +372,7 @@ module Stoplight
|
|
|
377
372
|
# @param config [Stoplight::Domain::Config] The light configuration
|
|
378
373
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
379
374
|
private def transition_to_red(config)
|
|
380
|
-
current_ts = current_time.to_f
|
|
375
|
+
current_ts = clock.current_time.to_f
|
|
381
376
|
meta_key = metadata_key(config)
|
|
382
377
|
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
383
378
|
|
|
@@ -410,7 +405,7 @@ module Stoplight
|
|
|
410
405
|
|
|
411
406
|
error_class = error_object["class"]
|
|
412
407
|
error_message = error_object["message"]
|
|
413
|
-
time =
|
|
408
|
+
time = clock.at(object["time"])
|
|
414
409
|
|
|
415
410
|
Domain::Failure.new(error_class, error_message, time)
|
|
416
411
|
end
|
|
@@ -505,7 +500,7 @@ module Stoplight
|
|
|
505
500
|
return unless should_sample?(0.01) # 1% chance
|
|
506
501
|
|
|
507
502
|
redis_seconds, _redis_millis = @redis.then(&:time)
|
|
508
|
-
app_seconds = current_time.to_i
|
|
503
|
+
app_seconds = clock.current_time.to_i
|
|
509
504
|
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
|
510
505
|
warn("Detected clock skew between Redis and the application server. Redis time: #{redis_seconds}, Application time: #{app_seconds}. See https://github.com/bolshakov/stoplight/wiki/Clock-Skew-and-Stoplight-Reliability")
|
|
511
506
|
end
|
|
@@ -514,11 +509,8 @@ module Stoplight
|
|
|
514
509
|
private def should_sample?(probability)
|
|
515
510
|
rand <= probability
|
|
516
511
|
end
|
|
517
|
-
|
|
518
|
-
private def current_time
|
|
519
|
-
Time.now
|
|
520
|
-
end
|
|
521
512
|
end
|
|
513
|
+
# steep:ignore:end
|
|
522
514
|
end
|
|
523
515
|
end
|
|
524
516
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module Redis
|
|
8
|
+
module Storage
|
|
9
|
+
# Immutable key namespace for a light within a system.
|
|
10
|
+
#
|
|
11
|
+
# Produces keys following entity-first structure:
|
|
12
|
+
# stoplight:v6:{system_id}:{light_id}:locks:recovery
|
|
13
|
+
# stoplight:v6:{system_id}:{light_id}:metrics:successes
|
|
14
|
+
# stoplight:v6:{system_id}:{light_id}:state
|
|
15
|
+
#
|
|
16
|
+
# Identifiers are derived from SHA-256 and truncated to 12 characters. Collisions are extremely
|
|
17
|
+
# unlikely at expected system scale.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# key_space = KeySpace.build(system_name: "payments", light_name: "stripe-api")
|
|
21
|
+
# key_space.key(:locks, :recovery) #=> "stoplight:v6:df384ae97c77:cfe6861fa39e:locks:recovery"
|
|
22
|
+
#
|
|
23
|
+
KeySpace = Data.define(:system_id, :light_id)
|
|
24
|
+
|
|
25
|
+
class KeySpace
|
|
26
|
+
# @!attribute system_id
|
|
27
|
+
# 12-char hex identifier for the system
|
|
28
|
+
#
|
|
29
|
+
# @!attribute light_id
|
|
30
|
+
# 12-char hex identifier for the light
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def build(system_name:, light_name:) = new(
|
|
34
|
+
system_id: hash_name(system_name),
|
|
35
|
+
light_id: hash_name(light_name)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Generates a truncated SHA256 hash for use in Redis keys.
|
|
39
|
+
def hash_name(name) = Digest::SHA256.hexdigest(name.to_s)[0, 12] #: String
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Builds a Redis key within this namespace.
|
|
43
|
+
#
|
|
44
|
+
# @param pieces Key segments to append
|
|
45
|
+
# @return Full Redis key
|
|
46
|
+
def key(*pieces) = [:stoplight, :v5, system_id, "{#{light_id}}", *pieces].join(":")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module Redis
|
|
8
|
+
module Storage
|
|
9
|
+
class Metrics
|
|
10
|
+
def serialize_exception(exception, timestamp:)
|
|
11
|
+
JSON.generate(
|
|
12
|
+
{
|
|
13
|
+
error: {
|
|
14
|
+
class: exception.class.name,
|
|
15
|
+
message: exception.message
|
|
16
|
+
},
|
|
17
|
+
time: timestamp
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deserialize_failure(failure_json)
|
|
23
|
+
return if failure_json.nil?
|
|
24
|
+
|
|
25
|
+
object = JSON.parse(failure_json)
|
|
26
|
+
error_object = object["error"]
|
|
27
|
+
|
|
28
|
+
error_class = error_object["class"]
|
|
29
|
+
error_message = error_object["message"]
|
|
30
|
+
time = Time.at(object["time"])
|
|
31
|
+
|
|
32
|
+
Domain::Failure.new(error_class, error_message, time)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def metrics_ttl = 86400 * 7 # 7 days
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Redis
|
|
6
|
+
module Storage
|
|
7
|
+
# Distributed recovery lock using Redis SET NX (set-if-not-exists).
|
|
8
|
+
#
|
|
9
|
+
# Lock Acquisition:
|
|
10
|
+
# - Uses unique UUID token to prevent accidental release of others' locks
|
|
11
|
+
# - Atomic SET with NX flag ensures only one process acquires recovery_lock
|
|
12
|
+
# - TTL (px: lock_timeout) auto-releases recovery_lock if process crashes
|
|
13
|
+
#
|
|
14
|
+
# Lock Release:
|
|
15
|
+
# - Lua script ensures only token holder can release (token comparison)
|
|
16
|
+
# - Best-effort release; TTL cleanup handles failures
|
|
17
|
+
#
|
|
18
|
+
# Failure Modes:
|
|
19
|
+
# - Lock contention: Returns false, caller should skip probe
|
|
20
|
+
# - Redis unavailable: raises an error and let caller decide
|
|
21
|
+
# - Crashed holder: raises an error and let caller decide. Lock auto-expires after lock_timeout
|
|
22
|
+
# - Release failure: Lock auto-expires after lock_timeout
|
|
23
|
+
#
|
|
24
|
+
class RecoveryLock
|
|
25
|
+
def initialize(config:, redis:, scripting:, key_space:)
|
|
26
|
+
@config = config
|
|
27
|
+
@redis = redis
|
|
28
|
+
@scripting = scripting
|
|
29
|
+
@key_space = key_space
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def acquire_lock
|
|
33
|
+
recovery_lock = Domain::Storage::RecoveryLockToken.new
|
|
34
|
+
|
|
35
|
+
acquired = redis.then do |client|
|
|
36
|
+
client.set(lock_key, recovery_lock.token, nx: true, px: lock_timeout)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
recovery_lock if acquired
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param recovery_lock [Stoplight::Infrastructure::Redis::DataStore::RecoveryLockToken]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def release_lock(recovery_lock)
|
|
45
|
+
scripting.call(
|
|
46
|
+
:"recovery_lock/release_lock",
|
|
47
|
+
keys: [lock_key], args: [recovery_lock.token]
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :config
|
|
54
|
+
attr_reader :redis
|
|
55
|
+
attr_reader :scripting
|
|
56
|
+
attr_reader :key_space
|
|
57
|
+
|
|
58
|
+
def lock_key = key_space.key(:locks, :recovery)
|
|
59
|
+
def lock_timeout = config.cool_off_time_in_milliseconds
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Redis
|
|
6
|
+
module Storage
|
|
7
|
+
# When a circuit is RED (open), Stoplight periodically sends "recovery probes"
|
|
8
|
+
# to test whether the protected service has recovered. These test requests have
|
|
9
|
+
# different semantics than normal requests and their metrics are tracked separately.
|
|
10
|
+
#
|
|
11
|
+
class RecoveryMetrics < UnboundedMetrics
|
|
12
|
+
def initialize(redis:, scripting:, key_space:, clock:)
|
|
13
|
+
super
|
|
14
|
+
@metrics_key = key_space.key(:recovery_metrics)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Redis
|
|
6
|
+
module Storage
|
|
7
|
+
class Scripting < Infrastructure::Redis::DataStore::Scripting
|
|
8
|
+
SCRIPTS_ROOT = __dir__ => String
|
|
9
|
+
private_constant :SCRIPTS_ROOT
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def default_scripts_root = SCRIPTS_ROOT
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
local meta_key = KEYS[1]
|
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
|
3
|
+
|
|
4
|
+
-- 1 if the field is a new field in the hash and the value was set
|
|
5
|
+
local became_green = redis.call('HSETNX', meta_key, 'recovered_at', current_ts)
|
|
6
|
+
|
|
7
|
+
if became_green == 1 then
|
|
8
|
+
redis.call("HDEL", meta_key, 'recovery_started_at', 'recovery_scheduled_after', 'breached_at')
|
|
9
|
+
end
|
|
10
|
+
return became_green
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
local meta_key = KEYS[1]
|
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
|
3
|
+
local recovery_scheduled_after_ts = tonumber(ARGV[2])
|
|
4
|
+
|
|
5
|
+
-- 1 if the field is a new field in the hash and the value was set
|
|
6
|
+
local became_red = redis.call('HSETNX', meta_key, 'breached_at', current_ts)
|
|
7
|
+
|
|
8
|
+
redis.call('HSET', meta_key, 'recovery_scheduled_after', recovery_scheduled_after_ts)
|
|
9
|
+
redis.call("HDEL", meta_key, "recovery_started_at", "recovered_at")
|
|
10
|
+
return became_red
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
local meta_key = KEYS[1]
|
|
2
|
+
local current_ts = tonumber(ARGV[1])
|
|
3
|
+
|
|
4
|
+
-- HSETNX returns 1 if field is new and was set, 0 if field already exists
|
|
5
|
+
local became_yellow = redis.call('HSETNX', meta_key, 'recovery_started_at', current_ts)
|
|
6
|
+
if became_yellow == 1 then
|
|
7
|
+
redis.call('HDEL', meta_key, 'recovery_scheduled_after', 'breached_at', 'recovered_at')
|
|
8
|
+
end
|
|
9
|
+
return became_yellow
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Infrastructure
|
|
5
|
+
module Redis
|
|
6
|
+
module Storage
|
|
7
|
+
# Redis-backed state storage for a single circuit breaker.
|
|
8
|
+
#
|
|
9
|
+
# Manages circuit breaker state transitions using Redis hashes and Lua scripts
|
|
10
|
+
# for atomic operations. Ensures notification deduplication across distributed
|
|
11
|
+
# processes - when multiple processes detect the same circuit condition,
|
|
12
|
+
# only one will receive +true+ from transition methods.
|
|
13
|
+
#
|
|
14
|
+
# All state is stored in a single Redis hash with fields:
|
|
15
|
+
# - +locked_state+: forced lock (UNLOCKED, LOCKED_GREEN, LOCKED_RED)
|
|
16
|
+
# - +breached_at+: timestamp (float) when circuit opened
|
|
17
|
+
# - +recovery_scheduled_after+: timestamp (float) when recovery probe allowed
|
|
18
|
+
# - +recovery_started_at+: timestamp (float) when recovery probe began
|
|
19
|
+
#
|
|
20
|
+
# @example Basic usage
|
|
21
|
+
# state = State.new(
|
|
22
|
+
# clock: SystemClock.new,
|
|
23
|
+
# redis: Redis.new,
|
|
24
|
+
# scripting: Scripting.new(redis:),
|
|
25
|
+
# key_space: KeySpace.build(light_name: "payments", system_name: "main"),
|
|
26
|
+
# cool_off_time: 60
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# # Multiple processes may call this concurrently
|
|
30
|
+
# if state.transition_to_color(Color::RED)
|
|
31
|
+
# # Only one process reaches here - send notification
|
|
32
|
+
# notifier.notify("payments", :opened)
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @note Thread safety is guaranteed by Redis's single-threaded execution model
|
|
36
|
+
# and the use of Lua scripts for atomic multistep operations.
|
|
37
|
+
#
|
|
38
|
+
# @see Stoplight::Memory::State for the in-memory equivalent
|
|
39
|
+
# @see Stoplight::Domain::StateSnapshot for the structure of state snapshots
|
|
40
|
+
#
|
|
41
|
+
class State
|
|
42
|
+
def initialize(clock:, redis:, scripting:, key_space:, cool_off_time:)
|
|
43
|
+
@redis = redis
|
|
44
|
+
@scripting = scripting
|
|
45
|
+
@key_space = key_space
|
|
46
|
+
@clock = clock
|
|
47
|
+
@cool_off_time = cool_off_time
|
|
48
|
+
|
|
49
|
+
@state_key = key_space.key(:state)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_state(state)
|
|
53
|
+
redis.with do |client|
|
|
54
|
+
client.hset(state_key, "locked_state", state)
|
|
55
|
+
end
|
|
56
|
+
state
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def state_snapshot
|
|
60
|
+
breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw = redis.with do |client|
|
|
61
|
+
client.hmget(state_key, :breached_at, :locked_state, :recovery_scheduled_after, :recovery_started_at)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Domain::StateSnapshot.new(
|
|
65
|
+
breached_at: breached_at_raw && clock.at(breached_at_raw.to_f),
|
|
66
|
+
locked_state: locked_state || Stoplight::State::UNLOCKED,
|
|
67
|
+
recovery_scheduled_after: recovery_scheduled_after_raw && clock.at(recovery_scheduled_after_raw.to_f),
|
|
68
|
+
recovery_started_at: recovery_started_at_raw && clock.at(recovery_started_at_raw.to_f),
|
|
69
|
+
time: clock.current_time
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def clear
|
|
74
|
+
redis.with do |client|
|
|
75
|
+
client.del(state_key)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def transition_to_color(color)
|
|
80
|
+
case color
|
|
81
|
+
when Color::GREEN
|
|
82
|
+
transition_to_green
|
|
83
|
+
when Color::YELLOW
|
|
84
|
+
transition_to_yellow
|
|
85
|
+
when Color::RED
|
|
86
|
+
transition_to_red
|
|
87
|
+
else
|
|
88
|
+
raise ArgumentError, "Invalid color: #{color}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
attr_reader :redis
|
|
95
|
+
attr_reader :scripting
|
|
96
|
+
attr_reader :key_space
|
|
97
|
+
attr_reader :clock
|
|
98
|
+
attr_reader :cool_off_time
|
|
99
|
+
attr_reader :state_key
|
|
100
|
+
|
|
101
|
+
# Transitions to GREEN state and ensures only one notification
|
|
102
|
+
#
|
|
103
|
+
def transition_to_green
|
|
104
|
+
became_green = scripting.call(
|
|
105
|
+
:"state/transition_to_green",
|
|
106
|
+
args: [clock.current_time.to_f],
|
|
107
|
+
keys: [state_key]
|
|
108
|
+
)
|
|
109
|
+
became_green == 1
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
113
|
+
#
|
|
114
|
+
def transition_to_yellow
|
|
115
|
+
became_yellow = scripting.call(
|
|
116
|
+
:"state/transition_to_yellow",
|
|
117
|
+
args: [clock.current_time.to_f],
|
|
118
|
+
keys: [state_key]
|
|
119
|
+
)
|
|
120
|
+
became_yellow == 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Transitions to RED state and ensures only one notification
|
|
124
|
+
#
|
|
125
|
+
def transition_to_red
|
|
126
|
+
current_ts = clock.current_time.to_f
|
|
127
|
+
recovery_scheduled_after_ts = current_ts + cool_off_time
|
|
128
|
+
|
|
129
|
+
became_red = scripting.call(
|
|
130
|
+
:"state/transition_to_red",
|
|
131
|
+
args: [current_ts, recovery_scheduled_after_ts],
|
|
132
|
+
keys: [state_key]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
became_red == 1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|