stoplight 6.0.0.rc3 → 6.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97295cc794acc8de621f091e6cb0cfc7903f2f3bc5c439773c7888cb267bd197
4
- data.tar.gz: c154e63a6dcd7094d56173647f022ed800514969db64655aac9dd3aefedd566a
3
+ metadata.gz: e9e835f7351f236b8965d8179da39eff4eaf768bbf066c94cb35f8ed6f0360f6
4
+ data.tar.gz: ea64aa21f4874b40843e2f18716da346b4e0a4fbf77f57ca777bafa8e396ae5b
5
5
  SHA512:
6
- metadata.gz: b6d7af0c9ad656fca39fb69bcc563e22cb955e3ec60e24e0615a7877fa3f09d7fe58152d7cb1a8797190529bab55b92ecfda544a4fddb7dbe1c16539adbe30b3
7
- data.tar.gz: 5e222668a15bdd9e7b5b384ec83fb9b94e61e069d34cc509da7855325740e932fb72afcd31f91db5ed74847ce2509f019a8b10db550245741cbb73ab37c484b3
6
+ metadata.gz: 3bfa3fb97ed1f8f2743930257fefb4eba4f380b9b7eea122af9ca792206b43c27b42fb35292d95ab6c608ad70e3f99f6cf3ca0eb319e2d31c2010943e50a9730
7
+ data.tar.gz: 330fce49dcde86b0d0a9864143523db53caa20f64105cd40f85fcc4930769e3cd35ddb1f199aefce184b6c08eed69259acf0d697a9c4dc4e8245f482fc4a2474
data/README.md CHANGED
@@ -80,8 +80,8 @@ After the last failure, the light turns red. The next call will raise a `Stoplig
80
80
  executing the block:
81
81
 
82
82
  ```ruby
83
- light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
84
- light.color # => "red"
83
+ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: Light `Example` is red - traffic stopped until recovery.
84
+ light.color # => :red
85
85
  ```
86
86
 
87
87
  The `Stoplight::Error::RedLight` provides metadata about the error:
@@ -108,7 +108,7 @@ light.run { 1 / 1 } #=> 1
108
108
  If the test probe succeeds, the light turns green again. If it fails, the light turns red again.
109
109
 
110
110
  ```ruby
111
- light.color #=> "green"
111
+ light.color #=> :green
112
112
  ```
113
113
 
114
114
  ### Using Fallbacks
@@ -388,7 +388,7 @@ By default, Stoplight uses an in-memory data store:
388
388
 
389
389
  ```ruby
390
390
  require "stoplight"
391
- Stoplight::Default::DATA_STORE
391
+ Stoplight::Wiring::Default::DATA_STORE
392
392
  # => #<Stoplight::DataStore::Memory:...>
393
393
  ```
394
394
 
@@ -678,8 +678,8 @@ Example: "Ruby 3.3 reaches end-of-life in March 2027, so Stoplight 7.0 will requ
678
678
  ## Development
679
679
 
680
680
  After checking out the repo, run `bundle install` to install dependencies. Run tests with `bundle exec rspec` and check
681
- code style with `bundle exec standardrb`. We follow a git flow branching strategy - see our [Git Flow wiki page] for
682
- details on branch naming, releases, and contribution workflow. Also check our CONTRIBUTING.md guide for contributors.
681
+ code style with `bundle exec standardrb`. All work happens on `main` - see [CONTRIBUTING.md] for branch naming,
682
+ releases, and the contribution workflow.
683
683
 
684
684
  ## Credits
685
685
 
@@ -700,7 +700,7 @@ Fowler’s [CircuitBreaker][] article.
700
700
  [stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
701
701
  [stoplight-honeybadger]: https://github.com/qoqa/stoplight-honeybadger
702
702
  [stoplight-statsd]: https://github.com/bolshakov/stoplight-statsd
703
- [notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/main/lib/stoplight/domain/state_transition_notifier.rb
703
+ [notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/main/sig/stoplight/domain/ports/state_transition_notifier.rbs
704
704
  [camdez]: https://github.com/camdez
705
705
  [tfausak]: https://github.com/tfausak
706
706
  [bolshakov]: https://github.com/bolshakov
@@ -708,7 +708,7 @@ Fowler’s [CircuitBreaker][] article.
708
708
  [complete list of contributors]: https://github.com/bolshakov/stoplight/graphs/contributors
709
709
  [CircuitBreaker]: http://martinfowler.com/bliki/CircuitBreaker.html
710
710
  [Redis]: https://redis.io/
711
- [Git Flow wiki page]: https://github.com/bolshakov/stoplight/wiki/Git-Flow
711
+ [CONTRIBUTING.md]: CONTRIBUTING.md
712
712
  [Valkey]: https://valkey.io/
713
713
  [Ruby Maintenance Branches]: https://www.ruby-lang.org/en/downloads/branches/
714
714
  [Redis's support policy]: https://redis.io/about/releases/
data/UPGRADING.md CHANGED
@@ -8,6 +8,7 @@ Here's what you'll want to tackle during your upgrade, roughly ordered from the
8
8
 
9
9
  - [ ] Make every `Stoplight("name", ...)` call site for the same name pass identical settings
10
10
  - [ ] Move `data_store`, `notifiers`, and `error_notifier` from individual lights to `Stoplight.configure`
11
+ - [ ] Make sure that you run at least Ruby 3.3+, Redis 7.4+ (or Valkey 7.2+)
11
12
  - [ ] Replace the `with_*` builder methods with settings passed at creation time
12
13
  - [ ] Replace proc and anonymous-class error matchers with named classes or modules
13
14
  - [ ] Account for Stoplight state reset after deployment
@@ -16,6 +17,7 @@ Here's what you'll want to tackle during your upgrade, roughly ordered from the
16
17
  - [ ] Round any fractional `cool_off_time` up to a whole number of seconds, at least 1
17
18
  - [ ] Drop `warn_on_clock_skew` from your Redis data store setup
18
19
  - [ ] Compare `light.color` and `light.state` against symbols, not strings, and pass a symbol to `light.lock`
20
+ - [ ] Check anything that parses or matches `Stoplight::Error::RedLight` messages - the message format changed
19
21
  - [ ] Test thoroughly in a staging environment
20
22
 
21
23
  ### Lights Are Registered and Reused
@@ -87,6 +89,13 @@ end
87
89
  If you genuinely need more than one data store - separate Redis instances per tenant, for example - use named
88
90
  systems, which own their own infrastructure. See [Systems: Namespace Isolation & Configuration][systems] for setup.
89
91
 
92
+ ### Ruby 3.3 and Redis 7.4 Are the new Minimum
93
+
94
+ Stoplight 6 requires Ruby 3.3 or newer.
95
+ We also require Redis 7.4 or newer and Valkey 7.2 or newer and no longer tested or supported earlier versions.
96
+
97
+ See the [maintenance policy] for the versions under test.
98
+
90
99
  ### The Light Builder API Is Gone
91
100
 
92
101
  `Light#with` and the whole `with_*` family are removed: `with_data_store`, `with_cool_off_time`, `with_threshold`,
@@ -276,6 +285,19 @@ light.lock(Stoplight::Color::GREEN)
276
285
  Symbols are the idiomatic Ruby choice for a closed set of values. Anything that serializes them - logs, metrics tags,
277
286
  the Admin dashboard's JSON - renders the same text as before.
278
287
 
288
+ ### The `RedLight` Exception Message Changed
289
+
290
+ The message of `Stoplight::Error::RedLight` used to be the bare light name. It is now a sentence that names the light
291
+ and says what happened, so a log line or an error tracker (e.g., in Sentry) entry is readable on its own:
292
+
293
+ ```
294
+ # 5.x
295
+ Stoplight::Error::RedLight: Payment Service
296
+
297
+ # 6.0
298
+ Stoplight::Error::RedLight: Light `Payment Service` is red - traffic stopped until recovery.
299
+ ```
300
+
279
301
  ### Getting Help
280
302
 
281
303
  If you run into anything this guide doesn't cover, post to our [Discussions forum]. The configuration errors in 6.0
@@ -582,6 +604,7 @@ Nothing. Stoplight will function as usual.
582
604
  [Community-supported notifiers]: https://github.com/bolshakov/stoplight/tree/master#community-supported-notifiers
583
605
  [How to implement your own notifier?]: https://github.com/bolshakov/stoplight/blob/master/lib/stoplight/notifier/generic.rb
584
606
  [dropped notifiers]: https://github.com/bolshakov/stoplight/tree/v3.0.1/lib/stoplight/notifier
607
+ [maintenance policy]: https://github.com/bolshakov/stoplight#maintenance-policy
585
608
  [without passing an empty block]: https://github.com/bolshakov/stoplight-admin/blob/9c9848eb94410e46b20972548f0863db224cb6da/lib/sinatra/stoplight_admin.rb#L30
586
609
  [sliding window]: https://github.com/bolshakov/stoplight#custom-window-size
587
610
  [systems]: https://github.com/bolshakov/stoplight/blob/master/docs/systems.md
@@ -30,6 +30,13 @@ module Stoplight
30
30
  # @see Stoplight::Infrastructure::Redis::Key
31
31
  def self.key_space = KEY_SPACE
32
32
  def key_space = self.class.key_space
33
+
34
+ # Lua scripts are loaded into Redis once per server, so every light shares a single script cache.
35
+ #
36
+ # @api private
37
+ def __stoplight__scripting
38
+ @scripting ||= Infrastructure::Redis::Storage::Scripting.new(redis:)
39
+ end
33
40
  end
34
41
 
35
42
  class Memory < Base
@@ -18,7 +18,7 @@ module Stoplight
18
18
  end
19
19
 
20
20
  class RedLight < Base
21
- EXCEPTION_MESSAGE = 'Stoplight "%{light_name}" is red - traffic stopped until recovery.'
21
+ EXCEPTION_MESSAGE = "Light `%{light_name}` is red - traffic stopped until recovery."
22
22
  private_constant :EXCEPTION_MESSAGE
23
23
 
24
24
  # @!attribute light_name
@@ -21,6 +21,7 @@ module Stoplight
21
21
  @shas = Hash.new do |hash, script_name|
22
22
  hash[script_name] = Digest::SHA1.hexdigest(resolve_source(script_name))
23
23
  end
24
+ @invoked = Set.new
24
25
  end
25
26
 
26
27
  def call(script_name, keys: [], args: [])
@@ -33,18 +34,23 @@ module Stoplight
33
34
  rescue ::Redis::CommandError => error
34
35
  if error.message.include?("NOSCRIPT") && !retried
35
36
  retried = true
36
- reload_script(script_name)
37
+ if @invoked.include?(script_name) # skip warning for the first script invocation on app boot
38
+ warn "Stoplight is unable to find the script '#{script_name}' from Redis's script cache. " \
39
+ "Reloading and retrying..."
40
+ end
41
+ load_script(script_name)
37
42
  retry
38
43
  else
39
44
  raise error
40
45
  end
46
+ ensure
47
+ @invoked << script_name
41
48
  end
42
49
  end
43
50
 
44
51
  private
45
52
 
46
- def reload_script(script_name)
47
- warn "Stoplight is unable to find the script '#{script_name}' in Redis's script cache. Reloading and retrying..."
53
+ def load_script(script_name)
48
54
  @redis.then { |client| client.script(:load, resolve_source(script_name)) }
49
55
  end
50
56
 
@@ -0,0 +1,9 @@
1
+ -- @include now
2
+
3
+ local meta_key = KEYS[1]
4
+
5
+ local state = redis.call('HMGET', meta_key, 'breached_at', 'locked_state', 'recovery_scheduled_after', 'recovery_started_at')
6
+ local current_time = now()
7
+ state[5] = current_time
8
+
9
+ return state
@@ -1,8 +1,9 @@
1
1
  -- @include now
2
2
 
3
3
  local meta_key = KEYS[1]
4
- local recovery_scheduled_after_ts = tonumber(ARGV[1])
4
+ local cool_off_time = tonumber(ARGV[1])
5
5
  local current_ts = now() / 1000
6
+ local recovery_scheduled_after_ts = current_ts + cool_off_time
6
7
 
7
8
  -- 1 if the field is a new field in the hash and the value was set
8
9
  local became_red = redis.call('HSETNX', meta_key, 'breached_at', current_ts)
@@ -57,16 +57,19 @@ module Stoplight
57
57
  end
58
58
 
59
59
  def state_snapshot
60
- breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw = redis.with do |client|
61
- client.hmget(state_key, :breached_at, :locked_state, :recovery_scheduled_after, :recovery_started_at)
62
- end
60
+ breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw, now_ms =
61
+ scripting.call(
62
+ "state/state_snapshot",
63
+ args: [],
64
+ keys: [state_key]
65
+ )
63
66
 
64
67
  Domain::StateSnapshot.new(
65
68
  breached_at: breached_at_raw && clock.at(breached_at_raw.to_f),
66
69
  locked_state: locked_state&.to_sym || Stoplight::State::UNLOCKED,
67
70
  recovery_scheduled_after: recovery_scheduled_after_raw && clock.at(recovery_scheduled_after_raw.to_f),
68
71
  recovery_started_at: recovery_started_at_raw && clock.at(recovery_started_at_raw.to_f),
69
- time: clock.current_time
72
+ time: clock.at(now_ms.fdiv(1000))
70
73
  )
71
74
  end
72
75
 
@@ -123,12 +126,9 @@ module Stoplight
123
126
  # Transitions to RED state and ensures only one notification
124
127
  #
125
128
  def transition_to_red
126
- current_ts = clock.current_time.to_f
127
- recovery_scheduled_after_ts = current_ts + cool_off_time
128
-
129
129
  became_red = scripting.call(
130
130
  "state/transition_to_red",
131
- args: [recovery_scheduled_after_ts],
131
+ args: [cool_off_time],
132
132
  keys: [state_key]
133
133
  )
134
134
 
@@ -18,6 +18,8 @@ local function evict_buckets(metrics_key, ts_index_key, window_start)
18
18
  return
19
19
  end
20
20
 
21
+ local failures, successes = 0, 0
22
+
21
23
  for from = 1, #buckets, EVICT_BATCH_SIZE do
22
24
  local to = math.min(from + EVICT_BATCH_SIZE - 1, #buckets)
23
25
 
@@ -29,16 +31,15 @@ local function evict_buckets(metrics_key, ts_index_key, window_start)
29
31
 
30
32
  local values = redis.call('HMGET', metrics_key, unpack(fields))
31
33
 
32
- local failures, successes = 0, 0
33
34
  for i = 1, #values, 2 do
34
35
  failures = failures + (tonumber(values[i]) or 0)
35
36
  successes = successes + (tonumber(values[i + 1]) or 0)
36
37
  end
37
38
 
38
- redis.call('HINCRBY', metrics_key, 'total_failures', -failures)
39
- redis.call('HINCRBY', metrics_key, 'total_successes', -successes)
40
39
  redis.call('HDEL', metrics_key, unpack(fields))
41
40
  end
42
41
 
42
+ redis.call('HINCRBY', metrics_key, 'total_failures', -failures)
43
+ redis.call('HINCRBY', metrics_key, 'total_successes', -successes)
43
44
  redis.call('ZREMRANGEBYSCORE', ts_index_key, '-inf', window_start)
44
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- VERSION = Gem::Version.new("6.0.0.rc3")
4
+ VERSION = Gem::Version.new("6.0.0")
5
5
  end
@@ -72,7 +72,16 @@ module Stoplight
72
72
  attr_reader :system_name
73
73
 
74
74
  def recovery_lock_store = storage_set.recovery_lock_store
75
- def storage_scripting = Infrastructure::Redis::Storage::Scripting.new(redis:)
75
+
76
+ def storage_scripting
77
+ case data_store_config
78
+ when DataStore::Redis
79
+ data_store_config.__stoplight__scripting
80
+ else
81
+ raise T.absurd(data_store_config)
82
+ end
83
+ end
84
+
76
85
  def failover_system = T.must(@failover_system)
77
86
 
78
87
  def key_space
data/lib/stoplight.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require "concurrent/map"
4
4
  require "zeitwerk"
5
5
 
6
- # steep:ignore:start
7
6
  loader = Zeitwerk::Loader.for_gem
8
7
  loader.inflector.inflect("io" => "IO")
9
8
  loader.do_not_eager_load(
@@ -14,7 +13,6 @@ loader.do_not_eager_load(
14
13
  loader.ignore("#{__dir__}/generators")
15
14
  loader.ignore("#{__dir__}/stoplight/rspec.rb", "#{__dir__}/stoplight/rspec")
16
15
  loader.setup
17
- # steep:ignore:end
18
16
 
19
17
  module Stoplight # rubocop:disable Style/Documentation
20
18
  T = Types
@@ -7,9 +7,12 @@ module Stoplight
7
7
  KEY_SPACE: Infrastructure::Redis::Key
8
8
 
9
9
  attr_reader redis: Infrastructure::redis
10
+ @scripting: Infrastructure::Redis::Storage::Scripting?
10
11
 
11
12
  def initialize: (Infrastructure::redis) -> void
12
13
 
14
+ def __stoplight__scripting: -> Infrastructure::Redis::Storage::Scripting
15
+
13
16
  def self.key_space: -> Infrastructure::Redis::Key
14
17
  def key_space: -> Infrastructure::Redis::Key
15
18
  end
@@ -11,6 +11,7 @@ module Stoplight
11
11
 
12
12
  @scripts_path: Array[String]
13
13
  @shas: Hash[String, String]
14
+ @invoked: Set[String]
14
15
  @redis: redis
15
16
 
16
17
  def initialize: (
@@ -24,7 +25,7 @@ module Stoplight
24
25
  ?args: Array[_ToS]
25
26
  ) -> untyped
26
27
 
27
- private def reload_script: (String script_name) -> void
28
+ private def load_script: (String script_name) -> void
28
29
 
29
30
  private def resolve_source: (String script_name) -> String
30
31
  end
@@ -9,7 +9,7 @@ module Stoplight
9
9
  attr_reader scripting: Scripting
10
10
  attr_reader key_space: Key
11
11
  attr_reader clock: Domain::_Clock
12
- attr_reader cool_off_time: Domain::timestamp
12
+ attr_reader cool_off_time: Domain::duration_seconds
13
13
  attr_reader state_key: String
14
14
 
15
15
  def initialize: (
@@ -17,7 +17,7 @@ module Stoplight
17
17
  redis: redis,
18
18
  scripting: Scripting,
19
19
  key_space: Key,
20
- cool_off_time: Domain::timestamp
20
+ cool_off_time: Domain::duration_seconds
21
21
  ) -> void
22
22
 
23
23
  def transition_to_green: -> bool
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: 6.0.0.rc3
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tëma Bolshakov
@@ -145,6 +145,7 @@ files:
145
145
  - lib/stoplight/infrastructure/redis/storage/registry.rb
146
146
  - lib/stoplight/infrastructure/redis/storage/scripting.rb
147
147
  - lib/stoplight/infrastructure/redis/storage/state.rb
148
+ - lib/stoplight/infrastructure/redis/storage/state/state_snapshot.lua
148
149
  - lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua
149
150
  - lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua
150
151
  - lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua
@@ -344,7 +345,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
345
  - !ruby/object:Gem::Version
345
346
  version: '0'
346
347
  requirements: []
347
- rubygems_version: 4.0.16
348
+ rubygems_version: 4.0.20
348
349
  specification_version: 4
349
350
  summary: Traffic control for code.
350
351
  test_files: []