minitest-distributed 0.2.11 → 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1605e583ce4427da1909789380c1ac20ffe629a5f1ddf96452f93e45d50188f2
|
|
4
|
+
data.tar.gz: 052ba1210bee26b878052bb28207b800a67682f023e6fdcc1e03bebaea758376
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb550faaa06a888561a3557b10f35fc867ca341e6e4008a49eb5c5137aa79b3490e6a5b24444d67c85ffc79c2e683aff7a21f948e7867b1b78bbafeccdebe9b8
|
|
7
|
+
data.tar.gz: ad60584710485b804af453696912a2676d56905e69642ead265111689ea10212768925f5c48017fd3c6da7547a94e8450b181dd2f89b5db82dc480d79d843d10
|
data/dev.yml
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "redis"
|
|
5
5
|
require "set"
|
|
6
|
+
require "logger"
|
|
6
7
|
|
|
7
8
|
module Minitest
|
|
8
9
|
module Distributed
|
|
@@ -152,63 +153,65 @@ module Minitest
|
|
|
152
153
|
|
|
153
154
|
return if consumer_group_exists
|
|
154
155
|
|
|
155
|
-
tests = T.let(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
)
|
|
212
215
|
|
|
213
216
|
redis.pipelined do |pipeline|
|
|
214
217
|
tests.each do |test|
|
|
@@ -242,10 +245,17 @@ module Minitest
|
|
|
242
245
|
# To make sure we don't end up in a busy loop overwhelming Redis with commands
|
|
243
246
|
# when there is no work to do, we increase the blocking time exponentially,
|
|
244
247
|
# and reset it to the initial value if we processed any tests.
|
|
245
|
-
|
|
246
|
-
|
|
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)
|
|
247
257
|
else
|
|
248
|
-
|
|
258
|
+
INITIAL_BACKOFF
|
|
249
259
|
end
|
|
250
260
|
end
|
|
251
261
|
|
|
@@ -268,7 +278,25 @@ module Minitest
|
|
|
268
278
|
|
|
269
279
|
sig { returns(Redis) }
|
|
270
280
|
def redis
|
|
271
|
-
@redis ||= Redis.new(
|
|
281
|
+
@redis ||= Redis.new(
|
|
282
|
+
url: configuration.coordinator_uri.to_s,
|
|
283
|
+
middlewares: custom_middlewares,
|
|
284
|
+
custom: custom_config,
|
|
285
|
+
timeout: 2,
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
sig { returns(T.nilable(T::Array[Module])) }
|
|
290
|
+
def custom_middlewares
|
|
291
|
+
return unless ENV.key?("MINITEST_DISTRIBUTED_REDIS_LOG")
|
|
292
|
+
|
|
293
|
+
require_relative "redis_instrumentation_middleware"
|
|
294
|
+
[RedisInstrumentationMiddleware]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
sig { returns(T.nilable(T::Hash[Symbol, File])) }
|
|
298
|
+
def custom_config
|
|
299
|
+
{ log_file: logger }.compact
|
|
272
300
|
end
|
|
273
301
|
|
|
274
302
|
sig { returns(String) }
|
|
@@ -517,8 +545,27 @@ module Minitest
|
|
|
517
545
|
adjust_combined_results(batch_result_aggregate)
|
|
518
546
|
end
|
|
519
547
|
|
|
548
|
+
sig { returns(T.nilable(Logger)) }
|
|
549
|
+
def logger
|
|
550
|
+
return unless (log_path = ENV["MINITEST_DISTRIBUTED_REDIS_LOG"])
|
|
551
|
+
|
|
552
|
+
@logger ||= T.let(Logger.new(log_path), T.nilable(Logger))
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
sig { params(backoff: Integer).returns(Integer) }
|
|
556
|
+
def next_backoff(backoff)
|
|
557
|
+
[backoff << 1, MAX_BACKOFF].min
|
|
558
|
+
end
|
|
559
|
+
|
|
520
560
|
INITIAL_BACKOFF = 10 # milliseconds
|
|
521
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
|
|
522
569
|
end
|
|
523
570
|
end
|
|
524
571
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "redis"
|
|
5
|
+
|
|
6
|
+
module Minitest
|
|
7
|
+
module Distributed
|
|
8
|
+
module Coordinators
|
|
9
|
+
# Redis middleware that logs all Redis commands to a file for debugging.
|
|
10
|
+
module RedisInstrumentationMiddleware
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(command: T::Array[T.untyped], redis_config: T.untyped).returns(T.untyped) }
|
|
14
|
+
def call(command, redis_config)
|
|
15
|
+
log_file = redis_config.custom[:log_file]
|
|
16
|
+
log_file.info("EXEC: #{command.inspect}")
|
|
17
|
+
result = super
|
|
18
|
+
log_file.info("RESULT: #{result.inspect}")
|
|
19
|
+
result
|
|
20
|
+
rescue => e
|
|
21
|
+
log_file.info("ERROR: #{e.class}")
|
|
22
|
+
Kernel.raise
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(commands: T::Array[T.untyped], redis_config: T.untyped).returns(T.untyped) }
|
|
26
|
+
def call_pipelined(commands, redis_config)
|
|
27
|
+
log_file = redis_config.custom[:log_file]
|
|
28
|
+
log_file.info("EXEC PIPELINED: #{commands.inspect}")
|
|
29
|
+
result = super
|
|
30
|
+
log_file.info("RESULT PIPELINED: #{result.inspect}")
|
|
31
|
+
result
|
|
32
|
+
rescue => e
|
|
33
|
+
log_file.info("ERROR PIPELINED: #{e.class}")
|
|
34
|
+
Kernel.raise
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minitest-distributed
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Willem van Bergen
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: minitest
|
|
@@ -116,6 +115,7 @@ files:
|
|
|
116
115
|
- lib/minitest/distributed/coordinators/coordinator_interface.rb
|
|
117
116
|
- lib/minitest/distributed/coordinators/memory_coordinator.rb
|
|
118
117
|
- lib/minitest/distributed/coordinators/redis_coordinator.rb
|
|
118
|
+
- lib/minitest/distributed/coordinators/redis_instrumentation_middleware.rb
|
|
119
119
|
- lib/minitest/distributed/enqueued_runnable.rb
|
|
120
120
|
- lib/minitest/distributed/filters/exclude_file_filter.rb
|
|
121
121
|
- lib/minitest/distributed/filters/exclude_filter.rb
|
|
@@ -169,7 +169,6 @@ metadata:
|
|
|
169
169
|
allowed_push_host: https://rubygems.org
|
|
170
170
|
homepage_uri: https://github.com/Shopify/minitest-distributed
|
|
171
171
|
source_code_uri: https://github.com/Shopify/minitest-distributed
|
|
172
|
-
post_install_message:
|
|
173
172
|
rdoc_options: []
|
|
174
173
|
require_paths:
|
|
175
174
|
- lib
|
|
@@ -184,8 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
184
183
|
- !ruby/object:Gem::Version
|
|
185
184
|
version: '0'
|
|
186
185
|
requirements: []
|
|
187
|
-
rubygems_version:
|
|
188
|
-
signing_key:
|
|
186
|
+
rubygems_version: 4.0.12
|
|
189
187
|
specification_version: 4
|
|
190
188
|
summary: Distributed test executor plugin for Minitest
|
|
191
189
|
test_files: []
|