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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f540f78d771a711da32d2e50cc150c4072cf805e4ef7e52e0b5ea07dd2435fea
4
- data.tar.gz: 2b859ca65577026db5e1e5af4bc2900e178edc0e230b1cfc75728ffcadc9efc8
3
+ metadata.gz: d96e1b5dcce81c642d059b07b2e274b8c7dbd7f246b86ee25ce40567eaba8418
4
+ data.tar.gz: e6b04f0ff592f89f345ff3f9ec9e26b86463ff0baeb56ef752cbd00b7f8952b8
5
5
  SHA512:
6
- metadata.gz: 88eaa1d335e4b956cc354d4c8295e46acd6f4dbb0188cc4cd5ecef99a13c4cf666e09e9c2d4c4f926f2ed0c5a582c9283341de028dc4489645cbde256d805504
7
- data.tar.gz: d94325879c511730d6b53b40d13fed577e10119813ec3be5f18f92caccf42d1a886d8c0fe8d6c887ff3d96610f7ac2c53e3d992d25e257cf38fea33795260272
6
+ metadata.gz: 01c5c2356f84eaeade94edba7113e700682911558d165b8b60186abbe0cda3d0f453427b8ab6e96f4ab51948e3ec5a109e53e1b51cc15c46c3fd79aad4865768
7
+ data.tar.gz: 94633b744ef88219f66289d9aae91bbb35b013bbea82c84f0f253abd15d4e5bd7ea5a123c39dffc36911479b1663cee71f786110be0ee15c3c8f56d035032e76
@@ -8,7 +8,7 @@ module Stoplight
8
8
  attr_reader :data_store
9
9
  private :data_store
10
10
 
11
- # @param data_store [Stoplight::DataStore::Base]
11
+ # @param data_store [Stoplight::Domain::DataStore]
12
12
  def initialize(data_store:)
13
13
  @data_store = data_store
14
14
  end
@@ -15,11 +15,16 @@ module Stoplight
15
15
  end
16
16
 
17
17
  private def data_store
18
- settings.data_store.tap do |data_store|
19
- if data_store.is_a?(Stoplight::DataStore::Memory)
20
- raise "Stoplight Admin requires a persistent data store, but the current data store is Memory. " \
21
- "Please configure a different data store in your Stoplight configuration."
22
- end
18
+ if settings.data_store.is_a?(Stoplight::DataStore::Memory)
19
+ raise "Stoplight Admin requires a persistent data store, but the current data store is Memory. " \
20
+ "Please configure a different data store in your Stoplight configuration."
21
+ else
22
+ Stoplight::Wiring::LightBuilder.new(
23
+ {
24
+ data_store: settings.data_store,
25
+ config: Wiring::Light::DefaultConfig
26
+ }
27
+ ).__send__(:data_store)
23
28
  end
24
29
  end
25
30
  end
@@ -4,11 +4,11 @@ module Stoplight
4
4
  class Admin
5
5
  class LightsRepository
6
6
  # @!attribute data_store
7
- # @return [Stoplight::DataStore::Base]
7
+ # @return [Stoplight::Domain::DataStore]
8
8
  attr_reader :data_store
9
9
  private :data_store
10
10
 
11
- # @param data_store [Stoplight::DataStore::Base]
11
+ # @param data_store [Stoplight::Domain::DataStore]
12
12
  def initialize(data_store:)
13
13
  @data_store = data_store
14
14
  end
@@ -37,46 +37,49 @@ module Stoplight
37
37
  # color
38
38
  # @return [void]
39
39
  def lock(name, color = nil)
40
- light = build_light(name)
40
+ config = build_config(name)
41
+ color ||= data_store.get_state_snapshot(config).color
41
42
 
42
- case color || light.color
43
+ case color
43
44
  when Stoplight::Color::GREEN
44
- light.lock(Stoplight::Color::GREEN)
45
+ data_store.set_state(config, Stoplight::State::LOCKED_GREEN)
45
46
  else
46
- light.lock(Stoplight::Color::RED)
47
+ data_store.set_state(config, Stoplight::State::LOCKED_RED)
47
48
  end
48
49
  end
49
50
 
50
51
  # @param name [String] unlocks light by its name
51
52
  # @return [void]
52
53
  def unlock(name)
53
- build_light(name).unlock
54
+ config = build_config(name)
55
+ data_store.set_state(config, Domain::State::UNLOCKED)
54
56
  end
55
57
 
56
58
  # @param name [String] removes light metadata by its name
57
59
  # @return [void]
58
60
  def remove(name)
59
- light = build_light(name)
61
+ config = build_config(name)
60
62
 
61
- data_store.delete_light(light.config)
63
+ data_store.delete_light(config)
62
64
  end
63
65
 
64
66
  private def load_light(name)
65
- light = build_light(name)
67
+ config = build_config(name)
68
+
66
69
  # failures, state
67
- state_snapshot = data_store.get_state_snapshot(light.config)
68
- metrics = data_store.get_metrics(light.config)
70
+ state_snapshot = data_store.get_state_snapshot(config)
71
+ metrics = data_store.get_metrics(config)
69
72
 
70
73
  Light.new(
71
74
  name: name,
72
- color: light.color,
75
+ color: state_snapshot.color,
73
76
  state: state_snapshot.locked_state,
74
77
  failures: [metrics.last_error].compact
75
78
  )
76
79
  end
77
80
 
78
- private def build_light(name)
79
- Stoplight(name, data_store: data_store)
81
+ private def build_config(name)
82
+ Wiring::Light::DefaultConfig.with(name:)
80
83
  end
81
84
  end
82
85
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi" # Ruby 3.2 needs this
3
+ require "cgi/escape"
4
+ require "cgi/util" if RUBY_VERSION < "3.5"
4
5
 
5
6
  begin
6
7
  require "sinatra/base"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Common
5
+ module Deprecations
6
+ extend self
7
+
8
+ def deprecate(message) = warn("[DEPRECATION] #{message}")
9
+ end
10
+ end
11
+ end
@@ -7,7 +7,7 @@ module Stoplight
7
7
  # # @!attribute [r] name
8
8
  # @return [String]
9
9
  #
10
- # @!attribute [r] cool_off_time
10
+ # @!attribute [r] cool_off_time - cool-off time in seconds
11
11
  # @return [Numeric]
12
12
  #
13
13
  # @!attribute [r] threshold
@@ -50,6 +50,10 @@ module Stoplight
50
50
 
51
51
  !skip && track
52
52
  end
53
+
54
+ def cool_off_time_in_milliseconds
55
+ cool_off_time * 1_000
56
+ end
53
57
  end
54
58
  end
55
59
  end
@@ -44,7 +44,11 @@ module Stoplight
44
44
  #
45
45
  # @param config [Stoplight::Domain::Config] The light configuration.
46
46
  # @return [void]
47
- def clear_windowed_metrics(config)
47
+ def clear_metrics(config)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def clear_recovery_metrics(config)
48
52
  raise NotImplementedError
49
53
  end
50
54
 
@@ -91,6 +95,18 @@ module Stoplight
91
95
  raise NotImplementedError
92
96
  end
93
97
 
98
+ # Acquires recovery lock for serializing probe execution.
99
+ #
100
+ # @param config [Stoplight::Domain::Config]
101
+ # @return [Stoplight::Domain::LockToken, nil] Lock if acquired, nil if contended
102
+ def acquire_recovery_lock(config) = raise NotImplementedError
103
+
104
+ # Releases previously acquired lock.
105
+ #
106
+ # @param lock [Stoplight::Domain::RecoveryLockToken]
107
+ # @return [void]
108
+ def release_recovery_lock(lock) = raise NotImplementedError
109
+
94
110
  # Transitions the Stoplight to the specified color.
95
111
  #
96
112
  # This method performs a color transition operation that works across distributed instances
@@ -15,9 +15,22 @@ module Stoplight
15
15
  #
16
16
  # @param data_store [DataStore::Base]
17
17
  # @return [Stoplight::Light]
18
- # @deprecated consider using +Light#with+ for reconfiguration
18
+ # @deprecated
19
19
  def with_data_store(data_store)
20
- with(data_store:)
20
+ deprecate(<<~MSG)
21
+ Light#with_data_store is deprecated and will be removed in v6.0.0.
22
+
23
+ Circuit breakers should be configured once at creation, not cloned with
24
+ modifications.
25
+
26
+ Instead of:
27
+ light = Stoplight('api-call')
28
+ modified = light.with_data_store(data_stare)
29
+
30
+ Configure correctly from the start:
31
+ Stoplight('api-call', data_store:)
32
+ MSG
33
+ with_without_warning(data_store:)
21
34
  end
22
35
 
23
36
  # Configures cool off time. Stoplight automatically tries to recover
@@ -29,9 +42,22 @@ module Stoplight
29
42
  #
30
43
  # @param cool_off_time [Numeric] number of seconds
31
44
  # @return [Stoplight::Light]
32
- # @deprecated consider using +Light#with+ for reconfiguration
45
+ # @deprecated
33
46
  def with_cool_off_time(cool_off_time)
34
- with(cool_off_time:)
47
+ deprecate(<<~MSG)
48
+ Light#with_cool_off_time is deprecated and will be removed in v6.0.0.
49
+
50
+ Circuit breakers should be configured once at creation, not cloned with
51
+ modifications.
52
+
53
+ Instead of:
54
+ light = Stoplight('api-call')
55
+ modified = light.with_cool_off_time(cool_off_time)
56
+
57
+ Configure correctly from the start:
58
+ Stoplight('api-call', cool_off_time:)
59
+ MSG
60
+ with_without_warning(cool_off_time:)
35
61
  end
36
62
 
37
63
  # Configures custom threshold. After this number of failures Stoplight
@@ -43,9 +69,22 @@ module Stoplight
43
69
  #
44
70
  # @param threshold [Numeric]
45
71
  # @return [Stoplight::Light]
46
- # @deprecated consider using +Light#with+ for reconfiguration
72
+ # @deprecated
47
73
  def with_threshold(threshold)
48
- with(threshold:)
74
+ deprecate(<<~MSG)
75
+ Light#with_threshold is deprecated and will be removed in v6.0.0.
76
+
77
+ Circuit breakers should be configured once at creation, not cloned with
78
+ modifications.
79
+
80
+ Instead of:
81
+ light = Stoplight('api-call')
82
+ modified = light.with_threshold(threshold)
83
+
84
+ Configure correctly from the start:
85
+ Stoplight('api-call', threshold:)
86
+ MSG
87
+ with_without_warning(threshold:)
49
88
  end
50
89
 
51
90
  # Configures custom window size which Stoplight uses to count failures. For example,
@@ -60,9 +99,22 @@ module Stoplight
60
99
  #
61
100
  # @param window_size [Numeric] number of seconds
62
101
  # @return [Stoplight::Light]
63
- # @deprecated consider using +Light#with+ for reconfiguration
102
+ # @deprecated
64
103
  def with_window_size(window_size)
65
- with(window_size:)
104
+ deprecate(<<~MSG)
105
+ Light#with_window_size is deprecated and will be removed in v6.0.0.
106
+
107
+ Circuit breakers should be configured once at creation, not cloned with
108
+ modifications.
109
+
110
+ Instead of:
111
+ light = Stoplight('api-call')
112
+ modified = light.with_window_size(window_size)
113
+
114
+ Configure correctly from the start:
115
+ Stoplight('api-call', window_size:)
116
+ MSG
117
+ with_without_warning(window_size:)
66
118
  end
67
119
 
68
120
  # Configures custom notifier
@@ -75,17 +127,43 @@ module Stoplight
75
127
  #
76
128
  # @param notifiers [Array<Notifier::Base>]
77
129
  # @return [Stoplight::Light]
78
- # @deprecated consider using +Light#with+ for reconfiguration
130
+ # @deprecated
79
131
  def with_notifiers(notifiers)
80
- with(notifiers:)
132
+ deprecate(<<~MSG)
133
+ Light#with_notifiers is deprecated and will be removed in v6.0.0.
134
+
135
+ Circuit breakers should be configured once at creation, not cloned with
136
+ modifications.
137
+
138
+ Instead of:
139
+ light = Stoplight('api-call')
140
+ modified = light.with_notifiers(notifiers)
141
+
142
+ Configure correctly from the start:
143
+ Stoplight('api-call', notifiers:)
144
+ MSG
145
+ with_without_warning(notifiers:)
81
146
  end
82
147
 
83
148
  # @param error_notifier [Proc]
84
149
  # @return [Stoplight::Light]
85
150
  # @api private
86
- # @deprecated consider using +Light#with+ for reconfiguration
151
+ # @deprecated
87
152
  def with_error_notifier(&error_notifier)
88
- with(error_notifier: error_notifier)
153
+ deprecate(<<~MSG)
154
+ Light#with_error_notifier is deprecated and will be removed in v6.0.0.
155
+
156
+ Circuit breakers should be configured once at creation, not cloned with
157
+ modifications.
158
+
159
+ Instead of:
160
+ light = Stoplight('api-call')
161
+ modified = light.with_error_notifier { |error| warn error }
162
+
163
+ Configure correctly from the start:
164
+ Stoplight('api-call', error_notifier: ->(error) { warn error })
165
+ MSG
166
+ with_without_warning(error_notifier: error_notifier)
89
167
  end
90
168
 
91
169
  # Configures a custom list of tracked errors that counts toward the threshold.
@@ -101,9 +179,22 @@ module Stoplight
101
179
  #
102
180
  # @param tracked_errors [Array<StandardError>]
103
181
  # @return [Stoplight::Light]
104
- # @deprecated consider using +Light#with+ for reconfiguration
182
+ # @deprecated
105
183
  def with_tracked_errors(*tracked_errors)
106
- with(tracked_errors:)
184
+ deprecate(<<~MSG)
185
+ Light#with_tracked_errors is deprecated and will be removed in v6.0.0.
186
+
187
+ Circuit breakers should be configured once at creation, not cloned with
188
+ modifications.
189
+
190
+ Instead of:
191
+ light = Stoplight('api-call')
192
+ modified = light.with_tracked_errors(TimeoutError, NetworkError)
193
+
194
+ Configure correctly from the start:
195
+ Stoplight('api-call', tracked_errors: [TimeoutError, NetworkError])
196
+ MSG
197
+ with_without_warning(tracked_errors:)
107
198
  end
108
199
 
109
200
  # Configures a custom list of skipped errors that do not count toward the threshold.
@@ -120,9 +211,22 @@ module Stoplight
120
211
  #
121
212
  # @param skipped_errors [Array<Exception>]
122
213
  # @return [Stoplight::Light]
123
- # @deprecated consider using +Light#with+ for reconfiguration
214
+ # @deprecated
124
215
  def with_skipped_errors(*skipped_errors)
125
- with(skipped_errors:)
216
+ deprecate(<<~MSG)
217
+ Light#with_skipped_errors is deprecated and will be removed in v6.0.0.
218
+
219
+ Circuit breakers should be configured once at creation, not cloned with
220
+ modifications.
221
+
222
+ Instead of:
223
+ light = Stoplight('api-call')
224
+ modified = light.with_skipped_errors(ActiveRecord::RecordNotFound)
225
+
226
+ Configure correctly from the start:
227
+ Stoplight('api-call', skipped_errors: [ActiveRecord::RecordNotFound])
228
+ MSG
229
+ with_without_warning(skipped_errors:)
126
230
  end
127
231
  end
128
232
  end
@@ -8,6 +8,7 @@ module Stoplight
8
8
  # @api private use +Stoplight()+ method instead
9
9
  class Light
10
10
  extend Forwardable
11
+ include Common::Deprecations
11
12
  include ConfigurationBuilderInterface
12
13
 
13
14
  # @!attribute [r] config
@@ -32,22 +33,22 @@ module Stoplight
32
33
  # @return [Stoplight::Domain::Strategies::RedRunStrategy]
33
34
  protected attr_reader :red_run_strategy
34
35
 
35
- # @!attribute [r] data_store
36
- # @return [Stoplight::Light::Base]
37
- protected attr_reader :data_store
38
-
39
36
  # @!attribute [r] factory
40
37
  # @return [Stoplight::Domain::LightFactory]
41
38
  protected attr_reader :factory
42
39
 
40
+ # @!attribute state_store
41
+ # @param [Stoplight::Domain::Storage::State]
42
+ protected attr_reader :state_store
43
+
43
44
  # @param config [Stoplight::Domain::Config]
44
- def initialize(config, green_run_strategy:, yellow_run_strategy:, red_run_strategy:, data_store:, factory:)
45
+ def initialize(config, green_run_strategy:, yellow_run_strategy:, red_run_strategy:, factory:, state_store:)
45
46
  @config = config
46
- @data_store = data_store
47
47
  @green_run_strategy = green_run_strategy
48
48
  @yellow_run_strategy = yellow_run_strategy
49
49
  @red_run_strategy = red_run_strategy
50
50
  @factory = factory
51
+ @state_store = state_store
51
52
  end
52
53
 
53
54
  # Returns the current state of the light:
@@ -56,9 +57,7 @@ module Stoplight
56
57
  # * +Stoplight::State::UNLOCKED+ -- light is not locked and follow the configured rules
57
58
  #
58
59
  # @return [String]
59
- def state
60
- state_snapshot.locked_state
61
- end
60
+ def state = state_snapshot.locked_state
62
61
 
63
62
  # Returns current color:
64
63
  # * +Stoplight::Color::GREEN+ -- circuit breaker is closed
@@ -70,9 +69,7 @@ module Stoplight
70
69
  # light.color #=> Color::GREEN
71
70
  #
72
71
  # @return [String] returns current light color
73
- def color
74
- state_snapshot.color
75
- end
72
+ def color = state_snapshot.color
76
73
 
77
74
  # Runs the given block of code with this circuit breaker
78
75
  #
@@ -112,7 +109,7 @@ module Stoplight
112
109
  else raise Error::IncorrectColor
113
110
  end
114
111
 
115
- data_store.set_state(config, state)
112
+ state_store.set_state(state)
116
113
 
117
114
  self
118
115
  end
@@ -126,7 +123,7 @@ module Stoplight
126
123
  #
127
124
  # @return [Stoplight::Light] returns unlocked light (circuit breaker)
128
125
  def unlock
129
- data_store.set_state(config, State::UNLOCKED)
126
+ state_store.set_state(State::UNLOCKED)
130
127
 
131
128
  self
132
129
  end
@@ -136,9 +133,7 @@ module Stoplight
136
133
  # @param other [any]
137
134
  # @return [Boolean]
138
135
  def ==(other)
139
- other.is_a?(self.class) && config == other.config && data_store == other.data_store &&
140
- green_run_strategy == other.green_run_strategy && yellow_run_strategy == other.yellow_run_strategy &&
141
- red_run_strategy == other.red_run_strategy && factory == other.factory
136
+ other.is_a?(self.class) && factory == other.factory
142
137
  end
143
138
 
144
139
  # Reconfigures the light with updated settings and returns a new instance.
@@ -171,8 +166,26 @@ module Stoplight
171
166
  # # Run the lights with their respective configurations
172
167
  # invoices_light.run(->(error) { [] }) { call_invoices_api }
173
168
  # payment_light.run(->(error) { nil }) { call_payment_api }
169
+ # @deprecated
174
170
  # @see +Stoplight()+
175
171
  def with(**settings)
172
+ deprecate(<<~MSG)
173
+ Light#with is deprecated and will be removed in v6.0.0.
174
+
175
+ Circuit breakers should be configured once at creation, not cloned with
176
+ modifications.
177
+
178
+ Instead of:
179
+ light = Stoplight('api-call', threshold: 5)
180
+ modified = light.with(threshold: 10)
181
+
182
+ Configure correctly from the start:
183
+ Stoplight('api-call', threshold: 10)
184
+ MSG
185
+ with_without_warning(**settings)
186
+ end
187
+
188
+ private def with_without_warning(**settings)
176
189
  factory.build_with(**settings)
177
190
  end
178
191
 
@@ -189,9 +202,7 @@ module Stoplight
189
202
  end
190
203
  end
191
204
 
192
- def state_snapshot
193
- data_store.get_state_snapshot(config)
194
- end
205
+ def state_snapshot = state_store.state_snapshot
195
206
  end
196
207
  end
197
208
  end
@@ -12,12 +12,12 @@ module Stoplight
12
12
  # A number of errors withing requested window. Zero for non-windowed metrics
13
13
  # @return [Integer]
14
14
  #
15
- # @!attribute total_consecutive_errors
16
- # A total number of consecutive errors
15
+ # @!attribute consecutive_errors
16
+ # A number of consecutive errors
17
17
  # @return [Integer]
18
18
  #
19
- # @!attribute total_consecutive_successes
20
- # A total number of consecutive successes
19
+ # @!attribute consecutive_successes
20
+ # A number of consecutive successes
21
21
  # @return [Integer]
22
22
  #
23
23
  # @!attribute last_error
@@ -30,32 +30,11 @@ module Stoplight
30
30
  Metrics = Data.define(
31
31
  :successes,
32
32
  :errors,
33
- :total_consecutive_errors,
34
- :total_consecutive_successes,
33
+ :consecutive_errors,
34
+ :consecutive_successes,
35
35
  :last_error,
36
36
  :last_success_at
37
37
  ) do
38
- # A number of consecutive errors withing requested window
39
- #
40
- # @return [Integer]
41
- def consecutive_errors
42
- if errors # we effectively check if this is windowed metrics
43
- [total_consecutive_errors, errors].min
44
- else
45
- total_consecutive_errors
46
- end
47
- end
48
-
49
- # A number of consecutive successes withing requested window
50
- #
51
- def consecutive_successes
52
- if successes # we effectively check if this is windowed metrics
53
- [total_consecutive_successes, successes].min
54
- else
55
- total_consecutive_successes
56
- end
57
- end
58
-
59
38
  # Calculates the error rate based on the number of successes and errors.
60
39
  #
61
40
  # @return [Float]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ # Token representing an acquired recovery lock.
6
+ #
7
+ # Returned by +DataStore#acquire_recovery_lock+ and passed to
8
+ # +DataStore#release_recovery_lock+ to identify which lock to release.
9
+ #
10
+ # The actual locking mechanism lives in DataStore implementations,
11
+ # not in these tokens.
12
+ class RecoveryLockToken
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Storage
6
+ # Encapsulates metrics storage for circuit breaker execution tracking.
7
+ #
8
+ # This abstraction isolates metrics collection and retrieval from the
9
+ # broader data store concerns, enabling:
10
+ # - Purpose-built implementations optimized for time-series data
11
+ # - Independent scaling and optimization of metrics vs. state storage
12
+ # - Clearer separation between "what happened" (metrics) and "what to do" (state)
13
+ #
14
+ # Lifecycle: A Metrics instance is scoped to a single circuit breaker
15
+ # configuration. Each circuit gets its own metrics store instance,
16
+ # allowing different circuits to use different storage strategies.
17
+ #
18
+ # @abstract
19
+ class Metrics
20
+ # Retrieves a snapshot of current metrics for decision-making.
21
+ #
22
+ # @return [Stoplight::Domain::Metrics]
23
+ def metrics_snapshot = raise NotImplementedError
24
+
25
+ # Records a successful circuit breaker execution
26
+ #
27
+ # @return [void]
28
+ def record_success = raise NotImplementedError
29
+
30
+ # Records a failed circuit breaker execution
31
+ #
32
+ # @param error [StandardError]
33
+ # @return [void]
34
+ def record_failure(error) = raise NotImplementedError
35
+
36
+ # Clears all metrics for this circuit
37
+ # @return [void]
38
+ def clear = raise NotImplementedError
39
+ end
40
+ end
41
+ end
42
+ end