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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d9c236d51c7ba9b67ccbc0be19c1836b90b2a1b1f6b9e38cdfcfa28cfd85d5e
4
- data.tar.gz: 85f04de041217bfd4ad1d0892d637847db62dba16e3dab8b00b5cb87b0c73725
3
+ metadata.gz: c930c989a3184cfc4fb254c720a854caf34fb56bbb550df150d65125969240b2
4
+ data.tar.gz: 3ca98448ca2a378d6101e4b10e4567adb24bc65c2747d77bccf4b9386aeba64c
5
5
  SHA512:
6
- metadata.gz: 6c5f9dfe0b88fb11ce8e2f830d73101ad39ed5a3c398d08e0ab4afd90c55246a4f62717f1eca4c1e463669ef123d8496e8525fc935b84f323ec7c15469d1db24
7
- data.tar.gz: 267bac02ac372e70646820ffee6e6209fe6de8fd26658c2975cd08f079676cb5862e41a5154e1f58d6897f4ede79b5284fad1678310cc458867fcedc5466f874
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 off time
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 goes 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.
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 uses an in-memory data store out of the box:
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
- For production environments, you'll likely want to use a persistent data store. Currently, [Redis] is the supported option:
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
- #### Connection Pooling with Redis
408
+ #### Valkey Support
403
409
 
404
- For high-traffic applications or when you want to control a number of opened connections to Redis:
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
- Stoplight supports the latest three minor versions of Ruby, which currently are: `3.2.x`, `3.3.x`, and `3.4.x`. Changing
544
- the minimum supported Ruby version is not considered a breaking change. We support the current stable Redis
545
- version (`7.4.x`) and the latest release of the previous major version (`6.2.x`)
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/master/lib/stoplight/notifier/generic.rb
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(Metadata.new, config) do
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(nil, config) do
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(nil, config) do
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(nil, config) do
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)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ EmptyMetadata = Metadata.new
5
+ end
@@ -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 Color::GREEN
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 Color::YELLOW
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 Color::RED
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
@@ -19,10 +19,10 @@ module Stoplight
19
19
  :recovered_at
20
20
  ) do
21
21
  def initialize(
22
- successes: nil,
23
- errors: nil,
24
- recovery_probe_successes: nil,
25
- recovery_probe_errors: nil,
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: Integer(consecutive_errors),
45
- consecutive_successes: Integer(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 successes.nil? || errors.nil? || (successes + errors).zero?
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 [String] One of the Stoplight::Color constants:
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 [String]
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
- Color::RED
59
+ TrafficRecovery::RED
58
60
  elsif [metadata.consecutive_successes, metadata.recovery_probe_successes].min >= config.recovery_threshold
59
- Color::GREEN
61
+ TrafficRecovery::GREEN
60
62
  else
61
- Color::YELLOW
63
+ TrafficRecovery::YELLOW
62
64
  end
63
65
  end
64
66
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module TrafficRecovery
5
+ Decision = Data.define(:decision)
6
+ GREEN = Decision.new("green")
7
+ YELLOW = Decision.new("yellow")
8
+ RED = Decision.new("red")
9
+ PASS = Decision.new("pass")
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- VERSION = Gem::Version.new("5.3.1")
4
+ VERSION = Gem::Version.new("5.3.5")
5
5
  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.3.1
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