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
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ # Abstract factory protocol for building +Stoplight::Light+ instances.
6
+ #
7
+ # This defines the interface that any Light factory must implement,
8
+ # regardless of the underlying implementation details.
9
+ #
10
+ # This uses the Abstract Factory pattern to decouple the domain (Light)
11
+ # from the application layer (concrete factory implementation). Light
12
+ # doesn't know HOW it's built, only that it needs something that can
13
+ # build variants of itself.
14
+ #
15
+ # By defining this interface in the domain layer:
16
+ # - Light can request reconfiguration without knowing about containers
17
+ # - Different factory implementations can be swapped (code, database-configured, etc.)
18
+ # - Dependency direction is preserved (Application → Domain, not Domain → Application)
19
+ #
20
+ # @abstract Subclasses must implement +#with+ and +#build+
21
+ # @api private
22
+ class LightFactory
23
+ # Creates a new factory with modified settings.
24
+ #
25
+ # This method must return a NEW factory instance - it should not
26
+ # modify the current factory. The new factory should inherit all
27
+ # configuration from the current factory, with the provided
28
+ # settings overriding specific values.
29
+ #
30
+ # @param settings [Hash] Configuration and dependency overrides
31
+ # @return [Stoplight::Domain::LightFactory] New factory with updated settings
32
+ # @raise [NotImplementedError] Must be implemented by subclass
33
+ # @abstract
34
+ # :nocov:
35
+ def with(**settings)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ # Builds a +Stoplight::Light+ instance with the current configuration.
40
+ #
41
+ # This method must construct a fully-wired Light with all required
42
+ # dependencies (strategies, data store, notifiers, etc.). The Light
43
+ # should be ready to use immediately after construction.
44
+ #
45
+ # @return [Stoplight::Light] Configured circuit breaker instance
46
+ # @raise [NotImplementedError] Must be implemented by subclass
47
+ # @raise [Stoplight::Error::ConfigurationError] If configuration is invalid
48
+ # @abstract
49
+ def build
50
+ raise NotImplementedError
51
+ end
52
+ # :nocov:
53
+
54
+ # Convenience method to configure and build in one operation.
55
+ #
56
+ # This combines +#with+ and +#build+ into a single call, which is
57
+ # useful when you want to create a customized Light without keeping
58
+ # a reference to the intermediate factory.
59
+ #
60
+ # @param settings [Hash] Settings to override before building
61
+ # @return [Stoplight::Light] Configured circuit breaker instance
62
+ #
63
+ # @example Usage
64
+ # # Instead of:
65
+ # new_factory = factory.with(threshold: 10)
66
+ # light = new_factory.build
67
+ #
68
+ # # You can do:
69
+ # light = factory.build_with(threshold: 10)
70
+ def build_with(**settings)
71
+ with(**settings).build
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ # @api private
6
+ Metadata = Data.define(
7
+ :successes,
8
+ :errors,
9
+ :recovery_probe_successes,
10
+ :recovery_probe_errors,
11
+ :last_error_at,
12
+ :last_success_at,
13
+ :consecutive_errors,
14
+ :consecutive_successes,
15
+ :last_error,
16
+ :breached_at,
17
+ :locked_state,
18
+ :recovery_scheduled_after,
19
+ :recovery_started_at,
20
+ :recovered_at,
21
+ :current_time
22
+ ) do
23
+ # YELLOW color could be entered implicitly through a timeout
24
+ # and explicitly through a transition.
25
+ #
26
+ # This method indicates whether the recovery has already started explicitly
27
+ #
28
+ # @return [Boolean]
29
+ def recovery_started?
30
+ recovery_started_at && recovery_started_at <= current_time
31
+ end
32
+
33
+ # @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
34
+ def color
35
+ if locked_state == State::LOCKED_GREEN
36
+ Color::GREEN
37
+ elsif locked_state == State::LOCKED_RED
38
+ Color::RED
39
+ elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
40
+ Color::YELLOW
41
+ elsif breached_at
42
+ Color::RED
43
+ else
44
+ Color::GREEN
45
+ end
46
+ end
47
+
48
+ # Calculates the error rate based on the number of successes and errors.
49
+ #
50
+ # @return [Float]
51
+ def error_rate
52
+ if (successes + errors).zero?
53
+ 0.0
54
+ else
55
+ errors.fdiv(successes + errors)
56
+ end
57
+ end
58
+
59
+ # @return [Integer]
60
+ def requests
61
+ successes + errors
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module State
6
+ UNLOCKED = "unlocked"
7
+ LOCKED_GREEN = "locked_green"
8
+ LOCKED_RED = "locked_red"
9
+ end
10
+ end
11
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- module Notifier
4
+ module Domain
5
5
  # Base class for creating custom notifiers in Stoplight.
6
6
  # This is an abstract class that defines the interface for notifiers.
7
7
  #
8
8
  # @abstract Subclasses must implement the `notify` method to define custom notification logic.
9
- # @see +Stoplight::Notifier::Generic+
10
- class Base
9
+ # :nocov:
10
+ class StateTransitionNotifier # ColorTransition?????
11
11
  # Sends a notification when a Stoplight changes state.
12
12
  #
13
- # @param config [Stoplight::Light::Config] The Stoplight instance triggering the notification.
13
+ # @param config [Stoplight::Domain::Config] The Stoplight instance triggering the notification.
14
14
  # @param from_color [String] The previous state color of the Stoplight.
15
15
  # @param to_color [String] The new state color of the Stoplight.
16
16
  # @param error [Exception, nil] The error (if any) that caused the state change.
@@ -20,5 +20,6 @@ module Stoplight
20
20
  raise NotImplementedError
21
21
  end
22
22
  end
23
+ # :nocov:
23
24
  end
24
25
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is green.
7
+ #
8
+ # This strategy clears failures after successful execution and handles errors
9
+ # by either raising them or invoking a fallback if provided.
10
+ #
11
+ # @api private
12
+ class GreenRunStrategy < RunStrategy
13
+ # @!attribute [r] request_tracker
14
+ # @return [Stoplight::Domain::Tracker::Request]
15
+ protected attr_reader :request_tracker
16
+
17
+ # @!attribute [r] config
18
+ # @return [Stoplight::Domain::Config] The configuration for the light.
19
+ protected attr_reader :config
20
+
21
+ # @param config [Stoplight::Domain::Config]
22
+ # @param request_tracker [Stoplight::Domain::Tracker::Request
23
+ def initialize(config:, request_tracker:)
24
+ @config = config
25
+ @request_tracker = request_tracker
26
+ end
27
+
28
+ # Executes the provided code block when the light is in the green state.
29
+ #
30
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
31
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
32
+ # @yield The code block to execute.
33
+ # @return [Object] The result of the code block if successful.
34
+ # @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
35
+ def execute(fallback, metadata:, &code)
36
+ # TODO: Consider implementing sampling rate to limit the memory footprint
37
+ code.call.tap { record_success }
38
+ rescue => error
39
+ if config.track_error?(error)
40
+ record_error(error)
41
+
42
+ if fallback
43
+ fallback.call(error)
44
+ else
45
+ raise
46
+ end
47
+ else
48
+ # User chose to not track the error, so we record it as a success
49
+ record_success
50
+ raise
51
+ end
52
+ end
53
+
54
+ private def record_error(error)
55
+ request_tracker.record_failure(error)
56
+ end
57
+
58
+ private def record_success
59
+ request_tracker.record_success
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def ==(other)
64
+ super && config == other.config && request_tracker == other.request_tracker
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is red.
7
+ #
8
+ # This strategy prevents execution of the code block and either raises an error
9
+ # or invokes a fallback if provided.
10
+ #
11
+ # @api private
12
+ class RedRunStrategy < RunStrategy
13
+ # @!attribute [r] config
14
+ # @return [Stoplight::Domain::Config] The configuration for the light.
15
+ protected attr_reader :config
16
+
17
+ def initialize(config:)
18
+ @config = config
19
+ end
20
+
21
+ # Executes the fallback proc when the light is in the red state.
22
+ #
23
+ # @param fallback [Proc, nil] A fallback proc to execute instead of the code block.
24
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
25
+ # @return [Object, nil] The result of the fallback proc if provided.
26
+ # @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
27
+ def execute(fallback, metadata:)
28
+ if fallback
29
+ fallback.call(nil)
30
+ else
31
+ raise Error::RedLight.new(
32
+ config.name,
33
+ cool_off_time: config.cool_off_time,
34
+ retry_after: metadata.recovery_scheduled_after
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Represents an abstract strategy for running a light's operations.
7
+ # Every new strategy should be a child of this class.
8
+ #
9
+ # @api private
10
+ # @abstract
11
+ class RunStrategy
12
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
13
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
14
+ # :nocov:
15
+ def execute(fallback, metadata:, &code)
16
+ raise NotImplementedError, "Subclasses must implement the execute method"
17
+ end
18
+ # :nocov:
19
+
20
+ # @return [Boolean]
21
+ def ==(other)
22
+ other.is_a?(self.class)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Strategies
6
+ # Defines how the light executes when it is yellow.
7
+ #
8
+ # This strategy clears failures after successful execution and notifies
9
+ # about color switch from Red to Green. It also handles errors by either
10
+ # raising them or invoking a fallback if provided.
11
+ #
12
+ # @api private
13
+ class YellowRunStrategy < RunStrategy
14
+ # @!attribute [r] config
15
+ # @return [Stoplight::Domain::Config] The configuration for the light.
16
+ protected attr_reader :config
17
+
18
+ # @!attribute [r] data_store
19
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
20
+ protected attr_reader :data_store
21
+
22
+ # @!attribute [r] notifiers
23
+ # @return [Stoplight::Domain::StateTransitionNotifier]
24
+ protected attr_reader :notifiers
25
+
26
+ # @!attribute [r] request_tracker
27
+ # @return [Stoplight::Domain::RecoveryProbeRequestRecorder]
28
+ protected attr_reader :request_tracker
29
+
30
+ # @param config [Stoplight::Domain::Config]
31
+ # @param data_store [Stoplight::DataStore::Base]
32
+ # @param notifiers [Array<Stoplight::Domain::StateTransitionNotifier>]
33
+ # @param request_tracker [Stoplight::Domain::Tracker::RecoveryProbe]
34
+ def initialize(config:, data_store:, notifiers:, request_tracker:)
35
+ @config = config
36
+ @data_store = data_store
37
+ @notifiers = notifiers
38
+ @request_tracker = request_tracker
39
+ end
40
+
41
+ # Executes the provided code block when the light is in the yellow state.
42
+ #
43
+ # @param fallback [Proc, nil] A fallback proc to execute in case of an error.
44
+ # @param metadata [Stoplight::Domain::Metadata] Metadata capturing the current state of the light.
45
+ # @yield The code block to execute.
46
+ # @return [Object] The result of the code block if successful.
47
+ # @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
48
+ def execute(fallback, metadata:, &code)
49
+ enter_recovery(metadata)
50
+ # TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
51
+ code.call.tap { record_recovery_probe_success }
52
+ rescue => error
53
+ if config.track_error?(error)
54
+ record_recovery_probe_failure(error)
55
+
56
+ if fallback
57
+ fallback.call(error)
58
+ else
59
+ raise
60
+ end
61
+ else
62
+ record_recovery_probe_success
63
+ raise
64
+ end
65
+ end
66
+
67
+ private def record_recovery_probe_success
68
+ request_tracker.record_success
69
+ end
70
+
71
+ private def record_recovery_probe_failure(error)
72
+ request_tracker.record_failure(error)
73
+ end
74
+
75
+ # @param metadata [Stoplight::Domain::Metadata]
76
+ # @return [void]
77
+ private def enter_recovery(metadata)
78
+ return if metadata.recovery_started?
79
+
80
+ if data_store.transition_to_color(config, Color::YELLOW)
81
+ notifiers.each do |notifier|
82
+ notifier.notify(config, Color::RED, Color::YELLOW, nil)
83
+ end
84
+ end
85
+ end
86
+
87
+ # @return [Boolean]
88
+ def ==(other)
89
+ super &&
90
+ config == other.config &&
91
+ notifiers == other.notifiers &&
92
+ data_store == other.data_store &&
93
+ request_tracker == other.request_tracker
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Tracker
6
+ # @api private
7
+ # @abstract
8
+ class Base
9
+ # @!attribute [r] data_store
10
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
11
+ protected attr_reader :data_store
12
+
13
+ # @!attribute [r] traffic_control
14
+ # @return [Stoplight::Domain::TrafficControl::Base]
15
+ protected attr_reader :notifiers
16
+
17
+ # @!attribute [r] config
18
+ # @return [Stoplight::Domain::Config] The configuration for the light.
19
+ protected attr_reader :config
20
+
21
+ def ==(other)
22
+ other.is_a?(self.class) &&
23
+ config == other.config &&
24
+ data_store == other.data_store &&
25
+ notifiers == other.notifiers
26
+ end
27
+
28
+ def transition_and_notify(from_color, to_color, error = nil)
29
+ if data_store.transition_to_color(config, to_color)
30
+ notifiers.each do |notifier|
31
+ notifier.notify(config, from_color, to_color, error)
32
+ end
33
+ true
34
+ else
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Tracker
6
+ class RecoveryProbe < Base
7
+ # @!attribute [r] data_store
8
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
9
+ protected attr_reader :data_store
10
+
11
+ # @!attribute [r] traffic_recovery
12
+ # @return [Stoplight::Domain::TrafficRecovery::Base]
13
+ protected attr_reader :traffic_recovery
14
+
15
+ # @!attribute [r] traffic_control
16
+ # @return [Stoplight::Domain::TrafficControl::Base]
17
+ protected attr_reader :notifiers
18
+
19
+ # @!attribute [r] config
20
+ # @return [Stoplight::Domain::Config] The configuration for the light.
21
+ protected attr_reader :config
22
+
23
+ # @param data_store [Stoplight::Domain::DataStore]
24
+ # @param traffic_recovery [Stoplight::Domain::TrafficRecovery::Base]
25
+ # @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
26
+ # @param config [Stoplight::Domain::Config]
27
+ def initialize(data_store:, traffic_recovery:, notifiers:, config:)
28
+ @data_store = data_store
29
+ @traffic_recovery = traffic_recovery
30
+ @notifiers = notifiers
31
+ @config = config
32
+ end
33
+
34
+ # @param exception [Exception]
35
+ def record_failure(exception)
36
+ metadata = data_store.record_recovery_probe_failure(config, exception)
37
+
38
+ recover(metadata)
39
+ end
40
+
41
+ def record_success
42
+ metadata = data_store.record_recovery_probe_success(config)
43
+
44
+ recover(metadata)
45
+ end
46
+ RECOVERY_TRANSITIONS = {
47
+ TrafficRecovery::GREEN => [Color::YELLOW, Color::GREEN],
48
+ TrafficRecovery::YELLOW => [Color::RED, Color::YELLOW],
49
+ TrafficRecovery::RED => [Color::YELLOW, Color::RED]
50
+ }.freeze
51
+
52
+ private def recover(metadata)
53
+ recovery_result = traffic_recovery.determine_color(config, metadata)
54
+
55
+ return if recovery_result == TrafficRecovery::PASS
56
+
57
+ from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
58
+ raise "recovery strategy returned unexpected color: #{recovery_result}"
59
+ end
60
+
61
+ transition_and_notify(from_color, to_color, nil)
62
+ end
63
+
64
+ # @param other [any]
65
+ # @return [bool]
66
+ def ==(other)
67
+ super && traffic_recovery == other.traffic_recovery
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Tracker
6
+ # Tracks request outcomes (success/failure) and manages state transitions
7
+ # for normal traffic.
8
+ #
9
+ # Used by +GreenRunStrategy+ to track failures and potentially open the circuit.
10
+ #
11
+ # @api private
12
+ class Request < Base
13
+ # @!attribute [r] data_store
14
+ # @return [Stoplight::DataStore::Base] The data store associated with the light.
15
+ protected attr_reader :data_store
16
+
17
+ # @!attribute [r] traffic_control
18
+ # @return [Stoplight::Domain::TrafficControl::Base]
19
+ protected attr_reader :traffic_control
20
+
21
+ # @!attribute [r] traffic_control
22
+ # @return [Stoplight::Domain::TrafficControl::Base]
23
+ protected attr_reader :notifiers
24
+
25
+ # @!attribute [r] config
26
+ # @return [Stoplight::Domain::Config] The configuration for the light.
27
+ protected attr_reader :config
28
+
29
+ # @param data_store [Stoplight::Domain::DataStore]
30
+ # @param traffic_control [Stoplight::Domain::TrafficControl::Base]
31
+ # @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
32
+ # @param config [Stoplight::Domain::Config]
33
+ def initialize(data_store:, traffic_control:, notifiers:, config:)
34
+ @data_store = data_store
35
+ @traffic_control = traffic_control
36
+ @notifiers = notifiers
37
+ @config = config
38
+ end
39
+
40
+ # @param exception [Exception]
41
+ # @return [void]
42
+ def record_failure(exception)
43
+ metadata = data_store.record_failure(config, exception)
44
+
45
+ transition_to_red(exception, metadata:)
46
+ end
47
+
48
+ # @return [void]
49
+ def record_success
50
+ data_store.record_success(config)
51
+ end
52
+
53
+ private def transition_to_red(exception, metadata:)
54
+ if traffic_control.stop_traffic?(config, metadata)
55
+ transition_and_notify(Color::GREEN, Color::RED, exception)
56
+ end
57
+ end
58
+
59
+ # @param other [any]
60
+ # @return [bool]
61
+ def ==(other)
62
+ super && traffic_control == other.traffic_control
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module TrafficControl
6
+ # Strategies for determining when a Stoplight should change color to red.
7
+ #
8
+ # These strategies evaluate the current state and metrics of a Stoplight to decide
9
+ # if traffic should be stopped (i.e., if the light should turn RED).
10
+ #
11
+ # @example Creating a custom strategy
12
+ # class ErrorRateStrategy < Stoplight::Domain::TrafficControl::Base
13
+ # def check_compatibility(config)
14
+ # if config.window_size.nil?
15
+ # incompatible("`window_size` should be set")
16
+ # else
17
+ # compatible
18
+ # end
19
+ # end
20
+ #
21
+ # def stop_traffic?(config, metadata)
22
+ # total = metadata.successes + metadata.failures
23
+ # return false if total < 10 # Minimum sample size
24
+ #
25
+ # error_rate = metadata.failures.fdiv(total)
26
+ # error_rate >= 0.5 # Stop traffic when error rate reaches 50%
27
+ # end
28
+ # end
29
+ #
30
+ # @abstract
31
+ # @api private
32
+ class Base
33
+ # Checks if the strategy is compatible with the given Stoplight configuration.
34
+ #
35
+ # @param config [Stoplight::Domain::Config]
36
+ # @return [Stoplight::Domain::CompatibilityResult]
37
+ # :nocov:
38
+ def check_compatibility(config)
39
+ raise NotImplementedError
40
+ end
41
+ # :nocov:
42
+
43
+ # Determines whether traffic should be stopped based on the Stoplight's
44
+ # current state and metrics.
45
+ #
46
+ # @param config [Stoplight::Domain::Config]
47
+ # @param metadata [Stoplight::Domain::Metadata]
48
+ # @return [Boolean] true if traffic should be stopped (rec), false otherwise (green)
49
+ # :nocov:
50
+ def stop_traffic?(config, metadata)
51
+ raise NotImplementedError
52
+ end
53
+ # :nocov:
54
+
55
+ # @param other [any]
56
+ # @return [Boolean]
57
+ def ==(other)
58
+ other.is_a?(self.class)
59
+ end
60
+
61
+ # Returns a compatibility result indicating the strategy is compatible.
62
+ #
63
+ # @return [Stoplight::Domain::CompatibilityResult] A compatible result.
64
+ private def compatible = CompatibilityResult.compatible
65
+
66
+ # Returns a compatibility result indicating the strategy is incompatible.
67
+ #
68
+ # @param errors [Array<String>] The list of error messages describing incompatibility.
69
+ # @return [Stoplight::Domain::CompatibilityResult] An incompatible result.
70
+ private def incompatible(*errors) = CompatibilityResult.incompatible(*errors)
71
+ end
72
+ end
73
+ end
74
+ end