stoplight 5.3.8 → 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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -2
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/actions/remove.rb +23 -0
  5. data/lib/stoplight/admin/dependencies.rb +6 -1
  6. data/lib/stoplight/admin/helpers.rb +10 -5
  7. data/lib/stoplight/admin/lights_repository.rb +26 -14
  8. data/lib/stoplight/admin/views/_card.erb +13 -1
  9. data/lib/stoplight/admin/views/layout.erb +3 -3
  10. data/lib/stoplight/admin.rb +13 -4
  11. data/lib/stoplight/common/deprecations.rb +11 -0
  12. data/lib/stoplight/domain/color.rb +11 -0
  13. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  14. data/lib/stoplight/domain/config.rb +59 -0
  15. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +71 -17
  16. data/lib/stoplight/domain/error.rb +42 -0
  17. data/lib/stoplight/domain/failure.rb +44 -0
  18. data/lib/stoplight/domain/light/configuration_builder_interface.rb +234 -0
  19. data/lib/stoplight/domain/light.rb +208 -0
  20. data/lib/stoplight/domain/light_factory.rb +75 -0
  21. data/lib/stoplight/domain/metrics.rb +64 -0
  22. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  23. data/lib/stoplight/domain/state.rb +11 -0
  24. data/lib/stoplight/domain/state_snapshot.rb +57 -0
  25. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  26. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  27. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  28. data/lib/stoplight/domain/storage/state.rb +87 -0
  29. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  30. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  31. data/lib/stoplight/domain/strategies/run_strategy.rb +22 -0
  32. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +125 -0
  33. data/lib/stoplight/domain/tracker/base.rb +12 -0
  34. data/lib/stoplight/domain/tracker/recovery_probe.rb +76 -0
  35. data/lib/stoplight/domain/tracker/request.rb +72 -0
  36. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  37. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +53 -0
  38. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  39. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  40. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +66 -0
  41. data/lib/stoplight/domain/traffic_recovery.rb +12 -0
  42. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  43. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  44. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  45. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  46. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  47. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  48. data/lib/stoplight/infrastructure/data_store/memory.rb +338 -0
  49. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/get_metrics.lua +26 -0
  50. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  51. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  52. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  53. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  54. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  55. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  56. data/lib/stoplight/infrastructure/data_store/redis.rb +524 -0
  57. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  58. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  59. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  60. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  61. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  62. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  63. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  64. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  65. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  66. data/lib/stoplight/version.rb +1 -1
  67. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  68. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  69. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  70. data/lib/stoplight/wiring/default.rb +28 -0
  71. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  72. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  73. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  74. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  75. data/lib/stoplight/wiring/light_builder.rb +185 -0
  76. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  77. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  78. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  79. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  80. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  81. data/lib/stoplight/wiring/light_factory.rb +101 -0
  82. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  83. data/lib/stoplight/wiring/public_api.rb +29 -0
  84. data/lib/stoplight.rb +55 -30
  85. metadata +92 -42
  86. data/lib/stoplight/color.rb +0 -9
  87. data/lib/stoplight/config/dsl.rb +0 -97
  88. data/lib/stoplight/config/library_default_config.rb +0 -21
  89. data/lib/stoplight/config/system_config.rb +0 -7
  90. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  91. data/lib/stoplight/data_store/memory.rb +0 -311
  92. data/lib/stoplight/data_store/redis/get_metadata.lua +0 -38
  93. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  94. data/lib/stoplight/data_store/redis.rb +0 -449
  95. data/lib/stoplight/data_store.rb +0 -6
  96. data/lib/stoplight/default.rb +0 -30
  97. data/lib/stoplight/error.rb +0 -10
  98. data/lib/stoplight/failure.rb +0 -71
  99. data/lib/stoplight/light/config.rb +0 -111
  100. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  101. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  102. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  103. data/lib/stoplight/light/run_strategy.rb +0 -32
  104. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  105. data/lib/stoplight/light.rb +0 -191
  106. data/lib/stoplight/metadata.rb +0 -99
  107. data/lib/stoplight/notifier/fail_safe.rb +0 -70
  108. data/lib/stoplight/notifier/generic.rb +0 -79
  109. data/lib/stoplight/notifier/io.rb +0 -21
  110. data/lib/stoplight/notifier/logger.rb +0 -19
  111. data/lib/stoplight/state.rb +0 -9
  112. data/lib/stoplight/traffic_control/base.rb +0 -70
  113. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  114. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  115. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  116. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  117. data/lib/stoplight/traffic_recovery.rb +0 -11
  118. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_failure.lua +0 -0
  119. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_success.lua +0 -0
  120. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_green.lua +0 -0
  121. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_red.lua +0 -0
  122. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_yellow.lua +0 -0
@@ -1,311 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "monitor"
4
-
5
- module Stoplight
6
- module DataStore
7
- # @see Base
8
- class Memory < Base
9
- include MonitorMixin
10
-
11
- KEY_SEPARATOR = ":"
12
-
13
- def initialize
14
- @errors = Hash.new { |h, k| h[k] = [] }
15
- @successes = Hash.new { |h, k| h[k] = [] }
16
-
17
- @recovery_probe_errors = Hash.new { |h, k| h[k] = [] }
18
- @recovery_probe_successes = Hash.new { |h, k| h[k] = [] }
19
-
20
- @metadata = Hash.new { |h, k| h[k] = Metadata.new }
21
- super # MonitorMixin
22
- end
23
-
24
- # @return [Array<String>]
25
- def names
26
- synchronize { @metadata.keys }
27
- end
28
-
29
- # @param config [Stoplight::Light::Config]
30
- # @return [Stoplight::Metadata]
31
- def get_metadata(config)
32
- light_name = config.name
33
-
34
- synchronize do
35
- current_time = Time.now
36
- recovery_window = (current_time - config.cool_off_time)..current_time
37
- recovered_at = @metadata[light_name].recovered_at
38
- window = if config.window_size
39
- window_start = [recovered_at, (current_time - config.window_size)].compact.max
40
- (window_start..current_time)
41
- else
42
- (..current_time)
43
- end
44
-
45
- errors = @errors[config.name].count do |request_time|
46
- window.cover?(request_time)
47
- end
48
-
49
- successes = @successes[config.name].count do |request_time|
50
- window.cover?(request_time)
51
- end
52
-
53
- recovery_probe_errors = @recovery_probe_errors[config.name].count do |request_time|
54
- recovery_window.cover?(request_time)
55
- end
56
- recovery_probe_successes = @recovery_probe_successes[config.name].count do |request_time|
57
- recovery_window.cover?(request_time)
58
- end
59
-
60
- @metadata[light_name].with(
61
- current_time:,
62
- errors:,
63
- successes:,
64
- recovery_probe_errors:,
65
- recovery_probe_successes:
66
- )
67
- end
68
- end
69
-
70
- # @param metrics [<Time>]
71
- # @param window_size [Numeric, nil]
72
- # @return [void]
73
- def cleanup(metrics, window_size:)
74
- min_age = Time.now - [window_size&.*(3), METRICS_RETENTION_TIME].compact.min
75
-
76
- metrics.reject! { _1 < min_age }
77
- end
78
-
79
- # @param config [Stoplight::Light::Config]
80
- # @param failure [Stoplight::Failure]
81
- # @return [Stoplight::Metadata]
82
- def record_failure(config, failure)
83
- light_name = config.name
84
-
85
- synchronize do
86
- @errors[light_name].unshift(failure.time) if config.window_size
87
-
88
- cleanup(@errors[light_name], window_size: config.window_size)
89
-
90
- metadata = @metadata[light_name]
91
- @metadata[light_name] = if metadata.last_error_at.nil? || failure.time > metadata.last_error_at
92
- metadata.with(
93
- last_error_at: failure.time,
94
- last_error: failure,
95
- consecutive_errors: metadata.consecutive_errors.succ,
96
- consecutive_successes: 0
97
- )
98
- else
99
- metadata.with(
100
- consecutive_errors: metadata.consecutive_errors.succ,
101
- consecutive_successes: 0
102
- )
103
- end
104
- get_metadata(config)
105
- end
106
- end
107
-
108
- # @param config [Stoplight::Light::Config]
109
- # @param request_id [String]
110
- # @param request_time [Time]
111
- # @return [void]
112
- def record_success(config, request_time: Time.now, request_id: SecureRandom.hex(12))
113
- light_name = config.name
114
-
115
- synchronize do
116
- @successes[light_name].unshift(request_time) if config.window_size
117
- cleanup(@successes[light_name], window_size: config.window_size)
118
-
119
- metadata = @metadata[light_name]
120
- @metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
121
- metadata.with(
122
- last_success_at: request_time,
123
- consecutive_errors: 0,
124
- consecutive_successes: metadata.consecutive_successes.succ
125
- )
126
- else
127
- metadata.with(
128
- consecutive_errors: 0,
129
- consecutive_successes: metadata.consecutive_successes.succ
130
- )
131
- end
132
- end
133
- end
134
-
135
- # @param config [Stoplight::Light::Config]
136
- # @param failure [Stoplight::Failure]
137
- # @return [Stoplight::Metadata]
138
- def record_recovery_probe_failure(config, failure)
139
- light_name = config.name
140
-
141
- synchronize do
142
- @recovery_probe_errors[light_name].unshift(failure.time)
143
- cleanup(@recovery_probe_errors[light_name], window_size: config.cool_off_time)
144
-
145
- metadata = @metadata[light_name]
146
- @metadata[light_name] = if metadata.last_error_at.nil? || failure.time > metadata.last_error_at
147
- metadata.with(
148
- last_error_at: failure.time,
149
- last_error: failure,
150
- consecutive_errors: metadata.consecutive_errors.succ,
151
- consecutive_successes: 0
152
- )
153
- else
154
- metadata.with(
155
- consecutive_errors: metadata.consecutive_errors.succ,
156
- consecutive_successes: 0
157
- )
158
- end
159
- get_metadata(config)
160
- end
161
- end
162
-
163
- # @param config [Stoplight::Light::Config]
164
- # @param request_id [String]
165
- # @param request_time [Time]
166
- # @return [Stoplight::Metadata]
167
- def record_recovery_probe_success(config, request_time: Time.now, request_id: SecureRandom.hex(12))
168
- light_name = config.name
169
-
170
- synchronize do
171
- @recovery_probe_successes[light_name].unshift(request_time)
172
- cleanup(@recovery_probe_successes[light_name], window_size: config.cool_off_time)
173
-
174
- metadata = @metadata[light_name]
175
- @metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
176
- metadata.with(
177
- last_success_at: request_time,
178
- consecutive_errors: 0,
179
- consecutive_successes: metadata.consecutive_successes.succ
180
- )
181
- else
182
- metadata.with(
183
- consecutive_errors: 0,
184
- consecutive_successes: metadata.consecutive_successes.succ
185
- )
186
- end
187
- get_metadata(config)
188
- end
189
- end
190
-
191
- # @param config [Stoplight::Light::Config]
192
- # @param state [String]
193
- # @return [String]
194
- def set_state(config, state)
195
- light_name = config.name
196
-
197
- synchronize do
198
- metadata = @metadata[light_name]
199
- @metadata[light_name] = metadata.with(locked_state: state)
200
- end
201
- state
202
- end
203
-
204
- # @return [String]
205
- def inspect
206
- "#<#{self.class.name}>"
207
- end
208
-
209
- # Combined method that performs the state transition based on color
210
- #
211
- # @param config [Stoplight::Light::Config] The light configuration
212
- # @param color [String] The color to transition to ("GREEN", "YELLOW", or "RED")
213
- # @param current_time [Time]
214
- # @return [Boolean] true if this is the first instance to detect this transition
215
- def transition_to_color(config, color, current_time: Time.now)
216
- case color
217
- when Color::GREEN
218
- transition_to_green(config)
219
- when Color::YELLOW
220
- transition_to_yellow(config, current_time:)
221
- when Color::RED
222
- transition_to_red(config, current_time:)
223
- else
224
- raise ArgumentError, "Invalid color: #{color}"
225
- end
226
- end
227
-
228
- # Transitions to GREEN state and ensures only one notification
229
- #
230
- # @param config [Stoplight::Light::Config] The light configuration
231
- # @return [Boolean] true if this is the first instance to detect this transition
232
- private def transition_to_green(config, current_time: Time.now)
233
- light_name = config.name
234
-
235
- synchronize do
236
- metadata = @metadata[light_name]
237
- if metadata.recovered_at
238
- false
239
- else
240
- @metadata[light_name] = metadata.with(
241
- recovered_at: current_time,
242
- recovery_started_at: nil,
243
- breached_at: nil,
244
- recovery_scheduled_after: nil
245
- )
246
- true
247
- end
248
- end
249
- end
250
-
251
- # Transitions to YELLOW (recovery) state and ensures only one notification
252
- #
253
- # @param config [Stoplight::Light::Config] The light configuration
254
- # @param current_time [Time]
255
- # @return [Boolean] true if this is the first instance to detect this transition
256
- private def transition_to_yellow(config, current_time: Time.now)
257
- light_name = config.name
258
-
259
- synchronize do
260
- metadata = @metadata[light_name]
261
- if metadata.recovery_started_at.nil?
262
- @metadata[light_name] = metadata.with(
263
- recovery_started_at: current_time,
264
- recovery_scheduled_after: nil,
265
- recovered_at: nil,
266
- breached_at: nil
267
- )
268
- true
269
- else
270
- @metadata[light_name] = metadata.with(
271
- recovery_scheduled_after: nil,
272
- recovered_at: nil,
273
- breached_at: nil
274
- )
275
- false
276
- end
277
- end
278
- end
279
-
280
- # Transitions to RED state and ensures only one notification
281
- #
282
- # @param config [Stoplight::Light::Config] The light configuration
283
- # @param current_time [Time]
284
- # @return [Boolean] true if this is the first instance to detect this transition
285
- private def transition_to_red(config, current_time: Time.now)
286
- light_name = config.name
287
- recovery_scheduled_after = current_time + config.cool_off_time
288
-
289
- synchronize do
290
- metadata = @metadata[light_name]
291
- if metadata.breached_at
292
- @metadata[light_name] = metadata.with(
293
- recovery_scheduled_after: recovery_scheduled_after,
294
- recovery_started_at: nil,
295
- recovered_at: nil
296
- )
297
- false
298
- else
299
- @metadata[light_name] = metadata.with(
300
- breached_at: current_time,
301
- recovery_scheduled_after: recovery_scheduled_after,
302
- recovery_started_at: nil,
303
- recovered_at: nil
304
- )
305
- true
306
- end
307
- end
308
- end
309
- end
310
- end
311
- end
@@ -1,38 +0,0 @@
1
- local number_of_metric_buckets = tonumber(ARGV[1])
2
- local number_of_recovery_buckets = tonumber(ARGV[2])
3
- local window_start_ts = tonumber(ARGV[3])
4
- local window_end_ts = tonumber(ARGV[4])
5
- local recovery_window_start_ts = tonumber(ARGV[5])
6
-
7
- local metadata_key = KEYS[1]
8
-
9
- -- It possible that after a successful recovery, Stoplight still see metrics
10
- -- that are older than the recovery window. To prevent this from happening,
11
- -- we need to limit the start time of the window to the time of the last recovery.
12
- local recovered_at = redis.call('HGET', metadata_key, "recovered_at")
13
- if recovered_at then
14
- window_start_ts = math.max(window_start_ts, tonumber(recovered_at))
15
- end
16
-
17
- local function count_events(start_idx, bucket_count, start_ts)
18
- local total = 0
19
- for idx = start_idx, start_idx + bucket_count - 1 do
20
- total = total + tonumber(redis.call('ZCOUNT', KEYS[idx], start_ts, window_end_ts))
21
- end
22
- return total
23
- end
24
-
25
- local offset = 2
26
- local successes = count_events(2, number_of_metric_buckets, window_start_ts)
27
-
28
- offset = offset + number_of_metric_buckets
29
- local errors = count_events(offset, number_of_metric_buckets, window_start_ts)
30
-
31
- offset = offset + number_of_metric_buckets
32
- local recovery_probe_successes = count_events(offset, number_of_recovery_buckets, recovery_window_start_ts)
33
-
34
- offset = offset + number_of_recovery_buckets
35
- local recovery_probe_errors = count_events(offset, number_of_recovery_buckets, recovery_window_start_ts)
36
-
37
- local metadata = redis.call('HGETALL', metadata_key)
38
- return {successes, errors, recovery_probe_successes, recovery_probe_errors, metadata}
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stoplight
4
- module DataStore
5
- class Redis
6
- # @api private
7
- module Lua
8
- class << self
9
- def read_lua_file(name_without_extension)
10
- File.read(File.join(__dir__, "#{name_without_extension}.lua"))
11
- end
12
- end
13
-
14
- RECORD_FAILURE = read_lua_file("record_failure")
15
- RECORD_SUCCESS = read_lua_file("record_success")
16
- GET_METADATA = read_lua_file("get_metadata")
17
- TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
18
- TRANSITION_TO_RED = read_lua_file("transition_to_red")
19
- TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
20
- end
21
- end
22
- end
23
- end