stoplight 5.4.0 → 5.5.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/stoplight/admin/views/layout.erb +3 -3
  4. data/lib/stoplight/admin.rb +4 -4
  5. data/lib/stoplight/domain/color.rb +11 -0
  6. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  7. data/lib/stoplight/domain/config.rb +55 -0
  8. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  9. data/lib/stoplight/domain/error.rb +42 -0
  10. data/lib/stoplight/domain/failure.rb +42 -0
  11. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  12. data/lib/stoplight/domain/light.rb +198 -0
  13. data/lib/stoplight/domain/light_factory.rb +75 -0
  14. data/lib/stoplight/domain/metadata.rb +65 -0
  15. data/lib/stoplight/domain/state.rb +11 -0
  16. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  17. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  18. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  19. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  20. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  21. data/lib/stoplight/domain/tracker/base.rb +41 -0
  22. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  23. data/lib/stoplight/domain/tracker/request.rb +67 -0
  24. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  25. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  26. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  27. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  28. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  29. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  30. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  31. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  32. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  33. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  34. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  36. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  37. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  38. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  39. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  40. data/lib/stoplight/version.rb +1 -1
  41. data/lib/stoplight/wiring/container.rb +80 -0
  42. data/lib/stoplight/wiring/default.rb +28 -0
  43. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  44. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  45. data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +22 -11
  46. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  47. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  48. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  49. data/lib/stoplight/wiring/light_factory.rb +188 -0
  50. data/lib/stoplight/wiring/public_api.rb +28 -0
  51. data/lib/stoplight/wiring/system_container.rb +9 -0
  52. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  53. data/lib/stoplight.rb +38 -28
  54. metadata +53 -43
  55. data/lib/stoplight/color.rb +0 -9
  56. data/lib/stoplight/config/dsl.rb +0 -97
  57. data/lib/stoplight/config/library_default_config.rb +0 -21
  58. data/lib/stoplight/config/system_config.rb +0 -10
  59. data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
  60. data/lib/stoplight/data_store/memory.rb +0 -285
  61. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  62. data/lib/stoplight/data_store/redis.rb +0 -446
  63. data/lib/stoplight/data_store.rb +0 -6
  64. data/lib/stoplight/default.rb +0 -30
  65. data/lib/stoplight/error.rb +0 -39
  66. data/lib/stoplight/failure.rb +0 -71
  67. data/lib/stoplight/light/config.rb +0 -112
  68. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  69. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  70. data/lib/stoplight/light/red_run_strategy.rb +0 -31
  71. data/lib/stoplight/light/run_strategy.rb +0 -32
  72. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  73. data/lib/stoplight/light.rb +0 -191
  74. data/lib/stoplight/metadata.rb +0 -99
  75. data/lib/stoplight/notifier/generic.rb +0 -79
  76. data/lib/stoplight/notifier/io.rb +0 -21
  77. data/lib/stoplight/notifier/logger.rb +0 -19
  78. data/lib/stoplight/state.rb +0 -9
  79. data/lib/stoplight/traffic_control/base.rb +0 -70
  80. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  81. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  82. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  83. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  84. data/lib/stoplight/traffic_recovery.rb +0 -11
  85. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  90. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module Notifier
5
- # The Generic module provides a reusable implementation for notifiers in Stoplight.
6
- # It includes a formatter for generating notification messages and defines the `notify` method.
7
- #
8
- # @example Custom Notifier Implementation and Usage
9
- # # Custom notifier that writes notifications to a file
10
- # class FileNotifier < Stoplight::Notifier::Base
11
- # include Stoplight::Notifier::Generic
12
- #
13
- # def initialize(file_path)
14
- # @file = File.open(file_path, 'a')
15
- # super(@file)
16
- # end
17
- #
18
- # private
19
- #
20
- # # Writes the notification message to the file
21
- # def put(message)
22
- # @file.puts(message)
23
- # end
24
- # end
25
- #
26
- # # Usage example
27
- # # Create a custom notifier that writes to 'stoplight.log'
28
- # notifier = FileNotifier.new('stoplight.log')
29
- #
30
- # # Configure Stoplight to use the custom notifier
31
- # Stoplight.configure do |config|
32
- # config.notifiers += [notifier]
33
- # end
34
- #
35
- # # Create a stoplight and trigger a state change
36
- # light = Stoplight('example-light')
37
- # light.run { raise 'Simulated failure' } rescue nil
38
- # light.run { raise 'Simulated failure' } rescue nil
39
- # light.run { raise 'Simulated failure' } rescue nil
40
- #
41
- module Generic # rubocop:disable Style/Documentation
42
- # @!attribute [r] formatter
43
- # @return [Proc] The formatter used to generate notification messages.
44
- # @see Stoplight::Default::FORMATTER
45
- attr_reader :formatter
46
-
47
- # @param object [Object] The object used by the notifier (e.g., a logger or external service).
48
- # @param formatter [Proc, nil] A custom formatter for generating notification messages.
49
- # If no formatter is provided, the default formatter is used.
50
- def initialize(object, formatter = nil)
51
- @object = object
52
- @formatter = formatter || Default::FORMATTER
53
- end
54
-
55
- # Sends a notification when a Stoplight changes state.
56
- #
57
- # @param light [Light] The Stoplight instance triggering the notification.
58
- # @param from_color [String] The previous state color of the Stoplight.
59
- # @param to_color [String] The new state color of the Stoplight.
60
- # @param error [Exception, nil] The error (if any) that caused the state change.
61
- # @return [String] The formatted notification message.
62
- def notify(light, from_color, to_color, error)
63
- message = formatter.call(light, from_color, to_color, error)
64
- put(message)
65
- message
66
- end
67
-
68
- private
69
-
70
- # Processes the notification message.
71
- #
72
- # @param message [String] The notification message to be processed.
73
- # @raise [NotImplementedError] If the method is not implemented in a subclass.
74
- def put(message)
75
- raise NotImplementedError
76
- end
77
- end
78
- end
79
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module Notifier
5
- # @see Base
6
- class IO < Base
7
- include Generic
8
-
9
- # @return [::IO]
10
- def io
11
- @object
12
- end
13
-
14
- private
15
-
16
- def put(message)
17
- io.puts(message)
18
- end
19
- end
20
- end
21
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module Notifier
5
- # @see Base
6
- class Logger < Base
7
- include Generic
8
-
9
- # @return [::Logger]
10
- def logger
11
- @object
12
- end
13
-
14
- def put(message)
15
- logger.warn(message)
16
- end
17
- end
18
- end
19
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module State
5
- UNLOCKED = "unlocked"
6
- LOCKED_GREEN = "locked_green"
7
- LOCKED_RED = "locked_red"
8
- end
9
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficControl
5
- # Strategies for determining when a Stoplight should change color to red.
6
- #
7
- # These strategies evaluate the current state and metrics of a Stoplight to decide
8
- # if traffic should be stopped (i.e., if the light should turn RED).
9
- #
10
- # @example Creating a custom strategy
11
- # class ErrorRateStrategy < Stoplight::TrafficControl::Base
12
- # def check_compatibility(config)
13
- # if config.window_size.nil?
14
- # incompatible("`window_size` should be set")
15
- # else
16
- # compatible
17
- # end
18
- # end
19
- #
20
- # def stop_traffic?(config, metadata)
21
- # total = metadata.successes + metadata.failures
22
- # return false if total < 10 # Minimum sample size
23
- #
24
- # error_rate = metadata.failures.fdiv(total)
25
- # error_rate >= 0.5 # Stop traffic when error rate reaches 50%
26
- # end
27
- # end
28
- #
29
- # @abstract
30
- # @api private
31
- class Base
32
- # Checks if the strategy is compatible with the given Stoplight configuration.
33
- #
34
- # @param config [Stoplight::Light::Config]
35
- # @return [Stoplight::Config::CompatibilityResult]
36
- # :nocov:
37
- def check_compatibility(config)
38
- raise NotImplementedError
39
- end
40
- # :nocov:
41
-
42
- # Determines whether traffic should be stopped based on the Stoplight's
43
- # current state and metrics.
44
- #
45
- # @param config [Stoplight::Light::Config]
46
- # @param metadata [Stoplight::Metadata]
47
- # @return [Boolean] true if traffic should be stopped (rec), false otherwise (green)
48
- def stop_traffic?(config, metadata)
49
- raise NotImplementedError
50
- end
51
-
52
- # @param other [any]
53
- # @return [Boolean]
54
- def ==(other)
55
- other.is_a?(self.class)
56
- end
57
-
58
- # Returns a compatibility result indicating the strategy is compatible.
59
- #
60
- # @return [Stoplight::Config::CompatibilityResult] A compatible result.
61
- private def compatible = Config::CompatibilityResult.compatible
62
-
63
- # Returns a compatibility result indicating the strategy is incompatible.
64
- #
65
- # @param errors [Array<String>] The list of error messages describing incompatibility.
66
- # @return [Stoplight::Config::CompatibilityResult] An incompatible result.
67
- private def incompatible(*errors) = Config::CompatibilityResult.incompatible(*errors)
68
- end
69
- end
70
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficControl
5
- # A strategy that stops the traffic based on consecutive failures number.
6
- #
7
- # This strategy implements two distinct behaviors based on whether a window size
8
- # is configured:
9
- #
10
- # 1. When window_size is set: The Stoplight turns red when the total number of
11
- # failures within the window reaches the threshold.
12
- #
13
- # 2. When window_size is not set: The Stoplight turns red when consecutive failures
14
- # reach the threshold.
15
- #
16
- # @example With window-based configuration
17
- # traffic_control = Stoplight::TrafficControl::ConsecutiveErrors.new
18
- # config = Stoplight::Light::Config.new(threshold: 5, window_size: 60, traffic_control:)
19
- #
20
- # Will switch to red if 5 consecutive failures occur within the 60-second window
21
- #
22
- # @example With total number of consecutive failures configuration
23
- # traffic_control = Stoplight::TrafficControl::ConsecutiveErrors.new
24
- # config = Stoplight::Light::Config.new(threshold: 5, window_size: nil, traffic_control:)
25
- #
26
- # Will switch to red only if 5 consecutive failures occur regardless of the time window
27
- # @api private
28
- class ConsecutiveErrors < Base
29
- # @param config [Stoplight::Light::Config]
30
- # @return [Stoplight::Config::CompatibilityResult]
31
- def check_compatibility(config)
32
- if config.threshold <= 0
33
- incompatible("`threshold` should be bigger than 0")
34
- elsif !config.threshold.is_a?(Integer)
35
- incompatible("`threshold` should be an integer")
36
- else
37
- compatible
38
- end
39
- end
40
-
41
- # Determines if traffic should be stopped based on failure counts.
42
- #
43
- # @param config [Stoplight::Light::Config]
44
- # @param metadata [Stoplight::Metadata]
45
- # @return [Boolean] true if failures have reached the threshold, false otherwise
46
- def stop_traffic?(config, metadata)
47
- if config.window_size
48
- [metadata.consecutive_errors, metadata.errors].min >= config.threshold
49
- else
50
- metadata.consecutive_errors >= config.threshold
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficControl
5
- # A strategy that stops the traffic based on error rate.
6
- #
7
- # @example
8
- # traffic_control = Stoplight::TrafficControl::ErrorRate.new
9
- # config = Stoplight::Light::Config.new(threshold: 0.6, window_size: 300, traffic_control:)
10
- #
11
- # Will switch to red if 60% error rate reached within the 5-minute (300 seconds) sliding window.
12
- # By default this traffic control strategy starts evaluating only after 10 requests have been made. You can
13
- # adjust this by passing a different value for `min_requests` when initializing the strategy.
14
- #
15
- # traffic_control = Stoplight::TrafficControl::ErrorRate.new(min_requests: 100)
16
- #
17
- # @api private
18
- class ErrorRate < Base
19
- # @!attribute min_requests
20
- # @return [Integer]
21
- attr_reader :min_requests
22
-
23
- # @param min_requests [Integer] Minimum number of requests before traffic control is applied.
24
- # until this number of requests is reached, the error rate will not be considered.
25
- def initialize(min_requests: 10)
26
- @min_requests = min_requests
27
- end
28
-
29
- # @param config [Stoplight::Light::Config]
30
- # @return [Stoplight::Config::CompatibilityResult]
31
- def check_compatibility(config)
32
- if config.window_size.nil?
33
- incompatible("`window_size` should be set")
34
- elsif config.threshold < 0 || config.threshold > 1
35
- incompatible("`threshold` should be between 0 and 1")
36
- else
37
- compatible
38
- end
39
- end
40
-
41
- # @param config [Stoplight::Light::Config]
42
- # @param metadata [Stoplight::Metadata]
43
- # @return [Boolean]
44
- def stop_traffic?(config, metadata)
45
- metadata.requests >= min_requests && metadata.error_rate >= config.threshold
46
- end
47
- end
48
- end
49
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficRecovery
5
- # Strategies for determining how to recover traffic flow through the Stoplight.
6
- # These strategies evaluate recovery metrics to decide which color the Stoplight should
7
- # transition to during the recovery process.
8
- #
9
- # @example Creating a custom traffic recovery strategy
10
- # class GradualRecovery < Stoplight::TrafficRecovery::Base
11
- # def initialize(min_success_rate: 0.8, min_samples: 100)
12
- # @min_success_rate = min_success_rate
13
- # @min_samples = min_samples
14
- # end
15
- #
16
- # def determine_color(config, metadata)
17
- # total_probes = metadata.recovery_probe_successes + metadata.recovery_probe_errors
18
- #
19
- # if total_probes < @min_samples
20
- # return Color::YELLOW # Keep recovering, not enough samples
21
- # end
22
- #
23
- # success_rate = metadata.recovery_probe_successes.fdiv(total_probes)
24
- # if success_rate >= @min_success_rate
25
- # Color::GREEN # Recovery successful
26
- # elsif success_rate <= 0.2
27
- # Color::RED # Recovery failed, too many errors
28
- # else
29
- # Color::YELLOW # Continue recovery
30
- # end
31
- # end
32
- # end
33
- #
34
- # @abstract
35
- # @api private
36
- class Base
37
- # Checks if the strategy is compatible with the given Stoplight configuration.
38
- #
39
- # @param config [Stoplight::Light::Config]
40
- # @return [Stoplight::Config::CompatibilityResult]
41
- # :nocov:
42
- def check_compatibility(config)
43
- raise NotImplementedError
44
- end
45
- # :nocov:
46
-
47
- # Determines the appropriate recovery state based on the Stoplight's
48
- # current metrics and recovery progress.
49
- #
50
- # @param config [Stoplight::Light::Config]
51
- # @param metadata [Stoplight::Metadata]
52
- # @return [TrafficRecovery::Decision]
53
- def determine_color(config, metadata)
54
- raise NotImplementedError
55
- end
56
-
57
- # @param other [any]
58
- # @return [Boolean]
59
- def ==(other)
60
- other.is_a?(self.class)
61
- end
62
-
63
- # Returns a compatibility result indicating the strategy is compatible.
64
- #
65
- # @return [Stoplight::Config::CompatibilityResult] A compatible result.
66
- private def compatible = Config::CompatibilityResult.compatible
67
-
68
- # Returns a compatibility result indicating the strategy is incompatible.
69
- #
70
- # @param errors [Array<String>] The list of error messages describing incompatibility.
71
- # @return [Stoplight::Config::CompatibilityResult] An incompatible result.
72
- private def incompatible(*errors) = Config::CompatibilityResult.incompatible(*errors)
73
- end
74
- end
75
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficRecovery
5
- # A conservative strategy that requires multiple consecutive successful probes
6
- # before resuming traffic flow.
7
- #
8
- # The strategy immediately returns to RED state if any failure occurs during
9
- # the recovery process, ensuring that only truly stable services resume
10
- # full traffic flow.
11
- #
12
- # @example Basic usage with 3 consecutive successes required
13
- # config = Stoplight::Light::Config.new(
14
- # cool_off_time: 60,
15
- # recovery_threshold: 3
16
- # )
17
- # strategy = Stoplight::TrafficRecovery::ConsecutiveSuccesses.new
18
- #
19
- # Recovery behavior:
20
- # - After cool-off period, Stoplight enters YELLOW (recovery) state
21
- # - Requires 3 consecutive successful probes to transition to GREEN
22
- # - Any failure during recovery immediately returns to RED state
23
- # - Process repeats after another cool-off period
24
- #
25
- # Configuration requirements:
26
- # - `recovery_threshold`: Integer > 0, specifies required consecutive successes
27
- #
28
- # Failure behavior:
29
- # Unlike some circuit breaker implementations that tolerate occasional failures
30
- # during recovery, this strategy takes a zero-tolerance approach: any failure
31
- # during the recovery phase immediately transitions back to RED state. This
32
- # conservative approach prioritizes stability over recovery speed.
33
- #
34
- # @api private
35
- class ConsecutiveSuccesses < Base
36
- # @param config [Stoplight::Light::Config]
37
- # @return [Stoplight::Config::CompatibilityResult]
38
- def check_compatibility(config)
39
- if config.recovery_threshold <= 0
40
- incompatible("`recovery_threshold` should be bigger than 0")
41
- elsif !config.recovery_threshold.is_a?(Integer)
42
- incompatible("`recovery_threshold` should be an integer")
43
- else
44
- compatible
45
- end
46
- end
47
-
48
- # Determines if traffic should be resumed based on successes counts.
49
- #
50
- # @param config [Stoplight::Light::Config]
51
- # @param metadata [Stoplight::Metadata]
52
- # @return [TrafficRecovery::Decision]
53
- def determine_color(config, metadata)
54
- return TrafficRecovery::PASS if metadata.color != Color::YELLOW
55
-
56
- recovery_started_at = metadata.recovery_started_at || metadata.recovery_scheduled_after
57
-
58
- if metadata.last_error_at && metadata.last_error_at >= recovery_started_at
59
- TrafficRecovery::RED
60
- elsif [metadata.consecutive_successes, metadata.recovery_probe_successes].min >= config.recovery_threshold
61
- TrafficRecovery::GREEN
62
- else
63
- TrafficRecovery::YELLOW
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module TrafficRecovery
5
- Decision = Data.define(:decision)
6
- GREEN = Decision.new("green")
7
- YELLOW = Decision.new("yellow")
8
- RED = Decision.new("red")
9
- PASS = Decision.new("pass")
10
- end
11
- end