stoplight 5.5.0 → 5.6.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 (33) 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 +5 -0
  5. data/lib/stoplight/admin/lights_repository.rb +12 -3
  6. data/lib/stoplight/admin/views/_card.erb +13 -1
  7. data/lib/stoplight/admin.rb +8 -0
  8. data/lib/stoplight/domain/data_store.rb +42 -6
  9. data/lib/stoplight/domain/failure.rb +2 -0
  10. data/lib/stoplight/domain/light.rb +7 -8
  11. data/lib/stoplight/domain/metrics.rb +85 -0
  12. data/lib/stoplight/domain/{metadata.rb → state_snapshot.rb} +29 -37
  13. data/lib/stoplight/domain/strategies/green_run_strategy.rb +2 -2
  14. data/lib/stoplight/domain/strategies/red_run_strategy.rb +3 -3
  15. data/lib/stoplight/domain/strategies/run_strategy.rb +2 -2
  16. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +7 -6
  17. data/lib/stoplight/domain/tracker/recovery_probe.rb +9 -6
  18. data/lib/stoplight/domain/tracker/request.rb +5 -4
  19. data/lib/stoplight/domain/traffic_control/base.rb +5 -5
  20. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +3 -7
  21. data/lib/stoplight/domain/traffic_control/error_rate.rb +3 -3
  22. data/lib/stoplight/domain/traffic_recovery/base.rb +6 -5
  23. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +8 -6
  24. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  25. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  26. data/lib/stoplight/infrastructure/data_store/memory.rb +125 -123
  27. data/lib/stoplight/infrastructure/data_store/redis/get_metrics.lua +26 -0
  28. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +1 -1
  29. data/lib/stoplight/infrastructure/data_store/redis.rb +115 -40
  30. data/lib/stoplight/version.rb +1 -1
  31. data/lib/stoplight/wiring/fail_safe_data_store.rb +27 -3
  32. metadata +7 -3
  33. data/lib/stoplight/infrastructure/data_store/redis/get_metadata.lua +0 -38
@@ -92,12 +92,13 @@ module Stoplight
92
92
  end
93
93
  end
94
94
 
95
- def get_metadata(config)
96
- detect_clock_skew
95
+ # @param config [Stoplight::Domain::Config]
96
+ # @return [Stoplight::Domain::Metrics]
97
+ def get_metrics(config)
98
+ config.name
97
99
 
98
100
  window_end_ts = current_time.to_f
99
- window_start_ts = window_end_ts - [config.window_size, METRICS_RETENTION_TIME].compact.min.to_f
100
- recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
101
+ window_start_ts = window_end_ts - config.window_size.to_i
101
102
 
102
103
  if config.window_size
103
104
  failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
@@ -106,54 +107,121 @@ module Stoplight
106
107
  failure_keys = []
107
108
  success_keys = []
108
109
  end
110
+
111
+ successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
112
+ client.evalsha(
113
+ get_metrics_sha,
114
+ argv: [
115
+ failure_keys.count,
116
+ window_start_ts,
117
+ window_end_ts,
118
+ "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
119
+ ],
120
+ keys: [
121
+ metadata_key(config),
122
+ *success_keys,
123
+ *failure_keys
124
+ ]
125
+ )
126
+ end
127
+
128
+ Domain::Metrics.new(
129
+ successes: (successes if config.window_size),
130
+ errors: (errors if config.window_size),
131
+ total_consecutive_errors: consecutive_errors.to_i,
132
+ total_consecutive_successes: consecutive_successes.to_i,
133
+ last_error: deserialize_failure(last_error_json),
134
+ last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
135
+ )
136
+ end
137
+
138
+ # @param config [Stoplight::Domain::Config]
139
+ # @return [Stoplight::Domain::Metrics]
140
+ def get_recovery_metrics(config)
141
+ config.name
142
+
143
+ window_end_ts = current_time.to_f
144
+ window_start_ts = window_end_ts - config.cool_off_time
145
+
109
146
  recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
110
147
  recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
111
148
 
112
- successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
149
+ successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = @redis.with do |client|
113
150
  client.evalsha(
114
- get_metadata_sha,
151
+ get_metrics_sha,
115
152
  argv: [
116
- failure_keys.count,
117
153
  recovery_probe_failure_keys.count,
118
154
  window_start_ts,
119
155
  window_end_ts,
120
- recovery_window_start_ts
156
+ "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
121
157
  ],
122
158
  keys: [
123
159
  metadata_key(config),
124
- *success_keys,
125
- *failure_keys,
126
160
  *recovery_probe_success_keys,
127
161
  *recovery_probe_failure_keys
128
162
  ]
129
163
  )
130
164
  end
131
- meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
132
- last_error_json = meta_hash.delete(:last_error_json)
133
- last_error = deserialize_failure(last_error_json) if last_error_json
134
165
 
135
- Domain::Metadata.new(
136
- current_time:,
166
+ Domain::Metrics.new(
137
167
  successes:,
138
168
  errors:,
139
- recovery_probe_successes:,
140
- recovery_probe_errors:,
141
- last_error:,
142
- last_error_at: (Time.at(meta_hash[:last_error_at].to_f) if meta_hash[:last_error_at]),
143
- last_success_at: (Time.at(meta_hash[:last_success_at].to_f) if meta_hash[:last_success_at]),
144
- consecutive_errors: meta_hash[:consecutive_errors].to_i,
145
- consecutive_successes: meta_hash[:consecutive_successes].to_i,
146
- breached_at: (Time.at(meta_hash[:breached_at].to_f) if meta_hash[:breached_at]),
147
- locked_state: meta_hash[:locked_state] || Domain::State::UNLOCKED,
148
- recovery_scheduled_after: (Time.at(meta_hash[:recovery_scheduled_after].to_f) if meta_hash[:recovery_scheduled_after]),
149
- recovery_started_at: (Time.at(meta_hash[:recovery_started_at].to_f) if meta_hash[:recovery_started_at]),
150
- recovered_at: (Time.at(meta_hash[:recovered_at].to_f) if meta_hash[:recovered_at])
169
+ total_consecutive_errors: consecutive_errors.to_i,
170
+ total_consecutive_successes: consecutive_successes.to_i,
171
+ last_error: deserialize_failure(last_error_json),
172
+ last_success_at: (Time.at(last_success_at.to_f) if last_success_at)
173
+ )
174
+ end
175
+
176
+ # @return [Stoplight::Domain::StateSnapshot]
177
+ def get_state_snapshot(config)
178
+ detect_clock_skew
179
+
180
+ breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw = @redis.with do |client|
181
+ client.hmget(metadata_key(config), :breached_at, :locked_state, :recovery_scheduled_after, :recovery_started_at)
182
+ end
183
+ breached_at = breached_at_raw&.to_f
184
+ recovery_scheduled_after = recovery_scheduled_after_raw&.to_f
185
+ recovery_started_at = recovery_started_at_raw&.to_f
186
+
187
+ Domain::StateSnapshot.new(
188
+ breached_at: (Time.at(breached_at) if breached_at),
189
+ locked_state: locked_state || Domain::State::UNLOCKED,
190
+ recovery_scheduled_after: (Time.at(recovery_scheduled_after) if recovery_scheduled_after),
191
+ recovery_started_at: (Time.at(recovery_started_at) if recovery_started_at),
192
+ time: current_time
193
+ )
194
+ end
195
+
196
+ def clear_windowed_metrics(config)
197
+ if config.window_size
198
+ window_end_ts = current_time.to_i
199
+ @redis.with do |client|
200
+ client.unlink(
201
+ *failure_bucket_keys(config, window_end: window_end_ts),
202
+ *success_bucket_keys(config, window_end: window_end_ts)
203
+ )
204
+ end
205
+ end
206
+ end
207
+
208
+ private def state_snapshot_from_hash(data, time: current_time)
209
+ breached_at = data[:breached_at]&.to_f
210
+ recovery_scheduled_after = data[:recovery_scheduled_after]&.to_f
211
+ recovery_started_at = data[:recovery_started_at]&.to_f
212
+
213
+ Domain::StateSnapshot.new(
214
+ breached_at: (Time.at(breached_at) if breached_at),
215
+ locked_state: data[:locked_state] || Domain::State::UNLOCKED,
216
+ recovery_scheduled_after: (Time.at(recovery_scheduled_after) if recovery_scheduled_after),
217
+ recovery_started_at: (Time.at(recovery_started_at) if recovery_started_at),
218
+ time:
151
219
  )
152
220
  end
153
221
 
154
222
  # @param config [Stoplight::Domain::Config] The light configuration.
155
223
  # @param exception [Exception]
156
- # @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
224
+ # @return [void]
157
225
  def record_failure(config, exception)
158
226
  current_time = self.current_time
159
227
  current_ts = current_time.to_f
@@ -169,7 +237,6 @@ module Stoplight
169
237
  ].compact
170
238
  )
171
239
  end
172
- get_metadata(config)
173
240
  end
174
241
 
175
242
  def record_success(config, request_id: SecureRandom.hex(12))
@@ -191,7 +258,7 @@ module Stoplight
191
258
  #
192
259
  # @param config [Stoplight::Domain::Config] The light configuration.
193
260
  # @param exception [Exception]
194
- # @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
261
+ # @return [void]
195
262
  def record_recovery_probe_failure(config, exception)
196
263
  current_time = self.current_time
197
264
  current_ts = current_time.to_f
@@ -207,14 +274,13 @@ module Stoplight
207
274
  ].compact
208
275
  )
209
276
  end
210
- get_metadata(config)
211
277
  end
212
278
 
213
279
  # Records a successful recovery probe for a specific light configuration.
214
280
  #
215
281
  # @param config [Stoplight::Domain::Config] The light configuration.
216
282
  # @param request_id [String] The unique identifier for the request
217
- # @return [Stoplight::Domain::Metadata] The updated metadata after recording the success.
283
+ # @return [void]
218
284
  def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
219
285
  current_ts = current_time.to_f
220
286
 
@@ -228,7 +294,6 @@ module Stoplight
228
294
  ].compact
229
295
  )
230
296
  end
231
- get_metadata(config)
232
297
  end
233
298
 
234
299
  def set_state(config, state)
@@ -283,7 +348,7 @@ module Stoplight
283
348
  # @param config [Stoplight::Domain::Config] The light configuration
284
349
  # @return [Boolean] true if this is the first instance to detect this transition
285
350
  private def transition_to_yellow(config)
286
- current_ts = current_time.to_i
351
+ current_ts = current_time.to_f
287
352
  meta_key = metadata_key(config)
288
353
 
289
354
  became_yellow = @redis.then do |client|
@@ -301,7 +366,7 @@ module Stoplight
301
366
  # @param config [Stoplight::Domain::Config] The light configuration
302
367
  # @return [Boolean] true if this is the first instance to detect this transition
303
368
  private def transition_to_red(config)
304
- current_ts = current_time.to_i
369
+ current_ts = current_time.to_f
305
370
  meta_key = metadata_key(config)
306
371
  recovery_scheduled_after_ts = current_ts + config.cool_off_time
307
372
 
@@ -316,9 +381,19 @@ module Stoplight
316
381
  became_red == 1
317
382
  end
318
383
 
319
- # @param failure_json [String]
320
- # @return [Domain::Failure]
384
+ # Removes all traces of a light from Redis metadata (metrics will expire by TTL).
385
+ #
386
+ # @param config [Stoplight::Domain::Config] The light configuration.
387
+ # @return [Integer] number of keys removed
388
+ def delete_light(config)
389
+ @redis.then { |client| client.del(metadata_key(config)) }
390
+ end
391
+
392
+ # @param failure_json [String, nil]
393
+ # @return [Domain::Failure, nil]
321
394
  private def deserialize_failure(failure_json)
395
+ return if failure_json.nil?
396
+
322
397
  object = JSON.parse(failure_json)
323
398
  error_object = object["error"]
324
399
 
@@ -439,9 +514,9 @@ module Stoplight
439
514
  end
440
515
  end
441
516
 
442
- private def get_metadata_sha
443
- @get_metadata_sha ||= @redis.then do |client|
444
- client.script("load", Lua::GET_METADATA)
517
+ private def get_metrics_sha
518
+ @get_metrics_sha ||= @redis.then do |client|
519
+ client.script("load", Lua::GET_METRICS)
445
520
  end
446
521
  end
447
522
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- VERSION = Gem::Version.new("5.5.0")
4
+ VERSION = Gem::Version.new("5.6.0")
5
5
  end
@@ -62,9 +62,27 @@ module Stoplight
62
62
  end
63
63
  end
64
64
 
65
- def get_metadata(config, *args, **kwargs)
66
- with_fallback(:get_metadata, config, *args, **kwargs) do
67
- data_store.get_metadata(config, *args, **kwargs)
65
+ def get_metrics(config, *args, **kwargs)
66
+ with_fallback(:get_metrics, config, *args, **kwargs) do
67
+ data_store.get_metrics(config, *args, **kwargs)
68
+ end
69
+ end
70
+
71
+ def get_recovery_metrics(config, *args, **kwargs)
72
+ with_fallback(:get_recovery_metrics, config, *args, **kwargs) do
73
+ data_store.get_recovery_metrics(config, *args, **kwargs)
74
+ end
75
+ end
76
+
77
+ def get_state_snapshot(config)
78
+ with_fallback(:get_state_snapshot, config) do
79
+ data_store.get_state_snapshot(config)
80
+ end
81
+ end
82
+
83
+ def clear_windowed_metrics(config)
84
+ with_fallback(:clear_windowed_metrics, config) do
85
+ data_store.clear_windowed_metrics(config)
68
86
  end
69
87
  end
70
88
 
@@ -104,6 +122,12 @@ module Stoplight
104
122
  end
105
123
  end
106
124
 
125
+ def delete_light(config, *args, **kwargs)
126
+ with_fallback(:delete_light, config, *args, **kwargs) do
127
+ data_store.delete_light(config, *args, **kwargs)
128
+ end
129
+ end
130
+
107
131
  def ==(other)
108
132
  other.is_a?(self.class) && other.data_store == data_store && other.error_notifier == error_notifier
109
133
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stoplight
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.0
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Desautels
@@ -47,6 +47,7 @@ files:
47
47
  - lib/stoplight/admin/actions/lock_all_green.rb
48
48
  - lib/stoplight/admin/actions/lock_green.rb
49
49
  - lib/stoplight/admin/actions/lock_red.rb
50
+ - lib/stoplight/admin/actions/remove.rb
50
51
  - lib/stoplight/admin/actions/stats.rb
51
52
  - lib/stoplight/admin/actions/unlock.rb
52
53
  - lib/stoplight/admin/dependencies.rb
@@ -66,8 +67,9 @@ files:
66
67
  - lib/stoplight/domain/light.rb
67
68
  - lib/stoplight/domain/light/configuration_builder_interface.rb
68
69
  - lib/stoplight/domain/light_factory.rb
69
- - lib/stoplight/domain/metadata.rb
70
+ - lib/stoplight/domain/metrics.rb
70
71
  - lib/stoplight/domain/state.rb
72
+ - lib/stoplight/domain/state_snapshot.rb
71
73
  - lib/stoplight/domain/state_transition_notifier.rb
72
74
  - lib/stoplight/domain/strategies/green_run_strategy.rb
73
75
  - lib/stoplight/domain/strategies/red_run_strategy.rb
@@ -83,9 +85,11 @@ files:
83
85
  - lib/stoplight/domain/traffic_recovery/base.rb
84
86
  - lib/stoplight/domain/traffic_recovery/consecutive_successes.rb
85
87
  - lib/stoplight/infrastructure/data_store/memory.rb
88
+ - lib/stoplight/infrastructure/data_store/memory/metrics.rb
86
89
  - lib/stoplight/infrastructure/data_store/memory/sliding_window.rb
90
+ - lib/stoplight/infrastructure/data_store/memory/state.rb
87
91
  - lib/stoplight/infrastructure/data_store/redis.rb
88
- - lib/stoplight/infrastructure/data_store/redis/get_metadata.lua
92
+ - lib/stoplight/infrastructure/data_store/redis/get_metrics.lua
89
93
  - lib/stoplight/infrastructure/data_store/redis/lua.rb
90
94
  - lib/stoplight/infrastructure/data_store/redis/record_failure.lua
91
95
  - lib/stoplight/infrastructure/data_store/redis/record_success.lua
@@ -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}