stoplight 5.6.0 → 5.8.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/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 +26 -5
- data/lib/stoplight/admin/lights_repository/light.rb +22 -6
- data/lib/stoplight/admin/lights_repository.rb +20 -16
- data/lib/stoplight/admin/views/_card.erb +8 -5
- data/lib/stoplight/admin.rb +2 -1
- data/lib/stoplight/color.rb +9 -0
- data/lib/stoplight/common/deprecations.rb +11 -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 -35
- 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 +122 -16
- data/lib/stoplight/domain/light.rb +44 -64
- 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 +74 -58
- data/lib/stoplight/domain/tracker/recovery_probe.rb +27 -43
- data/lib/stoplight/domain/tracker/request.rb +24 -39
- 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 +8 -18
- data/lib/stoplight/domain/traffic_recovery.rb +3 -5
- data/lib/stoplight/error.rb +46 -0
- data/lib/stoplight/infrastructure/fail_safe/data_store.rb +152 -0
- 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/memory/data_store/recovery_lock_store.rb +52 -0
- data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_token.rb +17 -0
- 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} +90 -57
- 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 +50 -0
- 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/redis/data_store/lua_scripts/record_recovery_probe_failure.lua +27 -0
- data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_success.lua +23 -0
- data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_store.rb +60 -0
- data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_token.rb +28 -0
- data/lib/stoplight/infrastructure/redis/data_store/scripting.rb +73 -0
- data/lib/stoplight/infrastructure/{data_store/redis.rb → redis/data_store.rb} +173 -210
- 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 +41 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +33 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +47 -0
- data/lib/stoplight/infrastructure/storage/compatibility_state.rb +44 -0
- 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 +2 -2
- 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 +198 -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 +74 -135
- data/lib/stoplight/wiring/memory/backend.rb +57 -0
- data/lib/stoplight/wiring/notifier_factory.rb +26 -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 +209 -23
- 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/sig/_private/stoplight/domain/tracker/base.rbs +8 -0
- 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/sig/_private/stoplight/wiring/default_config.rbs +7 -0
- 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 +199 -36
- data/lib/stoplight/domain/color.rb +0 -11
- data/lib/stoplight/domain/data_store.rb +0 -130
- data/lib/stoplight/domain/error.rb +0 -42
- data/lib/stoplight/domain/metrics.rb +0 -85
- data/lib/stoplight/domain/state.rb +0 -11
- data/lib/stoplight/domain/state_transition_notifier.rb +0 -25
- data/lib/stoplight/domain/strategies/run_strategy.rb +0 -27
- data/lib/stoplight/domain/tracker/base.rb +0 -41
- data/lib/stoplight/domain/traffic_control/base.rb +0 -74
- data/lib/stoplight/domain/traffic_recovery/base.rb +0 -80
- 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/default_factory_builder.rb +0 -25
- 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/light/default_config.rb +0 -18
- data/lib/stoplight/wiring/light/system_config.rb +0 -11
- data/lib/stoplight/wiring/public_api.rb +0 -28
- 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 → 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_success.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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class NotifierFactory
|
|
6
|
+
class << self
|
|
7
|
+
# Wraps a notifier with fail-safe mechanisms.
|
|
8
|
+
#
|
|
9
|
+
# @param notifier [Stoplight::Domain::StateTransitionNotifier] The notifier to wrap.
|
|
10
|
+
# @param error_notifier [Proc] called when wrapped data store fails
|
|
11
|
+
# @return [Stoplight::Notifier::FailSafe] The original notifier if it is already
|
|
12
|
+
# a +FailSafe+ instance, otherwise a new +FailSafe+ instance.
|
|
13
|
+
def create(notifier:, error_notifier:)
|
|
14
|
+
case notifier
|
|
15
|
+
in Infrastructure::Notifier::FailSafe if notifier.error_notifier == error_notifier
|
|
16
|
+
notifier
|
|
17
|
+
in Infrastructure::Notifier::FailSafe
|
|
18
|
+
Infrastructure::Notifier::FailSafe.new(notifier: notifier.notifier, error_notifier:)
|
|
19
|
+
else
|
|
20
|
+
Infrastructure::Notifier::FailSafe.new(notifier:, error_notifier:)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
module Redis
|
|
6
|
+
# Redis storage backend with automatic failover to in-memory storage.
|
|
7
|
+
#
|
|
8
|
+
# Every storage component is wrapped in a FailSafe decorator that catches
|
|
9
|
+
# Redis connection errors and falls back to a Memory backend. This ensures
|
|
10
|
+
# circuit breakers remain functional even when Redis is unavailable.
|
|
11
|
+
#
|
|
12
|
+
# The failover behavior is coordinated through a dedicated circuit breaker
|
|
13
|
+
# (`failover_light`) that prevents repeated Redis connection attempts during
|
|
14
|
+
# an outage.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# backend = Redis::Backend.new(
|
|
18
|
+
# redis: redis_connection,
|
|
19
|
+
# scripting: Scripting.new(redis:),
|
|
20
|
+
# key_space: KeySpace.build(system_name: "payments", light_name: "stripe"),
|
|
21
|
+
# config: light_config,
|
|
22
|
+
# error_notifier: ->(e) { Logger.error(e) },
|
|
23
|
+
# failover_light: Stoplight("redis-failover"),
|
|
24
|
+
# clock: SystemClock.new
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# backend.state_store #=> FailSafe::State wrapping Redis::State
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
class Backend < DataStoreBackend
|
|
31
|
+
def initialize(redis:, scripting:, key_space:, config:, error_notifier:, failover_light:, clock:)
|
|
32
|
+
@redis = redis
|
|
33
|
+
@scripting = scripting
|
|
34
|
+
@key_space = key_space
|
|
35
|
+
@clock = clock
|
|
36
|
+
@config = config
|
|
37
|
+
@error_notifier = error_notifier
|
|
38
|
+
@failover_light = failover_light
|
|
39
|
+
@memory_fallback = Memory::Backend.new(clock:, config:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def state_store
|
|
43
|
+
@state_store ||= Infrastructure::FailSafe::Storage::State.new(
|
|
44
|
+
primary_store: Infrastructure::Redis::Storage::State.new(
|
|
45
|
+
redis: @redis,
|
|
46
|
+
scripting: @scripting,
|
|
47
|
+
key_space: @key_space,
|
|
48
|
+
cool_off_time: @config.cool_off_time,
|
|
49
|
+
clock: @clock
|
|
50
|
+
),
|
|
51
|
+
error_notifier: @error_notifier,
|
|
52
|
+
failover_store: @memory_fallback.state_store,
|
|
53
|
+
circuit_breaker: @failover_light
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def recovery_lock_store
|
|
58
|
+
@recovery_lock_store ||= Infrastructure::FailSafe::Storage::RecoveryLock.new(
|
|
59
|
+
primary_store: Infrastructure::Redis::Storage::RecoveryLock.new(
|
|
60
|
+
config: @config, # TODO: pass cool_off_time directly
|
|
61
|
+
redis: @redis,
|
|
62
|
+
scripting: @scripting,
|
|
63
|
+
key_space: @key_space
|
|
64
|
+
),
|
|
65
|
+
error_notifier: @error_notifier,
|
|
66
|
+
failover_store: @memory_fallback.recovery_lock_store,
|
|
67
|
+
circuit_breaker: @failover_light
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def recovery_metrics_store
|
|
72
|
+
@recovery_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
|
|
73
|
+
error_notifier: @error_notifier,
|
|
74
|
+
primary_store: Infrastructure::Redis::Storage::RecoveryMetrics.new(
|
|
75
|
+
clock: @clock,
|
|
76
|
+
redis: @redis,
|
|
77
|
+
scripting: @scripting,
|
|
78
|
+
key_space: @key_space
|
|
79
|
+
),
|
|
80
|
+
failover_store: @memory_fallback.recovery_metrics_store,
|
|
81
|
+
circuit_breaker: @failover_light
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def windowed_metrics_store
|
|
86
|
+
@windowed_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
|
|
87
|
+
error_notifier: @error_notifier,
|
|
88
|
+
primary_store: Infrastructure::Redis::Storage::WindowMetrics.new(
|
|
89
|
+
config: @config, # TODO: pass window size directly
|
|
90
|
+
redis: @redis,
|
|
91
|
+
scripting: @scripting,
|
|
92
|
+
clock: @clock,
|
|
93
|
+
key_space: @key_space
|
|
94
|
+
),
|
|
95
|
+
failover_store: @memory_fallback.windowed_metrics_store,
|
|
96
|
+
circuit_breaker: @failover_light
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unbounded_metrics_store
|
|
101
|
+
@unbounded_metrics_store ||= Infrastructure::FailSafe::Storage::Metrics.new(
|
|
102
|
+
error_notifier: @error_notifier,
|
|
103
|
+
primary_store: Infrastructure::Redis::Storage::UnboundedMetrics.new(
|
|
104
|
+
clock: @clock,
|
|
105
|
+
redis: @redis,
|
|
106
|
+
scripting: @scripting,
|
|
107
|
+
key_space: @key_space
|
|
108
|
+
),
|
|
109
|
+
failover_store: @memory_fallback.unbounded_metrics_store,
|
|
110
|
+
circuit_breaker: @failover_light
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
# Assembles a StorageSet from a backend, selecting windowed or unbounded metrics.
|
|
6
|
+
#
|
|
7
|
+
# StorageSetBuilder is the single point where the windowed/unbounded decision
|
|
8
|
+
# is made. All other storage components (state, recovery lock, recovery metrics)
|
|
9
|
+
# are backend-specific but mode-independent.
|
|
10
|
+
#
|
|
11
|
+
# @example Windowed metrics (error rate tracking)
|
|
12
|
+
# builder = StorageSetBuilder.new(backend: redis_backend, windowed: true)
|
|
13
|
+
# storage = builder.build
|
|
14
|
+
# storage.metrics_store #=> FailSafe<WindowMetrics>
|
|
15
|
+
#
|
|
16
|
+
# @example Unbounded metrics (consecutive error tracking)
|
|
17
|
+
# builder = StorageSetBuilder.new(backend: memory_backend, windowed: false)
|
|
18
|
+
# storage = builder.build
|
|
19
|
+
# storage.metrics_store #=> UnboundedMetrics
|
|
20
|
+
#
|
|
21
|
+
# @see DataStoreBackend
|
|
22
|
+
# @see StorageSet
|
|
23
|
+
# @api private
|
|
24
|
+
class StorageSetBuilder
|
|
25
|
+
attr_reader :backend
|
|
26
|
+
attr_reader :windowed
|
|
27
|
+
|
|
28
|
+
def initialize(backend:, windowed:)
|
|
29
|
+
@backend = backend
|
|
30
|
+
@windowed = windowed
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build
|
|
34
|
+
StorageSet.new(
|
|
35
|
+
metrics_store:,
|
|
36
|
+
recovery_metrics_store: backend.recovery_metrics_store,
|
|
37
|
+
state_store: backend.state_store,
|
|
38
|
+
recovery_lock_store: backend.recovery_lock_store
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private def metrics_store
|
|
43
|
+
if windowed
|
|
44
|
+
backend.windowed_metrics_store
|
|
45
|
+
else
|
|
46
|
+
backend.unbounded_metrics_store
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class System
|
|
6
|
+
class LightBuilder < Wiring::LightBuilder
|
|
7
|
+
def initialize(system:, config:, factory:)
|
|
8
|
+
@system = system
|
|
9
|
+
|
|
10
|
+
super(config:, factory:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def key_space = @key_space ||= Infrastructure::Redis::Storage::KeySpace.build(
|
|
14
|
+
system_name: system.name,
|
|
15
|
+
light_name: config.name
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def storage_set
|
|
19
|
+
@storage_set ||= StorageSetBuilder.new(backend: build_backend, windowed: !config.window_size.nil?).build
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :system
|
|
25
|
+
|
|
26
|
+
def state_store = storage_set.state_store
|
|
27
|
+
def recovery_lock_store = storage_set.recovery_lock_store
|
|
28
|
+
def recovery_metrics_store = storage_set.recovery_metrics_store
|
|
29
|
+
def metrics_store = storage_set.metrics_store
|
|
30
|
+
def storage_scripting = Infrastructure::Redis::Storage::Scripting.new(redis:)
|
|
31
|
+
def failover_system = @failover_system ||= Stoplight.__stoplight__system("failover-#{system.name}")
|
|
32
|
+
|
|
33
|
+
def build_backend
|
|
34
|
+
case data_store_config
|
|
35
|
+
in DataStore::Memory
|
|
36
|
+
Memory::Backend.new(clock:, config:)
|
|
37
|
+
in DataStore::Redis
|
|
38
|
+
Redis::Backend.new(
|
|
39
|
+
redis:, scripting: storage_scripting, key_space:, clock:, config:, error_notifier:,
|
|
40
|
+
failover_light: failover_system.light("redis")
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
class System
|
|
6
|
+
class LightFactory < Wiring::LightFactory
|
|
7
|
+
attr_reader :system
|
|
8
|
+
|
|
9
|
+
def initialize(system:, config:)
|
|
10
|
+
@system = system
|
|
11
|
+
|
|
12
|
+
super(config:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def with(
|
|
16
|
+
name: T.undefined,
|
|
17
|
+
cool_off_time: T.undefined,
|
|
18
|
+
threshold: T.undefined,
|
|
19
|
+
recovery_threshold: T.undefined,
|
|
20
|
+
window_size: T.undefined,
|
|
21
|
+
tracked_errors: T.undefined,
|
|
22
|
+
skipped_errors: T.undefined,
|
|
23
|
+
data_store: T.undefined,
|
|
24
|
+
error_notifier: T.undefined,
|
|
25
|
+
notifiers: T.undefined,
|
|
26
|
+
traffic_control: T.undefined,
|
|
27
|
+
traffic_recovery: T.undefined
|
|
28
|
+
)
|
|
29
|
+
self.class.new(
|
|
30
|
+
system:,
|
|
31
|
+
config: ConfigurationDsl.new(
|
|
32
|
+
cool_off_time:,
|
|
33
|
+
threshold:,
|
|
34
|
+
recovery_threshold:,
|
|
35
|
+
window_size:,
|
|
36
|
+
tracked_errors:,
|
|
37
|
+
skipped_errors:,
|
|
38
|
+
traffic_control:,
|
|
39
|
+
traffic_recovery:,
|
|
40
|
+
error_notifier:,
|
|
41
|
+
data_store:,
|
|
42
|
+
notifiers:
|
|
43
|
+
).configure!(config)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class InternalLightFactory < Wiring::LightFactory
|
|
48
|
+
def initialize
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def with(**untyped) # steep:ignore
|
|
52
|
+
raise NotImplementedError, "You're not allowed to extend system lights"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private def light_builder(config:)
|
|
57
|
+
System::LightBuilder.new(system:, factory: light_factory, config:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private def light_factory = InternalLightFactory.new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/map"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Wiring
|
|
7
|
+
# 🚧UNDER CONSTRUCTION 🚧
|
|
8
|
+
# System provides namespace isolation and shared configuration for related circuits.
|
|
9
|
+
#
|
|
10
|
+
# Systems enforce configuration consistency within their scope - creating the same
|
|
11
|
+
# circuit name with different settings raises +Stoplight::Error::ConfigurationError+.
|
|
12
|
+
#
|
|
13
|
+
# This prevents subtle bugs where circuits silently interfere with each other.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# billing = Stoplight.system(:billing,
|
|
17
|
+
# data_store: billing_redis,
|
|
18
|
+
# threshold: 5,
|
|
19
|
+
# window_size: 300
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
# billing.light("stripe")
|
|
23
|
+
# billing.light("paypal")
|
|
24
|
+
#
|
|
25
|
+
# @example Multi-tenancy
|
|
26
|
+
# tenant_a = Stoplight.system(:tenant_a, data_store: tenant_a_redis)
|
|
27
|
+
# tenant_b = Stoplight.system(:tenant_b, data_store: tenant_b_redis)
|
|
28
|
+
#
|
|
29
|
+
# # Same circuit name, completely isolated
|
|
30
|
+
# tenant_a.light("api")
|
|
31
|
+
# tenant_b.light("api")
|
|
32
|
+
#
|
|
33
|
+
# @example Configuration inheritance
|
|
34
|
+
# system = Stoplight.system(:payments, threshold: 3, cool_off_time: 600)
|
|
35
|
+
#
|
|
36
|
+
# system.light("stripe") # Inherits threshold: 3
|
|
37
|
+
# system.light("paypal", threshold: 5) # Overrides threshold
|
|
38
|
+
#
|
|
39
|
+
# @note System configuration objects (data_store, notifiers) should be defined
|
|
40
|
+
# as constants and reused, not created inline. This ensures configuration
|
|
41
|
+
# matching works correctly across multiple system references.
|
|
42
|
+
#
|
|
43
|
+
# @note Light instances are cached within the system. Calling {#light} with
|
|
44
|
+
# the same name returns the cached instance.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
class System
|
|
48
|
+
attr_reader :name
|
|
49
|
+
# @!attribute system_config
|
|
50
|
+
# @api private
|
|
51
|
+
attr_reader :system_config
|
|
52
|
+
|
|
53
|
+
def initialize(config:)
|
|
54
|
+
@name = config.name
|
|
55
|
+
@system_config = config
|
|
56
|
+
@lights = Concurrent::Map.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates or retrieves a light.
|
|
60
|
+
#
|
|
61
|
+
# If a light with this name already exists, returns the cached instance.
|
|
62
|
+
# If settings differ from the existing light, raises +Stoplight::Error::ConfigurationError+.
|
|
63
|
+
#
|
|
64
|
+
#
|
|
65
|
+
# @raise [Stoplight::Error::ConfigurationError] if light exists with different settings
|
|
66
|
+
#
|
|
67
|
+
# @example Create a light
|
|
68
|
+
# light = system.light("stripe", threshold: 5, window_size: 60)
|
|
69
|
+
#
|
|
70
|
+
# @example Retrieve existing light - both return cached light
|
|
71
|
+
# light = system.light("stripe", threshold: 5, window_size: 60)
|
|
72
|
+
# light = system.light("stripe")
|
|
73
|
+
#
|
|
74
|
+
# @example Configuration conflict
|
|
75
|
+
# system.light("api", threshold: 5)
|
|
76
|
+
# system.light("api", threshold: 10) # Raises ConfigurationError
|
|
77
|
+
#
|
|
78
|
+
# @note Thread-safe: multiple threads can safely call this method concurrently
|
|
79
|
+
#
|
|
80
|
+
def light(
|
|
81
|
+
name,
|
|
82
|
+
cool_off_time: T.undefined,
|
|
83
|
+
threshold: T.undefined,
|
|
84
|
+
recovery_threshold: T.undefined,
|
|
85
|
+
window_size: T.undefined,
|
|
86
|
+
tracked_errors: T.undefined,
|
|
87
|
+
skipped_errors: T.undefined,
|
|
88
|
+
traffic_control: T.undefined,
|
|
89
|
+
traffic_recovery: T.undefined
|
|
90
|
+
)
|
|
91
|
+
light_config = ConfigurationDsl.new(
|
|
92
|
+
name:,
|
|
93
|
+
cool_off_time:,
|
|
94
|
+
threshold:,
|
|
95
|
+
recovery_threshold:,
|
|
96
|
+
window_size:,
|
|
97
|
+
tracked_errors:,
|
|
98
|
+
skipped_errors:,
|
|
99
|
+
traffic_control:,
|
|
100
|
+
traffic_recovery:
|
|
101
|
+
).configure!(system_config)
|
|
102
|
+
|
|
103
|
+
light, _ = lights.compute(name) do |existing|
|
|
104
|
+
if existing
|
|
105
|
+
existing_light, existing_config = existing
|
|
106
|
+
if light_config == existing_config
|
|
107
|
+
[existing_light, existing_config]
|
|
108
|
+
else
|
|
109
|
+
raise Stoplight::Error::ConfigurationError, <<~MSG
|
|
110
|
+
Light name `#{name}` reused with different settings:
|
|
111
|
+
existing settings: #{existing_config}
|
|
112
|
+
new settings: #{light_config}
|
|
113
|
+
|
|
114
|
+
You cannot use the same light name with different settings.
|
|
115
|
+
MSG
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
[LightFactory.new(system: self, config: light_config).build, light_config]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
light
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
attr_reader :lights
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|