stoplight 5.3.1 → 5.3.8
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.
- checksums.yaml +4 -4
- data/README.md +84 -15
- data/lib/stoplight/data_store/fail_safe.rb +3 -3
- data/lib/stoplight/data_store/memory.rb +15 -11
- data/lib/stoplight/data_store/redis/get_metadata.lua +1 -1
- data/lib/stoplight/data_store/redis.rb +7 -6
- data/lib/stoplight/light/green_run_strategy.rb +2 -1
- data/lib/stoplight/light/red_run_strategy.rb +2 -1
- data/lib/stoplight/light/run_strategy.rb +3 -1
- data/lib/stoplight/light/yellow_run_strategy.rb +21 -5
- data/lib/stoplight/light.rb +11 -10
- data/lib/stoplight/metadata.rb +27 -15
- data/lib/stoplight/traffic_recovery/base.rb +1 -4
- data/lib/stoplight/traffic_recovery/consecutive_successes.rb +6 -4
- data/lib/stoplight/traffic_recovery.rb +11 -0
- data/lib/stoplight/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e14702003b76fde01b28e5266b392f5d55b2924c83365a0ab122f11aebf2e4a7
|
4
|
+
data.tar.gz: e9c1b348b2637aa66408de62a2bbbc24b9b73927ca9712a8b4137446e336cd0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1601dd8a7fae2e9c8f89a52556c8f931d3712d71648300aef0e3dae226c9998b7b1fa35e7fa0ce151d943e961473e012ca382b718bb50b44000f5a43e4d667b8
|
7
|
+
data.tar.gz: 7c4e9696009e01bea1908cd3212d4625cc0ac3e03205d1cd69e4471f9aec4f3add7f101e58b393df210658e6be2c2d42b5d4a2ddedd954f832053505e291f232
|
data/README.md
CHANGED
@@ -3,9 +3,8 @@
|
|
3
3
|
[![Version badge][]][version]
|
4
4
|
[![Build badge][]][build]
|
5
5
|
[![Coverage badge][]][coverage]
|
6
|
-
[![Climate badge][]][climate]
|
7
6
|
|
8
|
-
Stoplight is traffic control for code. It's an implementation of the circuit breaker pattern in Ruby.
|
7
|
+
Stoplight is a traffic control for code. It's an implementation of the circuit breaker pattern in Ruby.
|
9
8
|
|
10
9
|
---
|
11
10
|
|
@@ -103,7 +102,7 @@ light.color # => "red"
|
|
103
102
|
After one minute, the light transitions to yellow, allowing a test execution:
|
104
103
|
|
105
104
|
```ruby
|
106
|
-
# Wait for the cool
|
105
|
+
# Wait for the cool-off time
|
107
106
|
sleep 60
|
108
107
|
light.run { 1 / 1 } #=> 1
|
109
108
|
```
|
@@ -130,7 +129,7 @@ receives `nil`. In both cases, the return value of the fallback becomes the retu
|
|
130
129
|
|
131
130
|
## Admin Panel
|
132
131
|
|
133
|
-
Stoplight
|
132
|
+
Stoplight comes with a built-in Admin Panel that can track all active Lights and manually lock them in the desired state (`Green` or `Red`). Locking lights in certain states might be helpful in scenarios like E2E testing.
|
134
133
|
|
135
134
|
To add Admin Panel protected by basic authentication to your Rails project, add this configuration to your `config/routes.rb` file.
|
136
135
|
|
@@ -378,7 +377,12 @@ single successful recovery probe will resume traffic flow.
|
|
378
377
|
|
379
378
|
### Data Store
|
380
379
|
|
381
|
-
Stoplight
|
380
|
+
Stoplight officially supports three data stores:
|
381
|
+
- In-memory data store
|
382
|
+
- Redis
|
383
|
+
- Valkey
|
384
|
+
|
385
|
+
By default, Stoplight uses an in-memory data store:
|
382
386
|
|
383
387
|
```ruby
|
384
388
|
require "stoplight"
|
@@ -386,7 +390,9 @@ Stoplight::Default::DATA_STORE
|
|
386
390
|
# => #<Stoplight::DataStore::Memory:...>
|
387
391
|
```
|
388
392
|
|
389
|
-
|
393
|
+
#### Redis for Production
|
394
|
+
|
395
|
+
For production environments, you'll likely want to use a persistent data store. One of the supported options is [Redis].
|
390
396
|
|
391
397
|
```ruby
|
392
398
|
# Configure Redis as the data store
|
@@ -399,9 +405,32 @@ Stoplight.configure do |config|
|
|
399
405
|
end
|
400
406
|
```
|
401
407
|
|
402
|
-
####
|
408
|
+
#### Valkey Support
|
403
409
|
|
404
|
-
|
410
|
+
Stoplight also supports [Valkey], a drop-in replacement for Redis.
|
411
|
+
Just point your Redis client to a Valkey instance and configure Stoplight as usual:
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
# ...
|
415
|
+
# We assume that Valkey is available on 127.0.0.1:6379 address
|
416
|
+
valkey = Redis.new(url: "redis://127.0.0.1:6379")
|
417
|
+
data_store = Stoplight::DataStore::Redis.new(valkey)
|
418
|
+
|
419
|
+
Stoplight.configure do |config|
|
420
|
+
config.data_store = data_store
|
421
|
+
# ...
|
422
|
+
end
|
423
|
+
```
|
424
|
+
|
425
|
+
#### DragonflyDB Support
|
426
|
+
|
427
|
+
Although Stoplight does not officially support [DragonflyDB], it can be used with it. For details, you may refer to the official [DragonflyDB documentation].
|
428
|
+
|
429
|
+
**NOTE**: Compatibility with [DragonflyDB] is not guaranteed, and results may vary. However, you are welcome to contribute to the project if you find any issues.
|
430
|
+
|
431
|
+
#### Connection Pooling
|
432
|
+
|
433
|
+
For high-traffic applications or when you want to control the number of open connections to the Data Store:
|
405
434
|
|
406
435
|
```ruby
|
407
436
|
require "connection_pool"
|
@@ -415,7 +444,7 @@ end
|
|
415
444
|
|
416
445
|
### Notifiers
|
417
446
|
|
418
|
-
Stoplight notifies when lights change state. Configure how these notifications are delivered:
|
447
|
+
Stoplight notifies when the lights change state. Configure how these notifications are delivered:
|
419
448
|
|
420
449
|
```ruby
|
421
450
|
# Log to a specific logger
|
@@ -540,9 +569,45 @@ stoplight = Stoplight("test-#{rand}")
|
|
540
569
|
|
541
570
|
## Maintenance Policy
|
542
571
|
|
543
|
-
|
544
|
-
|
545
|
-
|
572
|
+
We focus on supporting current, secure versions rather than maintaining extensive backwards compatibility. We follow
|
573
|
+
semantic versioning and give reasonable notice before dropping support.
|
574
|
+
|
575
|
+
### Stoplight Version Support
|
576
|
+
|
577
|
+
We only actively support the latest major version of Stoplight.
|
578
|
+
|
579
|
+
* ✅ Bug fixes and new features go to the current major version only (e.g., if you're on 4.x, upgrade to 5.x for fixes)
|
580
|
+
* ✅ Upgrade guidance is provided in release notes for major version changes
|
581
|
+
* ✅ We won't break compatibility in patch/minor releases
|
582
|
+
* ✅ We may accept community-contributed security patches for the previous major version
|
583
|
+
* ❌ We don't backport fixes to old Stoplight major versions
|
584
|
+
* ❌ We don't add new features to old Stoplight major versions
|
585
|
+
|
586
|
+
### What We Support
|
587
|
+
|
588
|
+
**Ruby**: Major versions that receive security updates (see [Ruby Maintenance Branches]):
|
589
|
+
|
590
|
+
* Currently: Ruby 3.2.x, 3.3.x and 3.4.x
|
591
|
+
* We test against these versions in CI
|
592
|
+
|
593
|
+
**Data Stores**: Current supported versions from upstream (versions that receive security updates):
|
594
|
+
|
595
|
+
* Redis: 8.0.x, 7.4.x, 7.2.x, 6.2.x (following [Redis's support policy])
|
596
|
+
* Valkey: 8.0.x, 7.2.x (following [Valkey's support policy])
|
597
|
+
* We test against the latest version of each major release
|
598
|
+
|
599
|
+
For dependencies:
|
600
|
+
* ✅ We test all supported dependency combinations in CI
|
601
|
+
* ✅ We investigate bug reports on supported dependency versions
|
602
|
+
* ❌ We don't test unsupported dependency versions (e.g., Ruby 3.1, Redis 5.x)
|
603
|
+
* ❌ We don't fix bugs specific to unsupported dependencies
|
604
|
+
|
605
|
+
### When We Drop Support
|
606
|
+
|
607
|
+
* Ruby: When Ruby core team ends security support, we drop it in our next major release
|
608
|
+
* Data Stores: When Redis/Valkey ends maintenance, we drop it in our next major release
|
609
|
+
|
610
|
+
Example: "Ruby 3.2 reaches end-of-life in March 2026, so Stoplight 6.0 will require Ruby 3.3+"
|
546
611
|
|
547
612
|
## Development
|
548
613
|
|
@@ -563,14 +628,12 @@ Fowler’s [CircuitBreaker][] article.
|
|
563
628
|
[build]: https://github.com/bolshakov/stoplight/actions?query=branch%3Amaster
|
564
629
|
[Coverage badge]: https://img.shields.io/coveralls/bolshakov/stoplight/master.svg?label=coverage
|
565
630
|
[coverage]: https://coveralls.io/r/bolshakov/stoplight
|
566
|
-
[Climate badge]: https://api.codeclimate.com/v1/badges/3451c2d281ffa345441a/maintainability
|
567
|
-
[climate]: https://codeclimate.com/github/bolshakov/stoplight
|
568
631
|
[stoplight-admin]: https://github.com/bolshakov/stoplight-admin
|
569
632
|
[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
|
570
633
|
[the change log]: CHANGELOG.md
|
571
634
|
[stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
|
572
635
|
[stoplight-honeybadger]: https://github.com/qoqa/stoplight-honeybadger
|
573
|
-
[notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/
|
636
|
+
[notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/main/lib/stoplight/notifier/generic.rb
|
574
637
|
[camdez]: https://github.com/camdez
|
575
638
|
[tfausak]: https://github.com/tfausak
|
576
639
|
[bolshakov]: https://github.com/bolshakov
|
@@ -579,3 +642,9 @@ Fowler’s [CircuitBreaker][] article.
|
|
579
642
|
[CircuitBreaker]: http://martinfowler.com/bliki/CircuitBreaker.html
|
580
643
|
[Redis]: https://redis.io/
|
581
644
|
[Git Flow wiki page]: https://github.com/bolshakov/stoplight/wiki/Git-Flow
|
645
|
+
[Valkey]: https://valkey.io/
|
646
|
+
[Ruby Maintenance Branches]: https://www.ruby-lang.org/en/downloads/branches/
|
647
|
+
[Redis's support policy]: https://redis.io/about/releases/
|
648
|
+
[Valkey's support policy]: https://valkey.io/topics/releases/
|
649
|
+
[DragonflyDB]: https://www.dragonflydb.io/
|
650
|
+
[DragonflyDB documentation]: https://www.dragonflydb.io/docs/managing-dragonfly/scripting#script-flags
|
@@ -54,7 +54,7 @@ module Stoplight
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def record_failure(config, failure)
|
57
|
-
with_fallback(
|
57
|
+
with_fallback(Metadata.new, config) do
|
58
58
|
data_store.record_failure(config, failure)
|
59
59
|
end
|
60
60
|
end
|
@@ -66,13 +66,13 @@ module Stoplight
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def record_recovery_probe_success(config, **args)
|
69
|
-
with_fallback(
|
69
|
+
with_fallback(Metadata.new, config) do
|
70
70
|
data_store.record_recovery_probe_success(config, **args)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
74
|
def record_recovery_probe_failure(config, failure)
|
75
|
-
with_fallback(
|
75
|
+
with_fallback(Metadata.new, config) do
|
76
76
|
data_store.record_recovery_probe_failure(config, failure)
|
77
77
|
end
|
78
78
|
end
|
@@ -7,6 +7,7 @@ module Stoplight
|
|
7
7
|
# @see Base
|
8
8
|
class Memory < Base
|
9
9
|
include MonitorMixin
|
10
|
+
|
10
11
|
KEY_SEPARATOR = ":"
|
11
12
|
|
12
13
|
def initialize
|
@@ -29,16 +30,16 @@ module Stoplight
|
|
29
30
|
# @return [Stoplight::Metadata]
|
30
31
|
def get_metadata(config)
|
31
32
|
light_name = config.name
|
32
|
-
window_end = Time.now
|
33
|
-
recovery_window = (window_end - config.cool_off_time + 1)..window_end
|
34
33
|
|
35
34
|
synchronize do
|
35
|
+
current_time = Time.now
|
36
|
+
recovery_window = (current_time - config.cool_off_time)..current_time
|
36
37
|
recovered_at = @metadata[light_name].recovered_at
|
37
38
|
window = if config.window_size
|
38
|
-
window_start = [recovered_at, (
|
39
|
-
(window_start..
|
39
|
+
window_start = [recovered_at, (current_time - config.window_size)].compact.max
|
40
|
+
(window_start..current_time)
|
40
41
|
else
|
41
|
-
(..
|
42
|
+
(..current_time)
|
42
43
|
end
|
43
44
|
|
44
45
|
errors = @errors[config.name].count do |request_time|
|
@@ -57,6 +58,7 @@ module Stoplight
|
|
57
58
|
end
|
58
59
|
|
59
60
|
@metadata[light_name].with(
|
61
|
+
current_time:,
|
60
62
|
errors:,
|
61
63
|
successes:,
|
62
64
|
recovery_probe_errors:,
|
@@ -170,17 +172,14 @@ module Stoplight
|
|
170
172
|
cleanup(@recovery_probe_successes[light_name], window_size: config.cool_off_time)
|
171
173
|
|
172
174
|
metadata = @metadata[light_name]
|
173
|
-
recovery_started_at = metadata.recovery_started_at || request_time
|
174
175
|
@metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
|
175
176
|
metadata.with(
|
176
177
|
last_success_at: request_time,
|
177
|
-
recovery_started_at:,
|
178
178
|
consecutive_errors: 0,
|
179
179
|
consecutive_successes: metadata.consecutive_successes.succ
|
180
180
|
)
|
181
181
|
else
|
182
182
|
metadata.with(
|
183
|
-
recovery_started_at:,
|
184
183
|
consecutive_errors: 0,
|
185
184
|
consecutive_successes: metadata.consecutive_successes.succ
|
186
185
|
)
|
@@ -259,9 +258,7 @@ module Stoplight
|
|
259
258
|
|
260
259
|
synchronize do
|
261
260
|
metadata = @metadata[light_name]
|
262
|
-
if metadata.recovery_started_at
|
263
|
-
false
|
264
|
-
else
|
261
|
+
if metadata.recovery_started_at.nil?
|
265
262
|
@metadata[light_name] = metadata.with(
|
266
263
|
recovery_started_at: current_time,
|
267
264
|
recovery_scheduled_after: nil,
|
@@ -269,6 +266,13 @@ module Stoplight
|
|
269
266
|
breached_at: nil
|
270
267
|
)
|
271
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
|
272
276
|
end
|
273
277
|
end
|
274
278
|
end
|
@@ -11,7 +11,7 @@ local metadata_key = KEYS[1]
|
|
11
11
|
-- we need to limit the start time of the window to the time of the last recovery.
|
12
12
|
local recovered_at = redis.call('HGET', metadata_key, "recovered_at")
|
13
13
|
if recovered_at then
|
14
|
-
window_start_ts = math.max(window_start_ts, recovered_at)
|
14
|
+
window_start_ts = math.max(window_start_ts, tonumber(recovered_at))
|
15
15
|
end
|
16
16
|
|
17
17
|
local function count_events(start_idx, bucket_count, start_ts)
|
@@ -94,8 +94,8 @@ module Stoplight
|
|
94
94
|
def get_metadata(config)
|
95
95
|
detect_clock_skew
|
96
96
|
|
97
|
-
|
98
|
-
window_end_ts =
|
97
|
+
current_time = Time.now
|
98
|
+
window_end_ts = current_time.to_i
|
99
99
|
window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
100
100
|
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
101
101
|
|
@@ -133,10 +133,11 @@ module Stoplight
|
|
133
133
|
last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
|
134
134
|
|
135
135
|
Metadata.new(
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
136
|
+
current_time:,
|
137
|
+
successes:,
|
138
|
+
errors:,
|
139
|
+
recovery_probe_successes:,
|
140
|
+
recovery_probe_errors:,
|
140
141
|
last_error:,
|
141
142
|
**meta_hash
|
142
143
|
)
|
@@ -12,10 +12,11 @@ module Stoplight
|
|
12
12
|
# Executes the provided code block when the light is in the green state.
|
13
13
|
#
|
14
14
|
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
15
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
15
16
|
# @yield The code block to execute.
|
16
17
|
# @return [Object] The result of the code block if successful.
|
17
18
|
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
18
|
-
def execute(fallback, &code)
|
19
|
+
def execute(fallback, metadata:, &code)
|
19
20
|
# TODO: Consider implementing sampling rate to limit the memory footprint
|
20
21
|
code.call.tap { record_success }
|
21
22
|
rescue => error
|
@@ -12,9 +12,10 @@ module Stoplight
|
|
12
12
|
# Executes the fallback proc when the light is in the red state.
|
13
13
|
#
|
14
14
|
# @param fallback [Proc, nil] A fallback proc to execute instead of the code block.
|
15
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
15
16
|
# @return [Object, nil] The result of the fallback proc if provided.
|
16
17
|
# @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
|
17
|
-
def execute(fallback)
|
18
|
+
def execute(fallback, metadata:)
|
18
19
|
if fallback
|
19
20
|
fallback.call(nil)
|
20
21
|
else
|
@@ -22,7 +22,9 @@ module Stoplight
|
|
22
22
|
@data_store = config.data_store
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
26
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
27
|
+
def execute(fallback, metadata:, &code)
|
26
28
|
raise NotImplementedError, "Subclasses must implement the execute method"
|
27
29
|
end
|
28
30
|
end
|
@@ -13,10 +13,12 @@ module Stoplight
|
|
13
13
|
# Executes the provided code block when the light is in the yellow state.
|
14
14
|
#
|
15
15
|
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
16
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
16
17
|
# @yield The code block to execute.
|
17
18
|
# @return [Object] The result of the code block if successful.
|
18
19
|
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
19
|
-
def execute(fallback, &code)
|
20
|
+
def execute(fallback, metadata:, &code)
|
21
|
+
transition_to_yellow(metadata:)
|
20
22
|
# TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
|
21
23
|
code.call.tap { record_recovery_probe_success }
|
22
24
|
rescue => error
|
@@ -47,28 +49,42 @@ module Stoplight
|
|
47
49
|
recover(metadata)
|
48
50
|
end
|
49
51
|
|
52
|
+
# @param metadata [Stoplight::Metadata]
|
53
|
+
# @return [void]
|
54
|
+
def transition_to_yellow(metadata:)
|
55
|
+
return unless metadata.color == Color::YELLOW
|
56
|
+
|
57
|
+
if metadata.recovery_scheduled_after && config.data_store.transition_to_color(config, Color::YELLOW)
|
58
|
+
config.notifiers.each do |notifier|
|
59
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
50
64
|
private def recover(metadata)
|
51
65
|
recovery_result = config.traffic_recovery.determine_color(config, metadata)
|
52
66
|
|
53
67
|
case recovery_result
|
54
|
-
when
|
68
|
+
when TrafficRecovery::GREEN
|
55
69
|
if data_store.transition_to_color(config, Color::GREEN)
|
56
70
|
config.notifiers.each do |notifier|
|
57
71
|
notifier.notify(config, Color::YELLOW, Color::GREEN, nil)
|
58
72
|
end
|
59
73
|
end
|
60
|
-
when
|
74
|
+
when TrafficRecovery::YELLOW
|
61
75
|
if data_store.transition_to_color(config, Color::YELLOW)
|
62
76
|
config.notifiers.each do |notifier|
|
63
|
-
notifier.notify(config, Color::
|
77
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
64
78
|
end
|
65
79
|
end
|
66
|
-
when
|
80
|
+
when TrafficRecovery::RED
|
67
81
|
if data_store.transition_to_color(config, Color::RED)
|
68
82
|
config.notifiers.each do |notifier|
|
69
83
|
notifier.notify(config, Color::YELLOW, Color::RED, nil)
|
70
84
|
end
|
71
85
|
end
|
86
|
+
when TrafficRecovery::PASS
|
87
|
+
# No state change, do nothing
|
72
88
|
else
|
73
89
|
raise "recovery strategy returned an expected color: #{recovery_result}"
|
74
90
|
end
|
data/lib/stoplight/light.rb
CHANGED
@@ -32,10 +32,7 @@ module Stoplight
|
|
32
32
|
#
|
33
33
|
# @return [String]
|
34
34
|
def state
|
35
|
-
|
36
|
-
.data_store
|
37
|
-
.get_metadata(config)
|
38
|
-
.locked_state
|
35
|
+
metadata.locked_state
|
39
36
|
end
|
40
37
|
|
41
38
|
# Returns current color:
|
@@ -49,10 +46,7 @@ module Stoplight
|
|
49
46
|
#
|
50
47
|
# @return [String] returns current light color
|
51
48
|
def color
|
52
|
-
|
53
|
-
.data_store
|
54
|
-
.get_metadata(config)
|
55
|
-
.color
|
49
|
+
metadata.color
|
56
50
|
end
|
57
51
|
|
58
52
|
# Runs the given block of code with this circuit breaker
|
@@ -72,8 +66,10 @@ module Stoplight
|
|
72
66
|
def run(fallback = nil, &code)
|
73
67
|
raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
|
74
68
|
|
75
|
-
|
76
|
-
|
69
|
+
metadata.then do |metadata|
|
70
|
+
strategy = state_strategy_factory(metadata.color)
|
71
|
+
strategy.execute(fallback, metadata:, &code)
|
72
|
+
end
|
77
73
|
end
|
78
74
|
|
79
75
|
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
@@ -186,5 +182,10 @@ module Stoplight
|
|
186
182
|
def reconfigure(config)
|
187
183
|
self.class.new(config)
|
188
184
|
end
|
185
|
+
|
186
|
+
# @return [Stoplight::Metadata]
|
187
|
+
def metadata
|
188
|
+
config.data_store.get_metadata(config)
|
189
|
+
end
|
189
190
|
end
|
190
191
|
end
|
data/lib/stoplight/metadata.rb
CHANGED
@@ -16,13 +16,15 @@ module Stoplight
|
|
16
16
|
:locked_state,
|
17
17
|
:recovery_scheduled_after,
|
18
18
|
:recovery_started_at,
|
19
|
-
:recovered_at
|
19
|
+
:recovered_at,
|
20
|
+
:current_time
|
20
21
|
) do
|
21
22
|
def initialize(
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
current_time: Time.now,
|
24
|
+
successes: 0,
|
25
|
+
errors: 0,
|
26
|
+
recovery_probe_successes: 0,
|
27
|
+
recovery_probe_errors: 0,
|
26
28
|
last_error_at: nil,
|
27
29
|
last_success_at: nil,
|
28
30
|
consecutive_errors: 0,
|
@@ -35,31 +37,41 @@ module Stoplight
|
|
35
37
|
recovered_at: nil
|
36
38
|
)
|
37
39
|
super(
|
38
|
-
recovery_probe_successes
|
39
|
-
recovery_probe_errors
|
40
|
-
successes
|
41
|
-
errors
|
40
|
+
recovery_probe_successes: recovery_probe_successes.to_i,
|
41
|
+
recovery_probe_errors: recovery_probe_errors.to_i,
|
42
|
+
successes: successes.to_i,
|
43
|
+
errors: errors.to_i,
|
42
44
|
last_error_at: (Time.at(Integer(last_error_at)) if last_error_at),
|
43
45
|
last_success_at: (Time.at(Integer(last_success_at)) if last_success_at),
|
44
|
-
consecutive_errors:
|
45
|
-
consecutive_successes:
|
46
|
+
consecutive_errors: consecutive_errors.to_i,
|
47
|
+
consecutive_successes: consecutive_successes.to_i,
|
46
48
|
last_error:,
|
47
49
|
breached_at: (Time.at(Integer(breached_at)) if breached_at),
|
48
50
|
locked_state: locked_state || State::UNLOCKED,
|
49
51
|
recovery_scheduled_after: (Time.at(Integer(recovery_scheduled_after)) if recovery_scheduled_after),
|
50
52
|
recovery_started_at: (Time.at(Integer(recovery_started_at)) if recovery_started_at),
|
51
53
|
recovered_at: (Time.at(Integer(recovered_at)) if recovered_at),
|
54
|
+
current_time:,
|
52
55
|
)
|
53
56
|
end
|
54
57
|
|
55
|
-
#
|
58
|
+
# Creates a new Metadata instance with updated attributes. This method overrides
|
59
|
+
# the default +with+ method provided by +Data.define+ to ensure constructor
|
60
|
+
# logic is applied.
|
61
|
+
#
|
62
|
+
# @param kwargs [Hash{Symbol => Object}]
|
63
|
+
# @return [Metadata]
|
64
|
+
def with(**kwargs)
|
65
|
+
self.class.new(**to_h.merge(current_time: Time.now, **kwargs))
|
66
|
+
end
|
67
|
+
|
56
68
|
# @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
|
57
|
-
def color
|
69
|
+
def color
|
58
70
|
if locked_state == State::LOCKED_GREEN
|
59
71
|
Color::GREEN
|
60
72
|
elsif locked_state == State::LOCKED_RED
|
61
73
|
Color::RED
|
62
|
-
elsif (recovery_scheduled_after && recovery_scheduled_after <
|
74
|
+
elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
|
63
75
|
Color::YELLOW
|
64
76
|
elsif breached_at
|
65
77
|
Color::RED
|
@@ -72,7 +84,7 @@ module Stoplight
|
|
72
84
|
#
|
73
85
|
# @return [Float]
|
74
86
|
def error_rate
|
75
|
-
if
|
87
|
+
if (successes + errors).zero?
|
76
88
|
0.0
|
77
89
|
else
|
78
90
|
errors.fdiv(successes + errors)
|
@@ -49,10 +49,7 @@ module Stoplight
|
|
49
49
|
#
|
50
50
|
# @param config [Stoplight::Light::Config]
|
51
51
|
# @param metadata [Stoplight::Metadata]
|
52
|
-
# @return [
|
53
|
-
# - Stoplight::Color::RED: Recovery failed, block all traffic
|
54
|
-
# - Stoplight::Color::YELLOW: Continue recovery process
|
55
|
-
# - Stoplight::Color::GREEN: Recovery successful, return to normal traffic flow
|
52
|
+
# @return [TrafficRecovery::Decision]
|
56
53
|
def determine_color(config, metadata)
|
57
54
|
raise NotImplementedError
|
58
55
|
end
|
@@ -49,16 +49,18 @@ module Stoplight
|
|
49
49
|
#
|
50
50
|
# @param config [Stoplight::Light::Config]
|
51
51
|
# @param metadata [Stoplight::Metadata]
|
52
|
-
# @return [
|
52
|
+
# @return [TrafficRecovery::Decision]
|
53
53
|
def determine_color(config, metadata)
|
54
|
+
return TrafficRecovery::PASS if metadata.color != Color::YELLOW
|
55
|
+
|
54
56
|
recovery_started_at = metadata.recovery_started_at || metadata.recovery_scheduled_after
|
55
57
|
|
56
58
|
if metadata.last_error_at && metadata.last_error_at >= recovery_started_at
|
57
|
-
|
59
|
+
TrafficRecovery::RED
|
58
60
|
elsif [metadata.consecutive_successes, metadata.recovery_probe_successes].min >= config.recovery_threshold
|
59
|
-
|
61
|
+
TrafficRecovery::GREEN
|
60
62
|
else
|
61
|
-
|
63
|
+
TrafficRecovery::YELLOW
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
data/lib/stoplight/version.rb
CHANGED
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.3.
|
4
|
+
version: 5.3.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cameron Desautels
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- lib/stoplight/traffic_control/base.rb
|
98
98
|
- lib/stoplight/traffic_control/consecutive_errors.rb
|
99
99
|
- lib/stoplight/traffic_control/error_rate.rb
|
100
|
+
- lib/stoplight/traffic_recovery.rb
|
100
101
|
- lib/stoplight/traffic_recovery/base.rb
|
101
102
|
- lib/stoplight/traffic_recovery/consecutive_successes.rb
|
102
103
|
- lib/stoplight/version.rb
|