stoplight 5.6.0 → 5.7.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stoplight/admin/dependencies.rb +1 -1
  3. data/lib/stoplight/admin/helpers.rb +10 -5
  4. data/lib/stoplight/admin/lights_repository.rb +18 -15
  5. data/lib/stoplight/admin.rb +2 -1
  6. data/lib/stoplight/common/deprecations.rb +11 -0
  7. data/lib/stoplight/domain/config.rb +5 -1
  8. data/lib/stoplight/domain/data_store.rb +17 -1
  9. data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
  10. data/lib/stoplight/domain/light.rb +31 -20
  11. data/lib/stoplight/domain/metrics.rb +6 -27
  12. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  13. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  14. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  15. data/lib/stoplight/domain/storage/state.rb +87 -0
  16. data/lib/stoplight/domain/strategies/run_strategy.rb +0 -5
  17. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +58 -32
  18. data/lib/stoplight/domain/tracker/base.rb +0 -29
  19. data/lib/stoplight/domain/tracker/recovery_probe.rb +23 -22
  20. data/lib/stoplight/domain/tracker/request.rb +23 -19
  21. data/lib/stoplight/domain/traffic_recovery/base.rb +1 -2
  22. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +2 -8
  23. data/lib/stoplight/domain/traffic_recovery.rb +0 -1
  24. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  25. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  26. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  27. data/lib/stoplight/infrastructure/data_store/memory.rb +61 -32
  28. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  29. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  30. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  31. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  32. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  33. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  34. data/lib/stoplight/infrastructure/data_store/redis.rb +133 -162
  35. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  36. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  37. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  38. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  39. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  40. data/lib/stoplight/version.rb +1 -1
  41. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  42. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  43. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  44. data/lib/stoplight/wiring/default.rb +1 -1
  45. data/lib/stoplight/wiring/default_configuration.rb +1 -1
  46. data/lib/stoplight/wiring/default_factory_builder.rb +1 -1
  47. data/lib/stoplight/wiring/light_builder.rb +185 -0
  48. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  49. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  50. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  51. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  52. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  53. data/lib/stoplight/wiring/light_factory.rb +45 -132
  54. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  55. data/lib/stoplight/wiring/public_api.rb +3 -2
  56. data/lib/stoplight.rb +18 -3
  57. metadata +50 -15
  58. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
  59. data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
  60. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
  61. data/lib/stoplight/wiring/container.rb +0 -80
  62. data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -147
  63. data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
  64. data/lib/stoplight/wiring/system_container.rb +0 -9
  65. data/lib/stoplight/wiring/system_light_factory.rb +0 -17
  66. /data/lib/stoplight/infrastructure/data_store/redis/{get_metrics.lua → lua_scripts/get_metrics.lua} +0 -0
  67. /data/lib/stoplight/infrastructure/data_store/redis/{record_failure.lua → lua_scripts/record_failure.lua} +0 -0
  68. /data/lib/stoplight/infrastructure/data_store/redis/{record_success.lua → lua_scripts/record_success.lua} +0 -0
  69. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_green.lua → lua_scripts/transition_to_green.lua} +0 -0
  70. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_red.lua → lua_scripts/transition_to_red.lua} +0 -0
  71. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_yellow.lua → lua_scripts/transition_to_yellow.lua} +0 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Storage
6
+ # Encapsulates recovery lock management for coordinating recovery probes.
7
+ #
8
+ # When a circuit enters YELLOW state (half-open), it begins sending
9
+ # "recovery probes" - test requests to check if the protected service
10
+ # has recovered. In distributed deployments with multiple instances,
11
+ # recovery locks ensure only ONE instance sends probes at a time.
12
+ #
13
+ # Without coordination, all instances would simultaneously:
14
+ # 1. Detect the circuit is YELLOW
15
+ # 2. Send recovery probes to the struggling service
16
+ # 3. Potentially overwhelm it with "test" traffic
17
+ #
18
+ # Lock Lifecycle:
19
+ #
20
+ # Instance A: acquire_lock -> probe -> release_lock
21
+ # Instance B: acquire_lock -> nil (already held) -> skip probe
22
+ # Instance C: acquire_lock -> nil (already held) -> skip probe
23
+ #
24
+ # Lock Semantics:
25
+ # - Returns +nil+ if lock is already held. Never blocks waiting for lock availability
26
+ # - Locks must automatically expire when persisted storage is used
27
+ # - Failed releases are acceptable (timeout provides safety)
28
+ #
29
+ # @abstract
30
+ # @see Stoplight::Domain::Strategies::YellowRunStrategy
31
+ class RecoveryLock
32
+ # Attempts to acquire recovery lock for exclusive probe execution.
33
+ #
34
+ # This method tries to acquire a lock that serializes recovery probe
35
+ # execution across multiple instances. If the lock is already held by
36
+ # another instance, returns +nil+ immediately without blocking.
37
+ #
38
+ # @return [Stoplight::Domain::RecoveryLockToken, nil]
39
+ # - +RecoveryLockToken+: Lock acquired, caller should send probe
40
+ # - +nil+: Lock unavailable, another instance is probing
41
+ #
42
+ def acquire_lock = raise NotImplementedError
43
+
44
+ # Releases a previously acquired lock.
45
+ #
46
+ # This method releases the lock token returned by +#acquire_lock+,
47
+ # allowing other instances to acquire it. Release should be called
48
+ # in an ensure block to guarantee cleanup even if probe fails.
49
+ #
50
+ # @param lock [Stoplight::Domain::RecoveryLockToken] The token returned by +#acquire_lock+
51
+ # @return [void]
52
+ def release_lock(lock) = raise NotImplementedError
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Storage
6
+ # Encapsulates circuit breaker state storage.
7
+ #
8
+ # State management handles the current operational mode of a circuit breaker:
9
+ # - Color (GREEN/YELLOW/RED) - whether the circuit is open or closed
10
+ # - Lock state (LOCKED_GREEN/LOCKED_RED/UNLOCKED) - manual overrides
11
+ # - State transitions - tracking color changes for notifications #
12
+ #
13
+ # State requires stronger consistency than metrics because:
14
+ # - Multiple instances must agree on circuit color
15
+ # - Race conditions during transitions must be handled
16
+ # - Lock states must be immediately visible across instances
17
+ #
18
+ # @abstract
19
+ # @see Stoplight::Domain::Storage::Metrics
20
+ class State
21
+ # Retrieves current state snapshot for decision-making.
22
+ #
23
+ # The snapshot is an immutable view of the circuit's current state,
24
+ # including its color and lock status. This method is called on every
25
+ # circuit breaker invocation to determine whether to allow traffic.
26
+ #
27
+ # This is called on every request, so implementations should be fast.
28
+ #
29
+ # @return [Stoplight::Domain::StateSnapshot]
30
+ def state_snapshot = raise NotImplementedError
31
+
32
+ # Sets the lock state of the circuit.
33
+ #
34
+ # Locks allow manual override of circuit behavior:
35
+ # - LOCKED_GREEN: Force circuit closed (allow all traffic)
36
+ # - LOCKED_RED: Force circuit open (block all traffic)
37
+ # - UNLOCKED: Follow normal circuit breaker rules
38
+ #
39
+ # Lock states take precedence over color states. A locked circuit
40
+ # ignores failure thresholds and stays in the locked state until
41
+ # explicitly unlocked.
42
+ #
43
+ # Use Cases:
44
+ # - Emergency traffic control during incidents
45
+ # - Maintenance windows (lock RED to prevent traffic)
46
+ # - Gradual rollout (lock GREEN during testing)
47
+ #
48
+ # @param state [String] The new state to set.
49
+ # @return [String] The state that was set.
50
+ def set_state(state) = raise NotImplementedError
51
+
52
+ # Transitions the Stoplight to the specified color.
53
+ #
54
+ # This method performs a color transition operation that works across distributed instances
55
+ # of the light. It ensures that in a multi-instance environment, only one instance
56
+ # is considered the "first" to perform the transition (and therefore responsible for
57
+ # triggering notifications).
58
+ #
59
+ # @param color [String] The target color/state to transition to.
60
+ # Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
61
+ #
62
+ # @return [Boolean] Returns +true+ if this instance was the first to perform this specific transition
63
+ # (and should therefore trigger notifications). Returns +false+ if another instance already
64
+ # initiated this transition.
65
+ #
66
+ # @note In distributed environments with multiple instances, race conditions can occur when instances
67
+ # attempt conflicting transitions simultaneously (e.g., one instance tries to transition from
68
+ # YELLOW to GREEN while another tries YELLOW to RED). The implementation handles this, but
69
+ # be aware that the last operation may determine the final color of the light.
70
+ #
71
+ def transition_to_color(color) = raise NotImplementedError
72
+
73
+ # Clears all state data for this circuit.
74
+ #
75
+ # This removes the circuit from storage entirely, resetting it to
76
+ # default (unlocked, green) state. The next invocation will start
77
+ # with fresh state.
78
+ #
79
+ # @note This does NOT clear metrics. If you want to fully
80
+ # reset a circuit, clear both state and metrics stores.
81
+ #
82
+ # @return [void]
83
+ def clear = raise NotImplementedError
84
+ end
85
+ end
86
+ end
87
+ end
@@ -16,11 +16,6 @@ module Stoplight
16
16
  raise NotImplementedError, "Subclasses must implement the execute method"
17
17
  end
18
18
  # :nocov:
19
-
20
- # @return [Boolean]
21
- def ==(other)
22
- other.is_a?(self.class)
23
- end
24
19
  end
25
20
  end
26
21
  end
@@ -15,9 +15,17 @@ module Stoplight
15
15
  # @return [Stoplight::Domain::Config] The configuration for the light.
16
16
  protected attr_reader :config
17
17
 
18
- # @!attribute [r] data_store
19
- # @return [Stoplight::DataStore::Base] The data store associated with the light.
20
- protected attr_reader :data_store
18
+ # @!attribute [r] stare_store
19
+ # @return [Stoplight::Domain::Storage::State]
20
+ protected attr_reader :state_store
21
+
22
+ # @!attribute [r] metrics_store
23
+ # @return [Stoplight::Domain::Storage::Metrics]
24
+ protected attr_reader :metrics_store
25
+
26
+ # @!attribute [r] recovery_lock_store
27
+ # @return [Stoplight::Domain::Storage::RecoveryLock]
28
+ protected attr_reader :recovery_lock_store
21
29
 
22
30
  # @!attribute [r] notifiers
23
31
  # @return [Stoplight::Domain::StateTransitionNotifier]
@@ -27,15 +35,23 @@ module Stoplight
27
35
  # @return [Stoplight::Domain::RecoveryProbeRequestRecorder]
28
36
  protected attr_reader :request_tracker
29
37
 
38
+ # @!attribute [r] red_run_strategy
39
+ # @return [Stoplight::Domain::Strategies::RedRunStrategy]
40
+ protected attr_reader :red_run_strategy
41
+
30
42
  # @param config [Stoplight::Domain::Config]
31
- # @param data_store [Stoplight::DataStore::Base]
32
43
  # @param notifiers [Array<Stoplight::Domain::StateTransitionNotifier>]
33
44
  # @param request_tracker [Stoplight::Domain::Tracker::RecoveryProbe]
34
- def initialize(config:, data_store:, notifiers:, request_tracker:)
45
+ # @param red_run_strategy [Stoplight::Domain::Strategies::RedRunStrategy]
46
+ # @param recovery_lock_store [Stoplight::Domain::Storage::RecoveryLock]
47
+ def initialize(config:, notifiers:, request_tracker:, red_run_strategy:, state_store:, metrics_store:, recovery_lock_store:)
35
48
  @config = config
36
- @data_store = data_store
37
49
  @notifiers = notifiers
38
50
  @request_tracker = request_tracker
51
+ @red_run_strategy = red_run_strategy
52
+ @state_store = state_store
53
+ @metrics_store = metrics_store
54
+ @recovery_lock_store = recovery_lock_store
39
55
  end
40
56
 
41
57
  # Executes the provided code block when the light is in the yellow state.
@@ -46,21 +62,41 @@ module Stoplight
46
62
  # @return [Object] The result of the code block if successful.
47
63
  # @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
48
64
  def execute(fallback, state_snapshot:, &code)
49
- enter_recovery(state_snapshot)
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)
65
+ # Everything withing this block executed exclusively:
66
+ # - enter recovery
67
+ # - execute user's code
68
+ # - record outcome
69
+ # - transition to green or red if needed
70
+ with_recovery_lock(fallback:, state_snapshot:) do
71
+ enter_recovery(state_snapshot)
72
+
73
+ code.call.tap { record_recovery_probe_success }
74
+ rescue => error
75
+ if config.track_error?(error)
76
+ record_recovery_probe_failure(error)
77
+
78
+ if fallback
79
+ fallback.call(error)
80
+ else
81
+ raise
82
+ end
58
83
  else
84
+ record_recovery_probe_success
59
85
  raise
60
86
  end
61
- else
62
- record_recovery_probe_success
63
- raise
87
+ end
88
+ end
89
+
90
+ def with_recovery_lock(fallback:, state_snapshot:)
91
+ recovery_lock_token = recovery_lock_store.acquire_lock
92
+ if recovery_lock_token.nil?
93
+ return red_run_strategy.execute(fallback, state_snapshot:)
94
+ end
95
+
96
+ begin
97
+ yield
98
+ ensure
99
+ recovery_lock_store.release_lock(recovery_lock_token)
64
100
  end
65
101
  end
66
102
 
@@ -77,22 +113,12 @@ module Stoplight
77
113
  private def enter_recovery(state_snapshot)
78
114
  return if state_snapshot.recovery_started?
79
115
 
80
- if data_store.transition_to_color(config, Color::YELLOW)
81
- data_store.clear_windowed_metrics(config)
82
- notifiers.each do |notifier|
83
- notifier.notify(config, Color::RED, Color::YELLOW, nil)
84
- end
116
+ state_store.transition_to_color(Color::YELLOW)
117
+ metrics_store.clear
118
+ notifiers.each do |notifier|
119
+ notifier.notify(config, Color::RED, Color::YELLOW, nil)
85
120
  end
86
121
  end
87
-
88
- # @return [Boolean]
89
- def ==(other)
90
- super &&
91
- config == other.config &&
92
- notifiers == other.notifiers &&
93
- data_store == other.data_store &&
94
- request_tracker == other.request_tracker
95
- end
96
122
  end
97
123
  end
98
124
  end
@@ -6,35 +6,6 @@ module Stoplight
6
6
  # @api private
7
7
  # @abstract
8
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
9
  end
39
10
  end
40
11
  end
@@ -4,10 +4,6 @@ module Stoplight
4
4
  module Domain
5
5
  module Tracker
6
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
7
  # @!attribute [r] traffic_recovery
12
8
  # @return [Stoplight::Domain::TrafficRecovery::Base]
13
9
  protected attr_reader :traffic_recovery
@@ -20,54 +16,59 @@ module Stoplight
20
16
  # @return [Stoplight::Domain::Config] The configuration for the light.
21
17
  protected attr_reader :config
22
18
 
23
- # @param data_store [Stoplight::Domain::DataStore]
19
+ # @!attribute [r] metrics_store
20
+ # @return [Stoplight::Domain::Storage::Metrics]
21
+ protected attr_reader :metrics_store
22
+
23
+ # @!attribute [r] state_store
24
+ # @return [Stoplight::Domain::Storage::State]
25
+ protected attr_reader :state_store
26
+
24
27
  # @param traffic_recovery [Stoplight::Domain::TrafficRecovery::Base]
25
28
  # @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
26
29
  # @param config [Stoplight::Domain::Config]
27
- def initialize(data_store:, traffic_recovery:, notifiers:, config:)
28
- @data_store = data_store
30
+ # @param metrics_store [Stoplight::Domain::Storage::Metrics]
31
+ # @param state_store [Stoplight::Domain::Storage::State]
32
+ def initialize(traffic_recovery:, notifiers:, config:, metrics_store:, state_store:)
29
33
  @traffic_recovery = traffic_recovery
30
34
  @notifiers = notifiers
31
35
  @config = config
36
+ @metrics_store = metrics_store
37
+ @state_store = state_store
32
38
  end
33
39
 
34
40
  # @param exception [Exception]
35
41
  def record_failure(exception)
36
- data_store.record_recovery_probe_failure(config, exception)
42
+ metrics_store.record_failure(exception)
37
43
 
38
44
  recover
39
45
  end
40
46
 
41
47
  def record_success
42
- data_store.record_recovery_probe_success(config)
48
+ metrics_store.record_success
43
49
 
44
50
  recover
45
51
  end
46
52
  RECOVERY_TRANSITIONS = {
47
53
  TrafficRecovery::GREEN => [Color::YELLOW, Color::GREEN],
48
- TrafficRecovery::YELLOW => [Color::RED, Color::YELLOW],
49
54
  TrafficRecovery::RED => [Color::YELLOW, Color::RED]
50
55
  }.freeze
51
56
 
52
57
  private def recover
53
- recovery_metrics = data_store.get_recovery_metrics(config)
54
- state_snapshot = data_store.get_state_snapshot(config) # TODO: is this really necessary?
55
-
56
- recovery_result = traffic_recovery.determine_color(config, recovery_metrics, state_snapshot)
58
+ recovery_metrics = metrics_store.metrics_snapshot
59
+ recovery_result = traffic_recovery.determine_color(config, recovery_metrics)
57
60
 
58
- return if recovery_result == TrafficRecovery::PASS
61
+ return if recovery_result == TrafficRecovery::YELLOW
59
62
 
60
63
  from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
61
64
  raise "recovery strategy returned unexpected color: #{recovery_result}"
62
65
  end
63
66
 
64
- transition_and_notify(from_color, to_color, nil)
65
- end
66
-
67
- # @param other [any]
68
- # @return [bool]
69
- def ==(other)
70
- super && traffic_recovery == other.traffic_recovery
67
+ state_store.transition_to_color(to_color)
68
+ metrics_store.clear
69
+ notifiers.each do |notifier|
70
+ notifier.notify(config, from_color, to_color, nil)
71
+ end
71
72
  end
72
73
  end
73
74
  end
@@ -10,10 +10,6 @@ module Stoplight
10
10
  #
11
11
  # @api private
12
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
13
  # @!attribute [r] traffic_control
18
14
  # @return [Stoplight::Domain::TrafficControl::Base]
19
15
  protected attr_reader :traffic_control
@@ -26,42 +22,50 @@ module Stoplight
26
22
  # @return [Stoplight::Domain::Config] The configuration for the light.
27
23
  protected attr_reader :config
28
24
 
29
- # @param data_store [Stoplight::Domain::DataStore]
25
+ # @!attribute metrics_store
26
+ # @return [Stoplight::Storage::Metrics]
27
+ protected attr_reader :metrics_store
28
+
29
+ # @!attribute [r] state_store
30
+ # @return [Stoplight::Domain::Storage::State]
31
+ protected attr_reader :state_store
32
+
30
33
  # @param traffic_control [Stoplight::Domain::TrafficControl::Base]
31
34
  # @param notifiers [<Stoplight::Domain::StateTransitionNotifier>]
32
35
  # @param config [Stoplight::Domain::Config]
33
- def initialize(data_store:, traffic_control:, notifiers:, config:)
34
- @data_store = data_store
36
+ # @param metrics_store [Stoplight::Storage::Metrics]
37
+ # @param state_store [Stoplight::Domain::Storage::State]
38
+ def initialize(traffic_control:, notifiers:, config:, metrics_store:, state_store:)
35
39
  @traffic_control = traffic_control
36
40
  @notifiers = notifiers
37
41
  @config = config
42
+ @metrics_store = metrics_store
43
+ @state_store = state_store
38
44
  end
39
45
 
40
46
  # @param exception [Exception]
41
47
  # @return [void]
42
48
  def record_failure(exception)
43
- data_store.record_failure(config, exception)
44
- metrics = data_store.get_metrics(config)
49
+ metrics_store.record_failure(exception)
50
+ metrics = metrics_store.metrics_snapshot
45
51
 
46
52
  transition_to_red(exception, metrics:)
47
53
  end
48
54
 
49
55
  # @return [void]
50
- def record_success
51
- data_store.record_success(config)
52
- end
56
+ def record_success = metrics_store.record_success
53
57
 
54
58
  private def transition_to_red(exception, metrics:)
55
59
  if traffic_control.stop_traffic?(config, metrics)
56
- transition_and_notify(Color::GREEN, Color::RED, exception)
60
+ # Returns true only if not yet in red therefore preventing
61
+ # duplicate notifications
62
+ if state_store.transition_to_color(Color::RED)
63
+ notifiers.each do |notifier|
64
+ notifier.notify(config, Color::GREEN, Color::RED, exception)
65
+ end
66
+ end
57
67
  end
58
68
  end
59
-
60
- # @param other [any]
61
- # @return [bool]
62
- def ==(other)
63
- super && traffic_control == other.traffic_control
64
- end
65
69
  end
66
70
  end
67
71
  end
@@ -50,10 +50,9 @@ module Stoplight
50
50
  #
51
51
  # @param config [Stoplight::Domain::Config]
52
52
  # @param metrics [Stoplight::Domain::Metrics]
53
- # @param state_snapshot [Stoplight::Domain::StateSnapshot]
54
53
  # @return [TrafficRecovery::Decision]
55
54
  # :nocov:
56
- def determine_color(config, metrics, state_snapshot)
55
+ def determine_color(config, metrics)
57
56
  raise NotImplementedError
58
57
  end
59
58
  # :nocov:
@@ -50,15 +50,9 @@ module Stoplight
50
50
  #
51
51
  # @param config [Stoplight::Domain::Config]
52
52
  # @param recovery_metrics [Stoplight::Domain::Metrics]
53
- # @param state_snapshot [Stoplight::Domain::StateSnapshot]
54
53
  # @return [TrafficRecovery::Decision]
55
- def determine_color(config, recovery_metrics, state_snapshot)
56
- return TrafficRecovery::PASS if state_snapshot.color != Color::YELLOW
57
-
58
- recovery_started_at = state_snapshot.recovery_started_at || state_snapshot.recovery_scheduled_after
59
-
60
- # TODO: Need to add metrics cleanup and we can just use recovery_metrics.errors > 0
61
- if recovery_metrics.last_error_at && recovery_metrics.last_error_at >= recovery_started_at
54
+ def determine_color(config, recovery_metrics)
55
+ if recovery_metrics.consecutive_errors > 0
62
56
  TrafficRecovery::RED
63
57
  elsif recovery_metrics.consecutive_successes >= config.recovery_threshold
64
58
  TrafficRecovery::GREEN
@@ -7,7 +7,6 @@ module Stoplight
7
7
  GREEN = Decision.new("green")
8
8
  YELLOW = Decision.new("yellow")
9
9
  RED = Decision.new("red")
10
- PASS = Decision.new("pass")
11
10
  end
12
11
  end
13
12
  end