stoplight 5.3.1 → 5.3.5
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 +4 -4
- data/lib/stoplight/data_store/redis/get_metadata.lua +1 -1
- data/lib/stoplight/empty_metadata.rb +5 -0
- data/lib/stoplight/light/yellow_run_strategy.rb +5 -3
- data/lib/stoplight/metadata.rb +21 -11
- 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 +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c930c989a3184cfc4fb254c720a854caf34fb56bbb550df150d65125969240b2
|
4
|
+
data.tar.gz: 3ca98448ca2a378d6101e4b10e4567adb24bc65c2747d77bccf4b9386aeba64c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: caf3fbc0b32e823a3877525841ab46948fdedbda6791da8006ded3c194229aad7add1c01df82f60b406606a34481a40535662036b2f21caf1ea953ed58ae3eab
|
7
|
+
data.tar.gz: 0d372655a21b4f2ebf04973a6fa41ae4e24e1bab2851bfef69c6ba866a2e38231cdc2a13f4c3afb7a267f56c7a5e3040d88c4ce0602f6b4392b9216fd57d6f2a
|
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
|
@@ -48,13 +48,13 @@ module Stoplight
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def get_metadata(config)
|
51
|
-
with_fallback(
|
51
|
+
with_fallback(EmptyMetadata, config) do
|
52
52
|
data_store.get_metadata(config)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
56
|
def record_failure(config, failure)
|
57
|
-
with_fallback(
|
57
|
+
with_fallback(EmptyMetadata, 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(EmptyMetadata, 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(EmptyMetadata, config) do
|
76
76
|
data_store.record_recovery_probe_failure(config, failure)
|
77
77
|
end
|
78
78
|
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)
|
@@ -51,24 +51,26 @@ module Stoplight
|
|
51
51
|
recovery_result = config.traffic_recovery.determine_color(config, metadata)
|
52
52
|
|
53
53
|
case recovery_result
|
54
|
-
when
|
54
|
+
when TrafficRecovery::GREEN
|
55
55
|
if data_store.transition_to_color(config, Color::GREEN)
|
56
56
|
config.notifiers.each do |notifier|
|
57
57
|
notifier.notify(config, Color::YELLOW, Color::GREEN, nil)
|
58
58
|
end
|
59
59
|
end
|
60
|
-
when
|
60
|
+
when TrafficRecovery::YELLOW
|
61
61
|
if data_store.transition_to_color(config, Color::YELLOW)
|
62
62
|
config.notifiers.each do |notifier|
|
63
63
|
notifier.notify(config, Color::GREEN, Color::YELLOW, nil)
|
64
64
|
end
|
65
65
|
end
|
66
|
-
when
|
66
|
+
when TrafficRecovery::RED
|
67
67
|
if data_store.transition_to_color(config, Color::RED)
|
68
68
|
config.notifiers.each do |notifier|
|
69
69
|
notifier.notify(config, Color::YELLOW, Color::RED, nil)
|
70
70
|
end
|
71
71
|
end
|
72
|
+
when TrafficRecovery::PASS
|
73
|
+
# No state change, do nothing
|
72
74
|
else
|
73
75
|
raise "recovery strategy returned an expected color: #{recovery_result}"
|
74
76
|
end
|
data/lib/stoplight/metadata.rb
CHANGED
@@ -19,10 +19,10 @@ module Stoplight
|
|
19
19
|
:recovered_at
|
20
20
|
) do
|
21
21
|
def initialize(
|
22
|
-
successes:
|
23
|
-
errors:
|
24
|
-
recovery_probe_successes:
|
25
|
-
recovery_probe_errors:
|
22
|
+
successes: 0,
|
23
|
+
errors: 0,
|
24
|
+
recovery_probe_successes: 0,
|
25
|
+
recovery_probe_errors: 0,
|
26
26
|
last_error_at: nil,
|
27
27
|
last_success_at: nil,
|
28
28
|
consecutive_errors: 0,
|
@@ -35,14 +35,14 @@ module Stoplight
|
|
35
35
|
recovered_at: nil
|
36
36
|
)
|
37
37
|
super(
|
38
|
-
recovery_probe_successes
|
39
|
-
recovery_probe_errors
|
40
|
-
successes
|
41
|
-
errors
|
38
|
+
recovery_probe_successes: recovery_probe_successes.to_i,
|
39
|
+
recovery_probe_errors: recovery_probe_errors.to_i,
|
40
|
+
successes: successes.to_i,
|
41
|
+
errors: errors.to_i,
|
42
42
|
last_error_at: (Time.at(Integer(last_error_at)) if last_error_at),
|
43
43
|
last_success_at: (Time.at(Integer(last_success_at)) if last_success_at),
|
44
|
-
consecutive_errors:
|
45
|
-
consecutive_successes:
|
44
|
+
consecutive_errors: consecutive_errors.to_i,
|
45
|
+
consecutive_successes: consecutive_successes.to_i,
|
46
46
|
last_error:,
|
47
47
|
breached_at: (Time.at(Integer(breached_at)) if breached_at),
|
48
48
|
locked_state: locked_state || State::UNLOCKED,
|
@@ -52,6 +52,16 @@ module Stoplight
|
|
52
52
|
)
|
53
53
|
end
|
54
54
|
|
55
|
+
# Creates a new Metadata instance with updated attributes. This method overrides
|
56
|
+
# the default +with+ method provided by +Data.define+ to ensure constructor
|
57
|
+
# logic is applied.
|
58
|
+
#
|
59
|
+
# @param kwargs [Hash{Symbol => Object}]
|
60
|
+
# @return [Metadata]
|
61
|
+
def with(**kwargs)
|
62
|
+
self.class.new(**to_h.merge(kwargs))
|
63
|
+
end
|
64
|
+
|
55
65
|
# @param at [Time] (Time.now) the moment of time when the color is determined
|
56
66
|
# @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
|
57
67
|
def color(at: Time.now)
|
@@ -72,7 +82,7 @@ module Stoplight
|
|
72
82
|
#
|
73
83
|
# @return [Float]
|
74
84
|
def error_rate
|
75
|
-
if
|
85
|
+
if (successes + errors).zero?
|
76
86
|
0.0
|
77
87
|
else
|
78
88
|
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.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cameron Desautels
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- lib/stoplight/data_store/redis/transition_to_red.lua
|
77
77
|
- lib/stoplight/data_store/redis/transition_to_yellow.lua
|
78
78
|
- lib/stoplight/default.rb
|
79
|
+
- lib/stoplight/empty_metadata.rb
|
79
80
|
- lib/stoplight/error.rb
|
80
81
|
- lib/stoplight/failure.rb
|
81
82
|
- lib/stoplight/light.rb
|
@@ -97,6 +98,7 @@ files:
|
|
97
98
|
- lib/stoplight/traffic_control/base.rb
|
98
99
|
- lib/stoplight/traffic_control/consecutive_errors.rb
|
99
100
|
- lib/stoplight/traffic_control/error_rate.rb
|
101
|
+
- lib/stoplight/traffic_recovery.rb
|
100
102
|
- lib/stoplight/traffic_recovery/base.rb
|
101
103
|
- lib/stoplight/traffic_recovery/consecutive_successes.rb
|
102
104
|
- lib/stoplight/version.rb
|