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
|
@@ -13,39 +13,42 @@ module Stoplight
|
|
|
13
13
|
# @see Stoplight::Domain::LightFactory
|
|
14
14
|
# @see Stoplight()
|
|
15
15
|
# @api private
|
|
16
|
-
|
|
17
|
-
class LightFactory
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
CONFIG_KEYS = Domain::Config.members.freeze
|
|
22
|
-
private_constant :CONFIG_KEYS
|
|
23
|
-
|
|
24
|
-
# @!attribute [r] settings
|
|
25
|
-
# @return [Hash]
|
|
26
|
-
protected attr_reader :settings
|
|
27
|
-
|
|
28
|
-
def initialize(settings = {})
|
|
29
|
-
@settings = settings
|
|
30
|
-
|
|
31
|
-
validate_settings!
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private def validate_settings!
|
|
35
|
-
recognized = CONFIG_KEYS + DEPENDENCY_KEYS
|
|
36
|
-
unknown = settings.keys - recognized
|
|
37
|
-
|
|
38
|
-
return if unknown.empty?
|
|
39
|
-
|
|
40
|
-
raise ArgumentError, "Unknown settings: #{unknown.join(", ")}", caller(2)
|
|
16
|
+
#
|
|
17
|
+
class LightFactory
|
|
18
|
+
def initialize(config:)
|
|
19
|
+
@config = config
|
|
41
20
|
end
|
|
42
21
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
22
|
+
def with(
|
|
23
|
+
name: T.undefined,
|
|
24
|
+
cool_off_time: T.undefined,
|
|
25
|
+
threshold: T.undefined,
|
|
26
|
+
recovery_threshold: T.undefined,
|
|
27
|
+
window_size: T.undefined,
|
|
28
|
+
tracked_errors: T.undefined,
|
|
29
|
+
skipped_errors: T.undefined,
|
|
30
|
+
data_store: T.undefined,
|
|
31
|
+
error_notifier: T.undefined,
|
|
32
|
+
notifiers: T.undefined,
|
|
33
|
+
traffic_control: T.undefined,
|
|
34
|
+
traffic_recovery: T.undefined
|
|
35
|
+
)
|
|
36
|
+
self.class.new(
|
|
37
|
+
config: ConfigurationDsl.new(
|
|
38
|
+
name:,
|
|
39
|
+
cool_off_time:,
|
|
40
|
+
threshold:,
|
|
41
|
+
recovery_threshold:,
|
|
42
|
+
window_size:,
|
|
43
|
+
tracked_errors:,
|
|
44
|
+
skipped_errors:,
|
|
45
|
+
traffic_control:,
|
|
46
|
+
traffic_recovery:,
|
|
47
|
+
error_notifier:,
|
|
48
|
+
data_store:,
|
|
49
|
+
notifiers:
|
|
50
|
+
).configure!(config)
|
|
51
|
+
)
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
# Builds a fully-configured Light instance.
|
|
@@ -65,37 +68,60 @@ module Stoplight
|
|
|
65
68
|
# light.run { api_call }
|
|
66
69
|
|
|
67
70
|
def build
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
light_builder(config:).build
|
|
72
|
+
end
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
dependency_settings
|
|
74
|
-
)
|
|
75
|
-
LightBuilder.new(factory: self, config:, **dependencies).build
|
|
74
|
+
def ==(other)
|
|
75
|
+
other.is_a?(self.class) && other.config == config
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
def validate_configuration!
|
|
80
|
-
config_settings = settings.slice(*CONFIG_KEYS)
|
|
81
|
-
dependency_settings = settings.slice(*DEPENDENCY_KEYS)
|
|
78
|
+
alias_method :eql?, :==
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
def build_with(
|
|
81
|
+
name: T.undefined,
|
|
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
|
+
data_store: T.undefined,
|
|
89
|
+
error_notifier: T.undefined,
|
|
90
|
+
notifiers: T.undefined,
|
|
91
|
+
traffic_control: T.undefined,
|
|
92
|
+
traffic_recovery: T.undefined
|
|
93
|
+
)
|
|
94
|
+
with(
|
|
95
|
+
name:,
|
|
96
|
+
cool_off_time:,
|
|
97
|
+
threshold:,
|
|
98
|
+
recovery_threshold:,
|
|
99
|
+
window_size:,
|
|
100
|
+
tracked_errors:,
|
|
101
|
+
skipped_errors:,
|
|
102
|
+
data_store:,
|
|
103
|
+
error_notifier:,
|
|
104
|
+
notifiers:,
|
|
105
|
+
traffic_control:,
|
|
106
|
+
traffic_recovery:
|
|
107
|
+
).build
|
|
88
108
|
end
|
|
89
109
|
|
|
90
|
-
def
|
|
91
|
-
|
|
110
|
+
def hash
|
|
111
|
+
[self.class, config].hash
|
|
92
112
|
end
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
protected
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
attr_reader :config
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def light_builder(config:)
|
|
121
|
+
LightBuilder.new(config:, factory: light_factory)
|
|
98
122
|
end
|
|
123
|
+
|
|
124
|
+
def light_factory = self
|
|
99
125
|
end
|
|
100
126
|
end
|
|
101
127
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Wiring
|
|
5
|
+
module Memory
|
|
6
|
+
# In-memory storage backend for single-process deployments.
|
|
7
|
+
#
|
|
8
|
+
# All storage components use thread-safe in-memory data structures.
|
|
9
|
+
# State is not shared across processes and is lost on restart.
|
|
10
|
+
#
|
|
11
|
+
# Memory backend is also used as the fallback layer for Redis backend
|
|
12
|
+
# when Redis is unavailable.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# backend = Memory::Backend.new(clock: SystemClock.new, config:)
|
|
16
|
+
# backend.state_store #=> Memory::Storage::State
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class Backend < DataStoreBackend
|
|
20
|
+
def initialize(clock:, config:)
|
|
21
|
+
@clock = clock
|
|
22
|
+
@config = config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def state_store
|
|
26
|
+
@state_store ||= Infrastructure::Memory::Storage::State.new(
|
|
27
|
+
clock: @clock,
|
|
28
|
+
cool_off_time: @config.cool_off_time
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def recovery_lock_store
|
|
33
|
+
@recovery_lock_store ||= Infrastructure::Memory::Storage::RecoveryLock.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def recovery_metrics_store
|
|
37
|
+
@recovery_metrics_store ||= Infrastructure::Memory::Storage::RecoveryMetrics.new(
|
|
38
|
+
clock: @clock
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def windowed_metrics_store
|
|
43
|
+
@windowed_metrics_store ||= Infrastructure::Memory::Storage::WindowMetrics.new(
|
|
44
|
+
window_size: T.must(@config.window_size),
|
|
45
|
+
clock: @clock
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unbounded_metrics_store
|
|
50
|
+
@unbounded_metrics_store ||= Infrastructure::Memory::Storage::UnboundedMetrics.new(
|
|
51
|
+
clock: @clock
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
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
|