minitest-distributed 0.2.12 → 0.2.13

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: 722ec2cc00da4f9c5f5efc0591e39c2becc2338ce7fe171c60339d284116242b
4
- data.tar.gz: aa1d3c52afa3b16c649a0b12d27e83ccb19ae11dd80fb68d3e57a669e0a081e5
3
+ metadata.gz: 1605e583ce4427da1909789380c1ac20ffe629a5f1ddf96452f93e45d50188f2
4
+ data.tar.gz: 052ba1210bee26b878052bb28207b800a67682f023e6fdcc1e03bebaea758376
5
5
  SHA512:
6
- metadata.gz: c2cfc291aeb202bb680b04a6e579272fd907efe47ef1d5e5ea2b66ca5ffe23967ded9ea38458e485e1e9b42609d84a2058fd2df3250217cf76a60272f3b3255c
7
- data.tar.gz: cb0934c54f7c3bdcc76abe8c0593b08e2e7ddfacd9cf2c4e7ce3a71e586b2e75170be91ebe3ac8b520fb7c8c7e4e961ae4a9a218c582bf1301dc916f41427847
6
+ metadata.gz: fb550faaa06a888561a3557b10f35fc867ca341e6e4008a49eb5c5137aa79b3490e6a5b24444d67c85ffc79c2e683aff7a21f948e7867b1b78bbafeccdebe9b8
7
+ data.tar.gz: ad60584710485b804af453696912a2676d56905e69642ead265111689ea10212768925f5c48017fd3c6da7547a94e8450b181dd2f89b5db82dc480d79d843d10
@@ -153,63 +153,65 @@ module Minitest
153
153
 
154
154
  return if consumer_group_exists
155
155
 
156
- tests = T.let([], T::Array[Minitest::Runnable])
157
- tests = if initial_attempt
158
- # If this is the first attempt for this run ID, we will schedule the full
159
- # test suite as returned by the test selector to run.
160
-
161
- tests_from_selector = test_selector.tests
162
- adjust_combined_results(ResultAggregate.new(size: tests_from_selector.size))
163
- tests_from_selector
164
-
165
- elsif configuration.retry_failures
166
- # Before starting a retry attempt, we first check if the previous attempt
167
- # was aborted before it was completed. If this is the case, we cannot use
168
- # retry mode, and should immediately fail the attempt.
169
- if combined_results.abort?
170
- # We mark this run as aborted, which causes this worker to not be successful.
171
- @aborted = true
172
-
173
- # We still publish an empty size run to Redis, so if there are any followers,
174
- # they will wind down normally. Only the leader will exit
175
- # with a non-zero exit status and fail the build; any follower will
176
- # exit with status 0.
177
- adjust_combined_results(ResultAggregate.new(size: 0))
178
- T.let([], T::Array[Minitest::Runnable])
179
- else
180
- previous_failures, previous_errors, _deleted = redis.multi do |pipeline|
181
- pipeline.lrange(list_key(ResultType::Failed.serialize), 0, -1)
182
- pipeline.lrange(list_key(ResultType::Error.serialize), 0, -1)
183
- pipeline.del(list_key(ResultType::Failed.serialize), list_key(ResultType::Error.serialize))
184
- end
156
+ tests = T.let(
157
+ if initial_attempt
158
+ # If this is the first attempt for this run ID, we will schedule the full
159
+ # test suite as returned by the test selector to run.
160
+
161
+ tests_from_selector = test_selector.tests
162
+ adjust_combined_results(ResultAggregate.new(size: tests_from_selector.size))
163
+ tests_from_selector
164
+
165
+ elsif configuration.retry_failures
166
+ # Before starting a retry attempt, we first check if the previous attempt
167
+ # was aborted before it was completed. If this is the case, we cannot use
168
+ # retry mode, and should immediately fail the attempt.
169
+ if combined_results.abort?
170
+ # We mark this run as aborted, which causes this worker to not be successful.
171
+ @aborted = true
172
+
173
+ # We still publish an empty size run to Redis, so if there are any followers,
174
+ # they will wind down normally. Only the leader will exit
175
+ # with a non-zero exit status and fail the build; any follower will
176
+ # exit with status 0.
177
+ adjust_combined_results(ResultAggregate.new(size: 0))
178
+ []
179
+ else
180
+ previous_failures, previous_errors, _deleted = redis.multi do |pipeline|
181
+ pipeline.lrange(list_key(ResultType::Failed.serialize), 0, -1)
182
+ pipeline.lrange(list_key(ResultType::Error.serialize), 0, -1)
183
+ pipeline.del(list_key(ResultType::Failed.serialize), list_key(ResultType::Error.serialize))
184
+ end
185
185
 
186
- # We set the `size` key to the number of tests we are planning to schedule.
187
- # We also adjust the number of failures and errors back to 0.
188
- # We set the number of requeues to the number of tests that failed, so the
189
- # run statistics will reflect that we retried some failed test.
190
- #
191
- # However, normally requeues are not acked, as we expect the test to be acked
192
- # by another worker later. This makes the test loop think iot is already done.
193
- # To prevent this, we initialize the number of acks negatively, so it evens out
194
- # in the statistics.
195
- total_failures = previous_failures.length + previous_errors.length
196
- adjust_combined_results(ResultAggregate.new(
197
- size: total_failures,
198
- failures: -previous_failures.length,
199
- errors: -previous_errors.length,
200
- requeues: total_failures,
201
- ))
202
-
203
- # For subsequent attempts, we check the list of previous failures and
204
- # errors, and only schedule to re-run those tests. This allows for faster
205
- # retries of potentially flaky tests.
206
- test_identifiers_to_retry = T.let(previous_failures + previous_errors, T::Array[String])
207
- test_identifiers_to_retry.map { |identifier| DefinedRunnable.from_identifier(identifier) }
208
- end
209
- else
210
- adjust_combined_results(ResultAggregate.new(size: 0))
211
- T.let([], T::Array[Minitest::Runnable])
212
- end
186
+ # We set the `size` key to the number of tests we are planning to schedule.
187
+ # We also adjust the number of failures and errors back to 0.
188
+ # We set the number of requeues to the number of tests that failed, so the
189
+ # run statistics will reflect that we retried some failed test.
190
+ #
191
+ # However, normally requeues are not acked, as we expect the test to be acked
192
+ # by another worker later. This makes the test loop think iot is already done.
193
+ # To prevent this, we initialize the number of acks negatively, so it evens out
194
+ # in the statistics.
195
+ total_failures = previous_failures.length + previous_errors.length
196
+ adjust_combined_results(ResultAggregate.new(
197
+ size: total_failures,
198
+ failures: -previous_failures.length,
199
+ errors: -previous_errors.length,
200
+ requeues: total_failures,
201
+ ))
202
+
203
+ # For subsequent attempts, we check the list of previous failures and
204
+ # errors, and only schedule to re-run those tests. This allows for faster
205
+ # retries of potentially flaky tests.
206
+ test_identifiers_to_retry = T.let(previous_failures + previous_errors, T::Array[String])
207
+ test_identifiers_to_retry.map { |identifier| DefinedRunnable.from_identifier(identifier) }
208
+ end
209
+ else
210
+ adjust_combined_results(ResultAggregate.new(size: 0))
211
+ []
212
+ end,
213
+ T::Array[Minitest::Runnable],
214
+ )
213
215
 
214
216
  redis.pipelined do |pipeline|
215
217
  tests.each do |test|
@@ -243,10 +245,17 @@ module Minitest
243
245
  # To make sure we don't end up in a busy loop overwhelming Redis with commands
244
246
  # when there is no work to do, we increase the blocking time exponentially,
245
247
  # and reset it to the initial value if we processed any tests.
246
- if stale_runnables.empty? && fresh_runnables.empty?
247
- exponential_backoff <<= 1
248
+ #
249
+ # The backoff is capped at MAX_BACKOFF to bound how long a worker can sit
250
+ # inside a single XREADGROUP BLOCK call. Without a cap, after ~15 empty
251
+ # iterations the worker is blocked in Redis for 5+ minutes and cannot
252
+ # re-check `complete?` / `abort?` until the BLOCK returns, which manifests
253
+ # as a long post-100% teardown hang when pipelined XACKs race the progress
254
+ # reporter.
255
+ exponential_backoff = if stale_runnables.empty? && fresh_runnables.empty?
256
+ next_backoff(exponential_backoff)
248
257
  else
249
- exponential_backoff = INITIAL_BACKOFF
258
+ INITIAL_BACKOFF
250
259
  end
251
260
  end
252
261
 
@@ -543,8 +552,20 @@ module Minitest
543
552
  @logger ||= T.let(Logger.new(log_path), T.nilable(Logger))
544
553
  end
545
554
 
555
+ sig { params(backoff: Integer).returns(Integer) }
556
+ def next_backoff(backoff)
557
+ [backoff << 1, MAX_BACKOFF].min
558
+ end
559
+
546
560
  INITIAL_BACKOFF = 10 # milliseconds
547
561
  private_constant :INITIAL_BACKOFF
562
+
563
+ # Cap on the XREADGROUP BLOCK timeout used by `consume`. Reached after roughly
564
+ # 9 consecutive empty iterations (10 ms * 2^9 = 5120 ms). Bounds the worst-case
565
+ # time a worker can be unresponsive to `complete?` / `abort?` after the queue
566
+ # is drained.
567
+ MAX_BACKOFF = 5_000 # milliseconds
568
+ private_constant :MAX_BACKOFF
548
569
  end
549
570
  end
550
571
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Minitest
5
5
  module Distributed
6
- VERSION = "0.2.12"
6
+ VERSION = "0.2.13"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-distributed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
4
+ version: 0.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
@@ -183,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
183
  - !ruby/object:Gem::Version
184
184
  version: '0'
185
185
  requirements: []
186
- rubygems_version: 3.7.2
186
+ rubygems_version: 4.0.12
187
187
  specification_version: 4
188
188
  summary: Distributed test executor plugin for Minitest
189
189
  test_files: []