stoplight 5.0.3 → 5.2.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 +162 -27
- data/lib/generators/stoplight/install/USAGE +16 -0
- data/lib/generators/stoplight/install/install_generator.rb +75 -0
- data/lib/generators/stoplight/install/templates/stoplight.rb.erb +18 -0
- data/lib/stoplight/admin.rb +1 -1
- data/lib/stoplight/config/compatibility_result.rb +54 -0
- data/lib/stoplight/config/dsl.rb +97 -0
- data/lib/stoplight/config/library_default_config.rb +13 -21
- data/lib/stoplight/config/system_config.rb +7 -0
- data/lib/stoplight/config/user_default_config.rb +19 -17
- data/lib/stoplight/data_store/fail_safe.rb +10 -3
- data/lib/stoplight/data_store/memory.rb +5 -0
- data/lib/stoplight/data_store/redis.rb +4 -0
- data/lib/stoplight/default.rb +4 -5
- data/lib/stoplight/light/config.rb +87 -94
- data/lib/stoplight/metadata.rb +16 -0
- data/lib/stoplight/notifier/fail_safe.rb +5 -2
- data/lib/stoplight/traffic_control/base.rb +35 -0
- data/lib/stoplight/traffic_control/{consecutive_failures.rb → consecutive_errors.rb} +17 -5
- data/lib/stoplight/traffic_control/error_rate.rb +49 -0
- data/lib/stoplight/traffic_recovery/base.rb +27 -0
- data/lib/stoplight/traffic_recovery/consecutive_successes.rb +66 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +46 -14
- metadata +15 -6
- data/lib/stoplight/config/config_provider.rb +0 -62
- data/lib/stoplight/traffic_recovery/single_success.rb +0 -35
data/lib/stoplight.rb
CHANGED
@@ -3,16 +3,20 @@
|
|
3
3
|
require "zeitwerk"
|
4
4
|
|
5
5
|
loader = Zeitwerk::Loader.for_gem
|
6
|
-
loader.inflector.inflect("io" => "IO")
|
6
|
+
loader.inflector.inflect("io" => "IO", "dsl" => "DSL")
|
7
7
|
loader.do_not_eager_load(
|
8
8
|
"#{__dir__}/stoplight/data_store",
|
9
9
|
"#{__dir__}/stoplight/admin",
|
10
10
|
"#{__dir__}/stoplight/admin.rb"
|
11
11
|
)
|
12
|
+
loader.ignore("#{__dir__}/generators")
|
12
13
|
loader.ignore("#{__dir__}/stoplight/rspec.rb", "#{__dir__}/stoplight/rspec")
|
13
14
|
loader.setup
|
14
15
|
|
15
16
|
module Stoplight # rubocop:disable Style/Documentation
|
17
|
+
CONFIG_DSL = Config::DSL.new
|
18
|
+
private_constant :CONFIG_DSL
|
19
|
+
|
16
20
|
CONFIG_MUTEX = Mutex.new
|
17
21
|
private_constant :CONFIG_MUTEX
|
18
22
|
|
@@ -26,7 +30,7 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
26
30
|
# It raises an error if called more than once.
|
27
31
|
#
|
28
32
|
# @yield [config] Provides a configuration object to the block.
|
29
|
-
# @yieldparam config [Stoplight::Config::
|
33
|
+
# @yieldparam config [Stoplight::Config::UserDefaultConfig] The configuration object.
|
30
34
|
# @raise [Stoplight::Error::ConfigurationError] If the library is already configured.
|
31
35
|
# @return [void]
|
32
36
|
#
|
@@ -46,7 +50,7 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
46
50
|
# produces a warning:
|
47
51
|
#
|
48
52
|
# "Stoplight reconfigured. Existing circuit breakers will not see the new configuration. New
|
49
|
-
# configuration:
|
53
|
+
# configuration: ...f
|
50
54
|
#
|
51
55
|
# If you really know what you are doing, you can pass the +trust_me_im_an_engineer+ parameter as +true+ to
|
52
56
|
# suppress this warning, which could be useful in test environments.
|
@@ -55,28 +59,47 @@ module Stoplight # rubocop:disable Style/Documentation
|
|
55
59
|
user_defaults = Config::UserDefaultConfig.new
|
56
60
|
yield(user_defaults) if block_given?
|
57
61
|
|
58
|
-
reconfigured = !@
|
62
|
+
reconfigured = !@default_config.nil?
|
59
63
|
|
60
|
-
@
|
61
|
-
user_default_config: user_defaults.freeze,
|
62
|
-
library_default_config: Config::LibraryDefaultConfig.new
|
63
|
-
).tap do
|
64
|
+
@default_config = Config::LibraryDefaultConfig.with(**user_defaults.to_h).tap do
|
64
65
|
if reconfigured && !trust_me_im_an_engineer
|
65
66
|
warn(
|
66
67
|
"Stoplight reconfigured. Existing circuit breakers will not see new configuration. " \
|
67
|
-
"New configuration: #{@
|
68
|
+
"New configuration: #{@default_config.inspect}"
|
68
69
|
)
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
74
|
+
# Creates a Light for internal use.
|
75
|
+
#
|
76
|
+
# @param name [String]
|
77
|
+
# @param settings [Hash]
|
78
|
+
# @return [Stoplight::Light]
|
79
|
+
# @api private
|
80
|
+
def system_light(name, **settings)
|
81
|
+
config = Config::SystemConfig.with(name:, **settings)
|
82
|
+
Stoplight::Light.new(config)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create a Light with the user default configuration.
|
86
|
+
#
|
87
|
+
# @param name [String]
|
88
|
+
# @param settings [Hash]
|
89
|
+
# @return [Stoplight::Light]
|
90
|
+
# @api private
|
91
|
+
def light(name, **settings)
|
92
|
+
config = Stoplight.default_config.with(name:, **settings)
|
93
|
+
Stoplight::Light.new(config)
|
94
|
+
end
|
95
|
+
|
73
96
|
# Retrieves the current configuration provider.
|
74
97
|
#
|
75
|
-
# @return [Stoplight::Config
|
98
|
+
# @return [Stoplight::Light::Config]
|
76
99
|
# @api private
|
77
|
-
def
|
100
|
+
def default_config
|
78
101
|
CONFIG_MUTEX.synchronize do
|
79
|
-
@
|
102
|
+
@default_config ||= configure
|
80
103
|
end
|
81
104
|
end
|
82
105
|
end
|
@@ -94,6 +117,8 @@ end
|
|
94
117
|
# @option settings [Numeric] :window_size The size of the rolling window for failure tracking.
|
95
118
|
# @option settings [Array<StandardError>] :tracked_errors A list of errors to track.
|
96
119
|
# @option settings [Array<Exception>] :skipped_errors A list of errors to skip.
|
120
|
+
# @option settings [Stoplight::TrafficControl::Base, Symbol, {Symbol, Hash{Symbol, any}}] :traffic_control The
|
121
|
+
# traffic control strategy to use.
|
97
122
|
#
|
98
123
|
# @return [Stoplight::Light] A new circuit breaker instance.
|
99
124
|
# @raise [ArgumentError] If an unknown option is provided in the settings.
|
@@ -117,7 +142,14 @@ end
|
|
117
142
|
# @example configure skipped errors
|
118
143
|
# light = Stoplight("Payment API", skipped_errors: [ActiveRecord::RecordNotFound])
|
119
144
|
#
|
145
|
+
# @example configure traffic control to trip using consecutive failures method
|
146
|
+
# # When 5 consecutive failures occur, the circuit breaker will trip.
|
147
|
+
# light = Stoplight("Payment API", traffic_control: :consecutive_errors, threshold: 5)
|
148
|
+
#
|
149
|
+
# @example configure traffic control to trip using error rate method
|
150
|
+
# # When 66.6% error rate reached withing a sliding 5 minute window, the circuit breaker will trip.
|
151
|
+
# light = Stoplight("Payment API", traffic_control: :error_rate, threshold: 0.666, window_size: 300)
|
152
|
+
#
|
120
153
|
def Stoplight(name, **settings) # rubocop:disable Naming/MethodName
|
121
|
-
|
122
|
-
Stoplight::Light.new(config)
|
154
|
+
Stoplight.light(name, **settings)
|
123
155
|
end
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stoplight
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0
|
4
|
+
version: 5.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cameron Desautels
|
8
8
|
- Taylor Fausak
|
9
9
|
- Justin Steffy
|
10
|
+
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2025-
|
13
|
+
date: 2025-07-11 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: zeitwerk
|
@@ -37,6 +38,9 @@ files:
|
|
37
38
|
- CHANGELOG.md
|
38
39
|
- LICENSE.md
|
39
40
|
- README.md
|
41
|
+
- lib/generators/stoplight/install/USAGE
|
42
|
+
- lib/generators/stoplight/install/install_generator.rb
|
43
|
+
- lib/generators/stoplight/install/templates/stoplight.rb.erb
|
40
44
|
- lib/stoplight.rb
|
41
45
|
- lib/stoplight/admin.rb
|
42
46
|
- lib/stoplight/admin/actions/action.rb
|
@@ -55,8 +59,10 @@ files:
|
|
55
59
|
- lib/stoplight/admin/views/index.erb
|
56
60
|
- lib/stoplight/admin/views/layout.erb
|
57
61
|
- lib/stoplight/color.rb
|
58
|
-
- lib/stoplight/config/
|
62
|
+
- lib/stoplight/config/compatibility_result.rb
|
63
|
+
- lib/stoplight/config/dsl.rb
|
59
64
|
- lib/stoplight/config/library_default_config.rb
|
65
|
+
- lib/stoplight/config/system_config.rb
|
60
66
|
- lib/stoplight/config/user_default_config.rb
|
61
67
|
- lib/stoplight/data_store.rb
|
62
68
|
- lib/stoplight/data_store/base.rb
|
@@ -90,14 +96,16 @@ files:
|
|
90
96
|
- lib/stoplight/rspec/generic_notifier.rb
|
91
97
|
- lib/stoplight/state.rb
|
92
98
|
- lib/stoplight/traffic_control/base.rb
|
93
|
-
- lib/stoplight/traffic_control/
|
99
|
+
- lib/stoplight/traffic_control/consecutive_errors.rb
|
100
|
+
- lib/stoplight/traffic_control/error_rate.rb
|
94
101
|
- lib/stoplight/traffic_recovery/base.rb
|
95
|
-
- lib/stoplight/traffic_recovery/
|
102
|
+
- lib/stoplight/traffic_recovery/consecutive_successes.rb
|
96
103
|
- lib/stoplight/version.rb
|
97
104
|
homepage: https://github.com/bolshakov/stoplight
|
98
105
|
licenses:
|
99
106
|
- MIT
|
100
107
|
metadata: {}
|
108
|
+
post_install_message:
|
101
109
|
rdoc_options: []
|
102
110
|
require_paths:
|
103
111
|
- lib
|
@@ -112,7 +120,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
120
|
- !ruby/object:Gem::Version
|
113
121
|
version: '0'
|
114
122
|
requirements: []
|
115
|
-
rubygems_version: 3.
|
123
|
+
rubygems_version: 3.4.19
|
124
|
+
signing_key:
|
116
125
|
specification_version: 4
|
117
126
|
summary: Traffic control for code.
|
118
127
|
test_files: []
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Stoplight
|
4
|
-
module Config
|
5
|
-
# Provides configuration for a Stoplight light by its name.
|
6
|
-
#
|
7
|
-
# It combines settings from three sources in the following order of precedence:
|
8
|
-
# 1. **Settings Overrides**: Explicit settings passed as arguments to +#provide+ method.
|
9
|
-
# 2. **User-level Default Settings**: Settings defined using the +Stoplight.configure+ method.
|
10
|
-
# 4. **Library-Level Default Settings**: Default settings defined in the +Stoplight::Config::UserDefaultConfig+ module.
|
11
|
-
#
|
12
|
-
# The settings are merged in this order, with higher-precedence settings overriding lower-precedence ones.
|
13
|
-
#
|
14
|
-
# @api private
|
15
|
-
class ConfigProvider
|
16
|
-
# @!attribute [r] default_settings
|
17
|
-
# @return [Hash]
|
18
|
-
private attr_reader :default_settings
|
19
|
-
|
20
|
-
# @param user_default_config [Stoplight::Config::UserDefaultConfig]
|
21
|
-
# @param library_default_config [Stoplight::Config::LibraryDefaultConfig]
|
22
|
-
# @raise [Error::ConfigurationError] if both user_default_config and legacy_config are not empty
|
23
|
-
def initialize(user_default_config:, library_default_config:)
|
24
|
-
@default_settings = library_default_config.to_h.merge(
|
25
|
-
user_default_config.to_h
|
26
|
-
)
|
27
|
-
end
|
28
|
-
|
29
|
-
# @return [Stoplight::DataStore::Base]
|
30
|
-
def data_store
|
31
|
-
default_settings.fetch(:data_store)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Returns a configuration for a specific light with the given name and settings overrides.
|
35
|
-
#
|
36
|
-
# @param light_name [Symbol, String] The name of the light.
|
37
|
-
# @param settings_overrides [Hash] The settings to override.
|
38
|
-
# @see +Stoplight()+
|
39
|
-
# @return [Stoplight::Light::Config] The configuration for the specified light.
|
40
|
-
# @raise [Error::ConfigurationError]
|
41
|
-
def provide(light_name, **settings_overrides)
|
42
|
-
raise Error::ConfigurationError, <<~ERROR if settings_overrides.has_key?(:name)
|
43
|
-
The +name+ setting cannot be overridden in the configuration.
|
44
|
-
ERROR
|
45
|
-
|
46
|
-
settings = default_settings.merge(settings_overrides, {name: light_name})
|
47
|
-
Light::Config.new(**settings)
|
48
|
-
end
|
49
|
-
|
50
|
-
def inspect
|
51
|
-
"#<#{self.class.name} " \
|
52
|
-
"cool_off_time=#{default_settings[:cool_off_time]}, " \
|
53
|
-
"threshold=#{default_settings[:threshold]}, " \
|
54
|
-
"window_size=#{default_settings[:window_size]}, " \
|
55
|
-
"tracked_errors=#{default_settings[:tracked_errors].join(",")}, " \
|
56
|
-
"skipped_errors=#{default_settings[:skipped_errors].join(",")}, " \
|
57
|
-
"data_store=#{default_settings[:data_store].class.name}" \
|
58
|
-
">"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Stoplight
|
4
|
-
module TrafficRecovery
|
5
|
-
# A basic strategy that recovers traffic flow after a successful recovery probe.
|
6
|
-
#
|
7
|
-
# This strategy allows traffic to resume when a single successful probe
|
8
|
-
# occurs after the Stoplight has been in red state. It's a simple "one success and we're back"
|
9
|
-
# approach.
|
10
|
-
#
|
11
|
-
# @example Basic usage
|
12
|
-
# config = Stoplight::Light::Config.new(cool_off_time: 60)
|
13
|
-
# strategy = Stoplight::TrafficRecovery::SingleSuccess.new
|
14
|
-
#
|
15
|
-
# After the Stoplight turns red:
|
16
|
-
# - The Stoplight will wait for the cool-off period (60 seconds)
|
17
|
-
# - Then enter the recovery phase (YELLOW color)
|
18
|
-
# - The first successful probe will resume normal traffic flow (green color)
|
19
|
-
# @api private
|
20
|
-
class SingleSuccess < Base
|
21
|
-
# @param config [Stoplight::Light::Config]
|
22
|
-
# @param metadata [Stoplight::Metadata]
|
23
|
-
# @return [String]
|
24
|
-
def determine_color(config, metadata)
|
25
|
-
recovery_started_at = metadata.recovery_started_at || metadata.recovery_scheduled_after
|
26
|
-
last_success_at = metadata.last_success_at
|
27
|
-
if last_success_at && recovery_started_at <= last_success_at
|
28
|
-
Color::GREEN
|
29
|
-
else
|
30
|
-
Color::RED
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|