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.
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::ProgrammaticConfig] The configuration object.
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: #<Stoplight::Config::ConfigProvider cool_off_time=32, threshold=3, window_size=94, tracked_errors=StandardError, skipped_errors=NoMemoryError,ScriptError,SecurityError,SignalException,SystemExit,SystemStackError, data_store=Stoplight::DataStore::Memory>\n"
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 = !@config_provider.nil?
62
+ reconfigured = !@default_config.nil?
59
63
 
60
- @config_provider = Config::ConfigProvider.new(
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: #{@config_provider.inspect}"
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::ConfigProvider]
98
+ # @return [Stoplight::Light::Config]
76
99
  # @api private
77
- def config_provider
100
+ def default_config
78
101
  CONFIG_MUTEX.synchronize do
79
- @config_provider ||= configure
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
- config = Stoplight.config_provider.provide(name, **settings)
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.3
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-06-26 00:00:00.000000000 Z
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/config_provider.rb
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/consecutive_failures.rb
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/single_success.rb
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.6.5
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