ci-queue 0.21.1 → 0.23.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: bb09b44ca70cde6218873faed3b7067295088a115b2b33ddc93b3853ad318042
4
- data.tar.gz: d0eb4a637fb89aa8c2858e5f1c363b86518cb310729d417fc6e7847c7bd261b8
3
+ metadata.gz: 652049f77362ba8fa3fd01b79a0260c5ff17ba678751ee2424de463afe035d38
4
+ data.tar.gz: 745b2c493350d2315a10f36147d5d3b4cc74df74d5f38e82d2250f10be219d4a
5
5
  SHA512:
6
- metadata.gz: e3b35848df951df70cddc2bab778f5160dc0293f09cc66218b31567ce8b2fe7e7a020f5aeec9fb2b6890e53a175b70ada164b404306ee5582cc1a40601515643
7
- data.tar.gz: 8f5ffdaaad075ddcc724117f3b2414f7a15b49509d0b0df5b4415827b6452efc251974398a58b2203c776fdb70819ab53c8042ce4d3add056b245ff8cafed179
6
+ metadata.gz: d47070eca76cfd6191cc003191af153ba2b40596dca4c3408325389a110ae1b798e99dcdb7e27b33c23d5bf457d6eab003dd608602a5df9de38741408d7bb48a
7
+ data.tar.gz: 0d2f4f69af4fc3714dbd6d5660d0e3106890a7bd5cef5782bae41847aa1552b35a8c9af6e77b7d8d18264b2100463062cb205f7ed45cef7646aeb2ff4ffe3bd3
data/README.md CHANGED
@@ -70,3 +70,9 @@ rspec-queue --queue redis://example.com --timeout 600 --report
70
70
  #### Limitations
71
71
 
72
72
  Because of how `ci-queue` executes the examples, `before(:all)` and `after(:all)` hooks are not supported. `rspec-queue` will explicitly reject them.
73
+
74
+ ## Custom Redis Expiry
75
+
76
+ `ci-queue` expects the Redis server to have an [eviction policy](https://redis.io/docs/manual/eviction/#eviction-policies) of `allkeys-lru`.
77
+
78
+ You can also use `--redis-ttl` to set a custom expiration time for all CI Queue keys, this defaults to 8 hours (28,800 seconds)
data/ci-queue.gemspec CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'bundler'
33
33
  spec.add_development_dependency 'rake'
34
34
  spec.add_development_dependency 'minitest', ENV.fetch('MINITEST_VERSION', '~> 5.11')
35
- spec.add_development_dependency 'rspec', '~> 3.7.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.10'
36
36
  spec.add_development_dependency 'redis'
37
37
  spec.add_development_dependency 'simplecov', '~> 0.12'
38
38
  spec.add_development_dependency 'minitest-reporters', '~> 1.1'
@@ -31,6 +31,10 @@ module CI
31
31
  Static.new(first_half + [config.failing_test], config).populate(@all_tests)
32
32
  end
33
33
 
34
+ def release!
35
+ # noop
36
+ end
37
+
34
38
  def failed!
35
39
  @tests = first_half
36
40
  end
@@ -11,6 +11,10 @@ module CI
11
11
  false
12
12
  end
13
13
 
14
+ def release!
15
+ # noop
16
+ end
17
+
14
18
  def flaky?(test)
15
19
  @config.flaky?(test)
16
20
  end
@@ -5,7 +5,7 @@ module CI
5
5
  attr_accessor :timeout, :worker_id, :max_requeues, :grind_count, :failure_file
6
6
  attr_accessor :requeue_tolerance, :namespace, :failing_test, :statsd_endpoint
7
7
  attr_accessor :max_test_duration, :max_test_duration_percentile, :track_test_duration
8
- attr_accessor :max_test_failed
8
+ attr_accessor :max_test_failed, :redis_ttl
9
9
  attr_reader :circuit_breakers
10
10
  attr_writer :seed, :build_id
11
11
  attr_writer :queue_init_timeout
@@ -18,6 +18,7 @@ module CI
18
18
  seed: env['CIRCLE_SHA1'] || env['BUILDKITE_COMMIT'] || env['TRAVIS_COMMIT'] || env['HEROKU_TEST_RUN_COMMIT_VERSION'] || env['SEMAPHORE_GIT_SHA'],
19
19
  flaky_tests: load_flaky_tests(env['CI_QUEUE_FLAKY_TESTS']),
20
20
  statsd_endpoint: env['CI_QUEUE_STATSD_ADDR'],
21
+ redis_ttl: env['CI_QUEUE_REDIS_TTL']&.to_i || 8 * 60 * 60,
21
22
  )
22
23
  end
23
24
 
@@ -34,7 +35,7 @@ module CI
34
35
  namespace: nil, seed: nil, flaky_tests: [], statsd_endpoint: nil, max_consecutive_failures: nil,
35
36
  grind_count: nil, max_duration: nil, failure_file: nil, max_test_duration: nil,
36
37
  max_test_duration_percentile: 0.5, track_test_duration: false, max_test_failed: nil,
37
- queue_init_timeout: nil
38
+ queue_init_timeout: nil, redis_ttl: 8 * 60 * 60
38
39
  )
39
40
  @build_id = build_id
40
41
  @circuit_breakers = [CircuitBreaker::Disabled]
@@ -55,6 +56,7 @@ module CI
55
56
  @worker_id = worker_id
56
57
  self.max_consecutive_failures = max_consecutive_failures
57
58
  self.max_duration = max_duration
59
+ @redis_ttl = redis_ttl
58
60
  end
59
61
 
60
62
  def queue_init_timeout
@@ -1,8 +1,10 @@
1
1
  -- AUTOGENERATED FILE DO NOT EDIT DIRECTLY
2
2
  local zset_key = KEYS[1]
3
3
  local processed_key = KEYS[2]
4
+ local owners_key = KEYS[3]
4
5
 
5
6
  local test = ARGV[1]
6
7
 
7
8
  redis.call('zrem', zset_key, test)
9
+ redis.call('hdel', owners_key, test) -- Doesn't matter if it was reclaimed by another workers
8
10
  return redis.call('sadd', processed_key, test)
@@ -21,16 +21,16 @@ module CI
21
21
  end
22
22
 
23
23
  def size
24
- redis.multi do
25
- redis.llen(key('queue'))
26
- redis.zcard(key('running'))
24
+ redis.multi do |transaction|
25
+ transaction.llen(key('queue'))
26
+ transaction.zcard(key('running'))
27
27
  end.inject(:+)
28
28
  end
29
29
 
30
30
  def to_a
31
- redis.multi do
32
- redis.lrange(key('queue'), 0, -1)
33
- redis.zrange(key('running'), 0, -1)
31
+ redis.multi do |transaction|
32
+ transaction.lrange(key('queue'), 0, -1)
33
+ transaction.zrange(key('running'), 0, -1)
34
34
  end.flatten.reverse.map { |k| index.fetch(k) }
35
35
  end
36
36
 
@@ -22,9 +22,9 @@ module CI
22
22
  end
23
23
 
24
24
  def pop_warnings
25
- warnings = redis.multi do
26
- redis.lrange(key('warnings'), 0, -1)
27
- redis.del(key('warnings'))
25
+ warnings = redis.multi do |transaction|
26
+ transaction.lrange(key('warnings'), 0, -1)
27
+ transaction.del(key('warnings'))
28
28
  end.first
29
29
 
30
30
  warnings.map { |p| Marshal.load(p) }
@@ -35,21 +35,22 @@ module CI
35
35
  end
36
36
 
37
37
  def record_error(id, payload, stats: nil)
38
- redis.pipelined do
39
- redis.hset(
38
+ redis.pipelined do |pipeline|
39
+ pipeline.hset(
40
40
  key('error-reports'),
41
41
  id.dup.force_encoding(Encoding::BINARY),
42
42
  payload.dup.force_encoding(Encoding::BINARY),
43
43
  )
44
- record_stats(stats)
44
+ pipeline.expire(key('error-reports'), config.redis_ttl)
45
+ record_stats(stats, pipeline: pipeline)
45
46
  end
46
47
  nil
47
48
  end
48
49
 
49
50
  def record_success(id, stats: nil)
50
- redis.pipelined do
51
- redis.hdel(key('error-reports'), id.dup.force_encoding(Encoding::BINARY))
52
- record_stats(stats)
51
+ redis.pipelined do |pipeline|
52
+ pipeline.hdel(key('error-reports'), id.dup.force_encoding(Encoding::BINARY))
53
+ record_stats(stats, pipeline: pipeline)
53
54
  end
54
55
  nil
55
56
  end
@@ -65,8 +66,8 @@ module CI
65
66
  end
66
67
 
67
68
  def fetch_stats(stat_names)
68
- counts = redis.pipelined do
69
- stat_names.each { |c| redis.hvals(key(c)) }
69
+ counts = redis.pipelined do |pipeline|
70
+ stat_names.each { |c| pipeline.hvals(key(c)) }
70
71
  end
71
72
  sum_counts = counts.map do |values|
72
73
  values.map(&:to_f).inject(:+).to_f
@@ -75,9 +76,9 @@ module CI
75
76
  end
76
77
 
77
78
  def reset_stats(stat_names)
78
- redis.pipelined do
79
+ redis.pipelined do |pipeline|
79
80
  stat_names.each do |stat_name|
80
- redis.hdel(key(stat_name), config.worker_id)
81
+ pipeline.hdel(key(stat_name), config.worker_id)
81
82
  end
82
83
  end
83
84
  end
@@ -86,10 +87,11 @@ module CI
86
87
 
87
88
  attr_reader :config, :redis
88
89
 
89
- def record_stats(stats)
90
+ def record_stats(stats, pipeline: redis)
90
91
  return unless stats
91
92
  stats.each do |stat_name, stat_value|
92
- redis.hset(key(stat_name), config.worker_id, stat_value)
93
+ pipeline.hset(key(stat_name), config.worker_id, stat_value)
94
+ pipeline.expire(key(stat_name), config.redis_ttl)
93
95
  end
94
96
  end
95
97
 
@@ -11,12 +11,13 @@ module CI
11
11
  end
12
12
 
13
13
  def record_error(payload, stats: nil)
14
- redis.pipelined do
15
- redis.lpush(
14
+ redis.pipelined do |pipeline|
15
+ pipeline.lpush(
16
16
  key('error-reports'),
17
17
  payload.force_encoding(Encoding::BINARY),
18
18
  )
19
- record_stats(stats)
19
+ pipeline.expire(key('error-reports'), config.redis_ttl)
20
+ record_stats(stats, pipeline: pipeline)
20
21
  end
21
22
  nil
22
23
  end
@@ -34,8 +35,8 @@ module CI
34
35
  end
35
36
 
36
37
  def fetch_stats(stat_names)
37
- counts = redis.pipelined do
38
- stat_names.each { |c| redis.hvals(key(c)) }
38
+ counts = redis.pipelined do |pipeline|
39
+ stat_names.each { |c| pipeline.hvals(key(c)) }
39
40
  end
40
41
  stat_names.zip(counts.map { |values| values.map(&:to_f).inject(:+).to_f }).to_h
41
42
  end
@@ -54,10 +55,11 @@ module CI
54
55
  ['build', config.build_id, *args].join(':')
55
56
  end
56
57
 
57
- def record_stats(stats)
58
+ def record_stats(stats, pipeline: redis)
58
59
  return unless stats
59
60
  stats.each do |stat_name, stat_value|
60
- redis.hset(key(stat_name), config.worker_id, stat_value)
61
+ pipeline.hset(key(stat_name), config.worker_id, stat_value)
62
+ pipeline.expire(key(stat_name), config.redis_ttl)
61
63
  end
62
64
  end
63
65
  end
@@ -0,0 +1,16 @@
1
+ -- AUTOGENERATED FILE DO NOT EDIT DIRECTLY
2
+ local zset_key = KEYS[1]
3
+ local worker_queue_key = KEYS[2]
4
+ local owners_key = KEYS[3]
5
+
6
+ -- owned_tests = {"SomeTest", "worker:1", "SomeOtherTest", "worker:2", ...}
7
+ local owned_tests = redis.call('hgetall', owners_key)
8
+ for index, owner_or_test in ipairs(owned_tests) do
9
+ if owner_or_test == worker_queue_key then -- If we owned a test
10
+ local test = owned_tests[index - 1]
11
+ redis.call('zadd', zset_key, "0", test) -- We expire the lease immediately
12
+ return nil
13
+ end
14
+ end
15
+
16
+ return nil
@@ -3,12 +3,18 @@ local processed_key = KEYS[1]
3
3
  local requeues_count_key = KEYS[2]
4
4
  local queue_key = KEYS[3]
5
5
  local zset_key = KEYS[4]
6
+ local worker_queue_key = KEYS[5]
7
+ local owners_key = KEYS[6]
6
8
 
7
9
  local max_requeues = tonumber(ARGV[1])
8
10
  local global_max_requeues = tonumber(ARGV[2])
9
11
  local test = ARGV[3]
10
12
  local offset = ARGV[4]
11
13
 
14
+ if redis.call('hget', owners_key, test) == worker_queue_key then
15
+ redis.call('hdel', owners_key, test)
16
+ end
17
+
12
18
  if redis.call('sismember', processed_key, test) == 1 then
13
19
  return false
14
20
  end
@@ -3,6 +3,7 @@ local queue_key = KEYS[1]
3
3
  local zset_key = KEYS[2]
4
4
  local processed_key = KEYS[3]
5
5
  local worker_queue_key = KEYS[4]
6
+ local owners_key = KEYS[5]
6
7
 
7
8
  local current_time = ARGV[1]
8
9
 
@@ -10,6 +11,7 @@ local test = redis.call('rpop', queue_key)
10
11
  if test then
11
12
  redis.call('zadd', zset_key, current_time, test)
12
13
  redis.call('lpush', worker_queue_key, test)
14
+ redis.call('hset', owners_key, test, worker_queue_key)
13
15
  return test
14
16
  else
15
17
  return nil
@@ -2,6 +2,7 @@
2
2
  local zset_key = KEYS[1]
3
3
  local processed_key = KEYS[2]
4
4
  local worker_queue_key = KEYS[3]
5
+ local owners_key = KEYS[4]
5
6
 
6
7
  local current_time = ARGV[1]
7
8
  local timeout = ARGV[2]
@@ -11,6 +12,7 @@ for _, test in ipairs(lost_tests) do
11
12
  if redis.call('sismember', processed_key, test) == 0 then
12
13
  redis.call('zadd', zset_key, current_time, test)
13
14
  redis.call('lpush', worker_queue_key, test)
15
+ redis.call('hset', owners_key, test, worker_queue_key) -- Take ownership
14
16
  return test
15
17
  end
16
18
  end
@@ -19,38 +19,37 @@ module CI
19
19
  attr_reader :redis
20
20
 
21
21
  def record_test_time(test_name, duration)
22
- redis.pipelined do
23
- redis.lpush(
22
+ redis.pipelined do |pipeline|
23
+ pipeline.lpush(
24
24
  test_time_key(test_name),
25
25
  duration.to_s.force_encoding(Encoding::BINARY),
26
26
  )
27
+ pipeline.expire(test_time_key(test_name), config.redis_ttl)
27
28
  end
28
29
  nil
29
30
  end
30
31
 
31
32
  def record_test_name(test_name)
32
- redis.pipelined do
33
- redis.lpush(
33
+ redis.pipelined do |pipeline|
34
+ pipeline.lpush(
34
35
  all_test_names_key,
35
36
  test_name.dup.force_encoding(Encoding::BINARY),
36
37
  )
38
+ pipeline.expire(all_test_names_key, config.redis_ttl)
37
39
  end
38
40
  nil
39
41
  end
40
42
 
41
43
  def fetch_all_test_names
42
- values = redis.pipelined do
43
- redis.lrange(all_test_names_key, 0, -1)
44
+ values = redis.pipelined do |pipeline|
45
+ pipeline.lrange(all_test_names_key, 0, -1)
44
46
  end
45
47
  values.flatten.map(&:to_s)
46
48
  end
47
49
 
48
50
  def fetch_test_time(test_name)
49
- values = redis.pipelined do
50
- key = test_time_key(test_name)
51
- redis.lrange(key, 0, -1)
52
- end
53
- values.flatten.map(&:to_f)
51
+ key = test_time_key(test_name)
52
+ redis.lrange(key, 0, -1).map(&:to_f)
54
53
  end
55
54
 
56
55
  def all_test_names_key
@@ -53,6 +53,10 @@ module CI
53
53
  sleep 0.05
54
54
  end
55
55
  end
56
+ redis.pipelined do |pipeline|
57
+ pipeline.expire(key('worker', worker_id, 'queue'), config.redis_ttl)
58
+ pipeline.expire(key('processed'), config.redis_ttl)
59
+ end
56
60
  rescue *CONNECTION_ERRORS
57
61
  end
58
62
 
@@ -92,7 +96,7 @@ module CI
92
96
  raise_on_mismatching_test(test_key)
93
97
  eval_script(
94
98
  :acknowledge,
95
- keys: [key('running'), key('processed')],
99
+ keys: [key('running'), key('processed'), key('owners')],
96
100
  argv: [test_key],
97
101
  ) == 1
98
102
  end
@@ -104,7 +108,14 @@ module CI
104
108
 
105
109
  requeued = config.max_requeues > 0 && global_max_requeues > 0 && eval_script(
106
110
  :requeue,
107
- keys: [key('processed'), key('requeues-count'), key('queue'), key('running')],
111
+ keys: [
112
+ key('processed'),
113
+ key('requeues-count'),
114
+ key('queue'),
115
+ key('running'),
116
+ key('worker', worker_id, 'queue'),
117
+ key('owners'),
118
+ ],
108
119
  argv: [config.max_requeues, global_max_requeues, test_key, offset],
109
120
  ) == 1
110
121
 
@@ -112,6 +123,15 @@ module CI
112
123
  requeued
113
124
  end
114
125
 
126
+ def release!
127
+ eval_script(
128
+ :release,
129
+ keys: [key('running'), key('worker', worker_id, 'queue'), key('owners')],
130
+ argv: [],
131
+ )
132
+ nil
133
+ end
134
+
115
135
  private
116
136
 
117
137
  attr_reader :index
@@ -144,7 +164,13 @@ module CI
144
164
  def try_to_reserve_test
145
165
  eval_script(
146
166
  :reserve,
147
- keys: [key('queue'), key('running'), key('processed'), key('worker', worker_id, 'queue')],
167
+ keys: [
168
+ key('queue'),
169
+ key('running'),
170
+ key('processed'),
171
+ key('worker', worker_id, 'queue'),
172
+ key('owners'),
173
+ ],
148
174
  argv: [Time.now.to_f],
149
175
  )
150
176
  end
@@ -152,7 +178,12 @@ module CI
152
178
  def try_to_reserve_lost_test
153
179
  lost_test = eval_script(
154
180
  :reserve_lost,
155
- keys: [key('running'), key('completed'), key('worker', worker_id, 'queue')],
181
+ keys: [
182
+ key('running'),
183
+ key('completed'),
184
+ key('worker', worker_id, 'queue'),
185
+ key('owners'),
186
+ ],
156
187
  argv: [Time.now.to_f, timeout],
157
188
  )
158
189
 
@@ -167,13 +198,18 @@ module CI
167
198
  @total = tests.size
168
199
 
169
200
  if @master = redis.setnx(key('master-status'), 'setup')
170
- redis.multi do
171
- redis.lpush(key('queue'), tests) unless tests.empty?
172
- redis.set(key('total'), @total)
173
- redis.set(key('master-status'), 'ready')
201
+ redis.multi do |transaction|
202
+ transaction.lpush(key('queue'), tests) unless tests.empty?
203
+ transaction.set(key('total'), @total)
204
+ transaction.set(key('master-status'), 'ready')
205
+
206
+ transaction.expire(key('queue'), config.redis_ttl)
207
+ transaction.expire(key('total'), config.redis_ttl)
208
+ transaction.expire(key('master-status'), config.redis_ttl)
174
209
  end
175
210
  end
176
211
  register
212
+ redis.expire(key('workers'), config.redis_ttl)
177
213
  rescue *CONNECTION_ERRORS
178
214
  raise if @master
179
215
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CI
4
4
  module Queue
5
- VERSION = '0.21.1'
5
+ VERSION = '0.23.0'
6
6
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
7
7
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
8
8
  end
@@ -22,6 +22,9 @@ module Minitest
22
22
  def initialize(argv)
23
23
  @queue_config = CI::Queue::Configuration.from_env(ENV)
24
24
  @command, @argv = parse(argv)
25
+ if Minitest.respond_to?(:seed=)
26
+ Minitest.seed = @queue_config.seed.to_i
27
+ end
25
28
  end
26
29
 
27
30
  def run!
@@ -81,6 +84,10 @@ module Minitest
81
84
  # Let minitest's at_exit hook trigger
82
85
  end
83
86
 
87
+ def release_command
88
+ queue.release!
89
+ end
90
+
84
91
  def grind_command
85
92
  invalid_usage!('No list to grind provided') if grind_list.nil?
86
93
  invalid_usage!('No grind count provided') if grind_count.nil?
@@ -483,6 +490,14 @@ module Minitest
483
490
  end
484
491
  end
485
492
 
493
+ help = <<~EOS
494
+ Defines how long the test report remain after the test run, in seconds.
495
+ Defaults to 28,800 (8 hours)
496
+ EOS
497
+ opts.on("--redis-ttl SECONDS", Integer, help) do |time|
498
+ queue.config.redis_ttl = time
499
+ end
500
+
486
501
  opts.separator ""
487
502
  opts.separator " retry: Replays a previous run in the same order."
488
503
 
data/lib/rspec/queue.rb CHANGED
@@ -157,6 +157,15 @@ module RSpec
157
157
  queue_config.max_consecutive_failures = Integer(max)
158
158
  end
159
159
 
160
+ help = <<~EOS
161
+ Defines how long the test report remain after the test run, in seconds.
162
+ Defaults to 28,800 (8 hours)
163
+ EOS
164
+ parser.separator ""
165
+ parser.on("--redis-ttl SECONDS", Integer, help) do |time|
166
+ queue.config.redis_ttl = time
167
+ end
168
+
160
169
  parser
161
170
  end
162
171
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ci-queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.1
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-20 00:00:00.000000000 Z
11
+ date: 2022-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 3.7.0
61
+ version: '3.10'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 3.7.0
68
+ version: '3.10'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: redis
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -187,6 +187,7 @@ files:
187
187
  - lib/ci/queue/redis/grind.rb
188
188
  - lib/ci/queue/redis/grind_record.rb
189
189
  - lib/ci/queue/redis/grind_supervisor.rb
190
+ - lib/ci/queue/redis/release.lua
190
191
  - lib/ci/queue/redis/requeue.lua
191
192
  - lib/ci/queue/redis/reserve.lua
192
193
  - lib/ci/queue/redis/reserve_lost.lua
@@ -238,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
239
  - !ruby/object:Gem::Version
239
240
  version: '0'
240
241
  requirements: []
241
- rubygems_version: 3.2.20
242
+ rubygems_version: 3.3.3
242
243
  signing_key:
243
244
  specification_version: 4
244
245
  summary: Distribute tests over many workers using a queue