stoplight 5.5.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/stoplight/admin/actions/remove.rb +23 -0
  4. data/lib/stoplight/admin/dependencies.rb +6 -1
  5. data/lib/stoplight/admin/helpers.rb +10 -5
  6. data/lib/stoplight/admin/lights_repository.rb +26 -14
  7. data/lib/stoplight/admin/views/_card.erb +13 -1
  8. data/lib/stoplight/admin.rb +9 -0
  9. data/lib/stoplight/common/deprecations.rb +11 -0
  10. data/lib/stoplight/domain/config.rb +5 -1
  11. data/lib/stoplight/domain/data_store.rb +58 -6
  12. data/lib/stoplight/domain/failure.rb +2 -0
  13. data/lib/stoplight/domain/light/configuration_builder_interface.rb +120 -16
  14. data/lib/stoplight/domain/light.rb +34 -24
  15. data/lib/stoplight/domain/metrics.rb +64 -0
  16. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  17. data/lib/stoplight/domain/{metadata.rb → state_snapshot.rb} +29 -37
  18. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  19. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  20. data/lib/stoplight/domain/storage/state.rb +87 -0
  21. data/lib/stoplight/domain/strategies/green_run_strategy.rb +2 -2
  22. data/lib/stoplight/domain/strategies/red_run_strategy.rb +3 -3
  23. data/lib/stoplight/domain/strategies/run_strategy.rb +2 -7
  24. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +63 -36
  25. data/lib/stoplight/domain/tracker/base.rb +0 -29
  26. data/lib/stoplight/domain/tracker/recovery_probe.rb +26 -22
  27. data/lib/stoplight/domain/tracker/request.rb +26 -21
  28. data/lib/stoplight/domain/traffic_control/base.rb +5 -5
  29. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +3 -7
  30. data/lib/stoplight/domain/traffic_control/error_rate.rb +3 -3
  31. data/lib/stoplight/domain/traffic_recovery/base.rb +5 -5
  32. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +4 -8
  33. data/lib/stoplight/domain/traffic_recovery.rb +0 -1
  34. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  35. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  36. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  37. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  38. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  39. data/lib/stoplight/infrastructure/data_store/memory.rb +163 -132
  40. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/get_metrics.lua +26 -0
  41. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  42. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  43. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  44. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  45. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  46. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  47. data/lib/stoplight/infrastructure/data_store/redis.rb +211 -165
  48. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  49. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  50. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  51. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  52. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  53. data/lib/stoplight/version.rb +1 -1
  54. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  55. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  56. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  57. data/lib/stoplight/wiring/default.rb +1 -1
  58. data/lib/stoplight/wiring/default_configuration.rb +1 -1
  59. data/lib/stoplight/wiring/default_factory_builder.rb +1 -1
  60. data/lib/stoplight/wiring/light_builder.rb +185 -0
  61. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  62. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  63. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  64. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  65. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  66. data/lib/stoplight/wiring/light_factory.rb +45 -132
  67. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  68. data/lib/stoplight/wiring/public_api.rb +3 -2
  69. data/lib/stoplight.rb +18 -3
  70. metadata +55 -16
  71. data/lib/stoplight/infrastructure/data_store/redis/get_metadata.lua +0 -38
  72. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
  73. data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
  74. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
  75. data/lib/stoplight/wiring/container.rb +0 -80
  76. data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -123
  77. data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
  78. data/lib/stoplight/wiring/system_container.rb +0 -9
  79. data/lib/stoplight/wiring/system_light_factory.rb +0 -17
  80. /data/lib/stoplight/infrastructure/data_store/redis/{record_failure.lua → lua_scripts/record_failure.lua} +0 -0
  81. /data/lib/stoplight/infrastructure/data_store/redis/{record_success.lua → lua_scripts/record_success.lua} +0 -0
  82. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_green.lua → lua_scripts/transition_to_green.lua} +0 -0
  83. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_red.lua → lua_scripts/transition_to_red.lua} +0 -0
  84. /data/lib/stoplight/infrastructure/data_store/redis/{transition_to_yellow.lua → lua_scripts/transition_to_yellow.lua} +0 -0
@@ -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,40 +35,68 @@ 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.
42
58
  #
43
59
  # @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.
60
+ # @param state_snapshot [Stoplight::Domain::StateSnapshot]
45
61
  # @yield The code block to execute.
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
- 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)
64
+ def execute(fallback, state_snapshot:, &code)
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
 
@@ -72,26 +108,17 @@ module Stoplight
72
108
  request_tracker.record_failure(error)
73
109
  end
74
110
 
75
- # @param metadata [Stoplight::Domain::Metadata]
111
+ # @param state_snapshot [Stoplight::Domain::StateSnapshot]
76
112
  # @return [void]
77
- private def enter_recovery(metadata)
78
- return if metadata.recovery_started?
113
+ private def enter_recovery(state_snapshot)
114
+ return if state_snapshot.recovery_started?
79
115
 
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
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)
84
120
  end
85
121
  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
122
  end
96
123
  end
97
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,51 +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
- metadata = data_store.record_recovery_probe_failure(config, exception)
42
+ metrics_store.record_failure(exception)
37
43
 
38
- recover(metadata)
44
+ recover
39
45
  end
40
46
 
41
47
  def record_success
42
- metadata = data_store.record_recovery_probe_success(config)
48
+ metrics_store.record_success
43
49
 
44
- recover(metadata)
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
- private def recover(metadata)
53
- recovery_result = traffic_recovery.determine_color(config, metadata)
57
+ private def recover
58
+ recovery_metrics = metrics_store.metrics_snapshot
59
+ recovery_result = traffic_recovery.determine_color(config, recovery_metrics)
54
60
 
55
- return if recovery_result == TrafficRecovery::PASS
61
+ return if recovery_result == TrafficRecovery::YELLOW
56
62
 
57
63
  from_color, to_color = RECOVERY_TRANSITIONS.fetch(recovery_result) do
58
64
  raise "recovery strategy returned unexpected color: #{recovery_result}"
59
65
  end
60
66
 
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
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
68
72
  end
69
73
  end
70
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,41 +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
- metadata = data_store.record_failure(config, exception)
49
+ metrics_store.record_failure(exception)
50
+ metrics = metrics_store.metrics_snapshot
44
51
 
45
- transition_to_red(exception, metadata:)
52
+ transition_to_red(exception, metrics:)
46
53
  end
47
54
 
48
55
  # @return [void]
49
- def record_success
50
- data_store.record_success(config)
51
- end
56
+ def record_success = metrics_store.record_success
52
57
 
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)
58
+ private def transition_to_red(exception, metrics:)
59
+ if traffic_control.stop_traffic?(config, metrics)
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
56
67
  end
57
68
  end
58
-
59
- # @param other [any]
60
- # @return [bool]
61
- def ==(other)
62
- super && traffic_control == other.traffic_control
63
- end
64
69
  end
65
70
  end
66
71
  end
@@ -18,11 +18,11 @@ module Stoplight
18
18
  # end
19
19
  # end
20
20
  #
21
- # def stop_traffic?(config, metadata)
22
- # total = metadata.successes + metadata.failures
21
+ # def stop_traffic?(config, metrics)
22
+ # total = metrics.successes + metrics.failures
23
23
  # return false if total < 10 # Minimum sample size
24
24
  #
25
- # error_rate = metadata.failures.fdiv(total)
25
+ # error_rate = metrics.failures.fdiv(total)
26
26
  # error_rate >= 0.5 # Stop traffic when error rate reaches 50%
27
27
  # end
28
28
  # end
@@ -44,10 +44,10 @@ module Stoplight
44
44
  # current state and metrics.
45
45
  #
46
46
  # @param config [Stoplight::Domain::Config]
47
- # @param metadata [Stoplight::Domain::Metadata]
47
+ # @param metrics [Stoplight::Domain::Metrics]
48
48
  # @return [Boolean] true if traffic should be stopped (rec), false otherwise (green)
49
49
  # :nocov:
50
- def stop_traffic?(config, metadata)
50
+ def stop_traffic?(config, metrics)
51
51
  raise NotImplementedError
52
52
  end
53
53
  # :nocov:
@@ -42,14 +42,10 @@ module Stoplight
42
42
  # Determines if traffic should be stopped based on failure counts.
43
43
  #
44
44
  # @param config [Stoplight::Domain::Config]
45
- # @param metadata [Stoplight::Domain::Metadata]
45
+ # @param metrics [Stoplight::Domain::Metrics]
46
46
  # @return [Boolean] true if failures have reached the threshold, false otherwise
47
- def stop_traffic?(config, metadata)
48
- if config.window_size
49
- [metadata.consecutive_errors, metadata.errors].min >= config.threshold
50
- else
51
- metadata.consecutive_errors >= config.threshold
52
- end
47
+ def stop_traffic?(config, metrics)
48
+ metrics.consecutive_errors >= config.threshold
53
49
  end
54
50
  end
55
51
  end
@@ -40,10 +40,10 @@ module Stoplight
40
40
  end
41
41
 
42
42
  # @param config [Stoplight::Domain::Config]
43
- # @param metadata [Stoplight::Domain::Metadata]
43
+ # @param metrics [Stoplight::Domain::Metrics]
44
44
  # @return [Boolean]
45
- def stop_traffic?(config, metadata)
46
- metadata.requests >= min_requests && metadata.error_rate >= config.threshold
45
+ def stop_traffic?(config, metrics)
46
+ metrics.requests >= min_requests && metrics.error_rate >= config.threshold
47
47
  end
48
48
  end
49
49
  end
@@ -14,14 +14,14 @@ module Stoplight
14
14
  # @min_samples = min_samples
15
15
  # end
16
16
  #
17
- # def determine_color(config, metadata)
18
- # total_probes = metadata.recovery_probe_successes + metadata.recovery_probe_errors
17
+ # def determine_color(config, metrics)
18
+ # total_probes = metrics.recovery_probe_successes + metrics.recovery_probe_errors
19
19
  #
20
20
  # if total_probes < @min_samples
21
21
  # return Color::YELLOW # Keep recovering, not enough samples
22
22
  # end
23
23
  #
24
- # success_rate = metadata.recovery_probe_successes.fdiv(total_probes)
24
+ # success_rate = metrics.recovery_probe_successes.fdiv(total_probes)
25
25
  # if success_rate >= @min_success_rate
26
26
  # Color::GREEN # Recovery successful
27
27
  # elsif success_rate <= 0.2
@@ -49,10 +49,10 @@ module Stoplight
49
49
  # current metrics and recovery progress.
50
50
  #
51
51
  # @param config [Stoplight::Domain::Config]
52
- # @param metadata [Stoplight::Domain::Metadata]
52
+ # @param metrics [Stoplight::Domain::Metrics]
53
53
  # @return [TrafficRecovery::Decision]
54
54
  # :nocov:
55
- def determine_color(config, metadata)
55
+ def determine_color(config, metrics)
56
56
  raise NotImplementedError
57
57
  end
58
58
  # :nocov:
@@ -49,16 +49,12 @@ module Stoplight
49
49
  # Determines if traffic should be resumed based on successes counts.
50
50
  #
51
51
  # @param config [Stoplight::Domain::Config]
52
- # @param metadata [Stoplight::Domain::Metadata]
52
+ # @param recovery_metrics [Stoplight::Domain::Metrics]
53
53
  # @return [TrafficRecovery::Decision]
54
- def determine_color(config, metadata)
55
- return TrafficRecovery::PASS if metadata.color != Color::YELLOW
56
-
57
- recovery_started_at = metadata.recovery_started_at || metadata.recovery_scheduled_after
58
-
59
- if metadata.last_error_at && metadata.last_error_at >= recovery_started_at
54
+ def determine_color(config, recovery_metrics)
55
+ if recovery_metrics.consecutive_errors > 0
60
56
  TrafficRecovery::RED
61
- elsif [metadata.consecutive_successes, metadata.recovery_probe_successes].min >= config.recovery_threshold
57
+ elsif recovery_metrics.consecutive_successes >= config.recovery_threshold
62
58
  TrafficRecovery::GREEN
63
59
  else
64
60
  TrafficRecovery::YELLOW
@@ -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
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Stoplight
6
+ module Infrastructure
7
+ module DataStore
8
+ # A wrapper around a data store that provides fail-safe mechanisms using a
9
+ # circuit breaker. It ensures that operations on the data store can gracefully
10
+ # handle failures by falling back to default values when necessary.
11
+ #
12
+ # @api private
13
+ class FailSafe < Domain::DataStore
14
+ # @!attribute data_store
15
+ # @return [Stoplight::DataStore::Base] The underlying primary data store being used
16
+ attr_reader :data_store
17
+
18
+ # @!attribute error_notifier
19
+ # @return [Proc]
20
+ attr_reader :error_notifier
21
+
22
+ # @!attribute failover_data_store
23
+ # @return [Stoplight::DataStore::Base] The fallback data store used when the primary fails.
24
+ attr_reader :failover_data_store
25
+
26
+ # @!attribute circuit_breaker
27
+ # @return [Stoplight::Light] The circuit breaker used to handle data store failures.
28
+ private attr_reader :circuit_breaker
29
+
30
+ # @param data_store [Stoplight::Domain::DataStore]
31
+ # @param error_notifier [Proc]
32
+ # @param failover_data_store [Stoplight::Domain::DataStore]
33
+ # @param circuit_breaker [Stoplight::Domain::Light]
34
+ def initialize(data_store:, error_notifier:, failover_data_store:, circuit_breaker:)
35
+ @data_store = data_store
36
+ @error_notifier = error_notifier
37
+ @failover_data_store = failover_data_store
38
+ @circuit_breaker = circuit_breaker
39
+ end
40
+
41
+ def names
42
+ with_fallback(:names) do
43
+ data_store.names
44
+ end
45
+ end
46
+
47
+ def get_metrics(config, *args, **kwargs)
48
+ with_fallback(:get_metrics, config, *args, **kwargs) do
49
+ data_store.get_metrics(config, *args, **kwargs)
50
+ end
51
+ end
52
+
53
+ def get_recovery_metrics(config, *args, **kwargs)
54
+ with_fallback(:get_recovery_metrics, config, *args, **kwargs) do
55
+ data_store.get_recovery_metrics(config, *args, **kwargs)
56
+ end
57
+ end
58
+
59
+ def get_state_snapshot(config)
60
+ with_fallback(:get_state_snapshot, config) do
61
+ data_store.get_state_snapshot(config)
62
+ end
63
+ end
64
+
65
+ def clear_metrics(config)
66
+ with_fallback(:clear_metrics, config) do
67
+ data_store.clear_metrics(config)
68
+ end
69
+ end
70
+
71
+ def clear_recovery_metrics(config)
72
+ with_fallback(:clear_recovery_metrics, config) do
73
+ data_store.clear_recovery_metrics(config)
74
+ end
75
+ end
76
+
77
+ def record_failure(config, *args, **kwargs)
78
+ with_fallback(:record_failure, config, *args, **kwargs) do
79
+ data_store.record_failure(config, *args, **kwargs)
80
+ end
81
+ end
82
+
83
+ def record_success(config, *args, **kwargs)
84
+ with_fallback(:record_success, config, *args, **kwargs) do
85
+ data_store.record_success(config, *args, **kwargs)
86
+ end
87
+ end
88
+
89
+ def record_recovery_probe_success(config, *args, **kwargs)
90
+ with_fallback(:record_recovery_probe_success, config, *args, **kwargs) do
91
+ data_store.record_recovery_probe_success(config, *args, **kwargs)
92
+ end
93
+ end
94
+
95
+ def record_recovery_probe_failure(config, *args, **kwargs)
96
+ with_fallback(:record_recovery_probe_failure, config, *args, **kwargs) do
97
+ data_store.record_recovery_probe_failure(config, *args, **kwargs)
98
+ end
99
+ end
100
+
101
+ def set_state(config, *args, **kwargs)
102
+ with_fallback(:set_state, config, *args, **kwargs) do
103
+ data_store.set_state(config, *args, **kwargs)
104
+ end
105
+ end
106
+
107
+ def transition_to_color(config, *args, **kwargs)
108
+ with_fallback(:transition_to_color, config, *args, **kwargs) do
109
+ data_store.transition_to_color(config, *args, **kwargs)
110
+ end
111
+ end
112
+
113
+ def delete_light(config, *args, **kwargs)
114
+ with_fallback(:delete_light, config, *args, **kwargs) do
115
+ data_store.delete_light(config, *args, **kwargs)
116
+ end
117
+ end
118
+
119
+ # @param config [Stoplight::Domain::Config]
120
+ def acquire_recovery_lock(config)
121
+ with_fallback(:acquire_recovery_lock, config) do
122
+ data_store.acquire_recovery_lock(config)
123
+ end
124
+ end
125
+
126
+ # Routes release to correct store based on token type.
127
+ # Redis tokens release via primary (with error notification on failure).
128
+ # Memory tokens release via failover directly.
129
+ #
130
+ # @param recovery_lock_token [Stoplight::Domain::RecoveryLockToken]
131
+ def release_recovery_lock(recovery_lock_token)
132
+ case recovery_lock_token
133
+ in Redis::RecoveryLockToken
134
+ fallback = proc do |error|
135
+ error_notifier.call(error) if error
136
+ end
137
+
138
+ circuit_breaker.run(fallback) do
139
+ data_store.release_recovery_lock(recovery_lock_token)
140
+ end
141
+ in Memory::RecoveryLockToken
142
+ failover_data_store.release_recovery_lock(recovery_lock_token)
143
+ end
144
+ end
145
+
146
+ def ==(other)
147
+ other.is_a?(self.class) && other.data_store == data_store && other.error_notifier == error_notifier &&
148
+ other.failover_data_store == failover_data_store
149
+ end
150
+
151
+ # @param method_name [Symbol] protected method name
152
+ private def with_fallback(method_name, *args, **kwargs, &code)
153
+ fallback = proc do |error|
154
+ config = args.first
155
+ error_notifier.call(error) if config && error
156
+ @failover_data_store.public_send(method_name, *args, **kwargs)
157
+ end
158
+
159
+ circuit_breaker.run(fallback, &code)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module DataStore
6
+ class Memory
7
+ class Metrics
8
+ attr_accessor :consecutive_errors
9
+ attr_accessor :consecutive_successes
10
+ attr_accessor :last_error
11
+ attr_accessor :last_success_at
12
+
13
+ def initialize(consecutive_errors: 0, consecutive_successes: 0, last_error: nil, last_success_at: nil)
14
+ @consecutive_errors = consecutive_errors
15
+ @consecutive_successes = consecutive_successes
16
+ @last_error = last_error
17
+ @last_success_at = last_success_at
18
+ end
19
+
20
+ def last_error_at
21
+ @last_error&.time
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end