ci-queue 0.88.0 → 0.90.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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/ci/queue/configuration.rb +5 -0
- data/lib/ci/queue/queue_entry.rb +3 -1
- data/lib/ci/queue/redis/build_record.rb +14 -0
- data/lib/ci/queue/redis/retry.rb +15 -0
- data/lib/ci/queue/redis/supervisor.rb +17 -0
- data/lib/ci/queue/redis/worker.rb +9 -0
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue/build_status_reporter.rb +10 -2
- data/lib/minitest/queue.rb +10 -1
- data/lib/rspec/queue/build_status_recorder.rb +2 -2
- data/lib/rspec/queue.rb +4 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5233596f5e87ddd8954a44a2b7c2be3d258ade0c567ff808b32d99194d89a7b
|
|
4
|
+
data.tar.gz: 1026734a1076455a5ac95821ef8a1dbba83cb8ed445720fab8ca93cdd53eb693
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68b5ce15acc1b90b3c75fb861ad8ea2ff6599c44d096a96ed5d018fa8d4eb4c6c20a9949950e31284436241d9d1df5c2917430eadef673872c9e4a693893ed20
|
|
7
|
+
data.tar.gz: a1ba50c8998de99d54620efd899bc1a471b0bf037a3cb9a9e056b36d45f551f9430a9bac1b4f46713c5e17426fc7e409be4b307f77071321a9f0789c0eca5ee8
|
data/Gemfile.lock
CHANGED
|
@@ -99,6 +99,11 @@ module CI
|
|
|
99
99
|
@lazy_load_test_helpers.split(',').map(&:strip)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
def retry?
|
|
103
|
+
ENV.fetch("BUILDKITE_RETRY_COUNT", "0").to_i > 0 ||
|
|
104
|
+
ENV["SEMAPHORE_PIPELINE_RERUN"] == "true"
|
|
105
|
+
end
|
|
106
|
+
|
|
102
107
|
def queue_init_timeout
|
|
103
108
|
@queue_init_timeout || timeout
|
|
104
109
|
end
|
data/lib/ci/queue/queue_entry.rb
CHANGED
|
@@ -17,7 +17,9 @@ module CI
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.format(test_id, file_path)
|
|
20
|
-
|
|
20
|
+
raise ArgumentError, "file_path is required for '#{test_id}' — the test file path must be resolvable" if file_path.nil? || file_path.empty?
|
|
21
|
+
canonical = load_error_payload?(file_path) ? file_path : ::File.expand_path(file_path)
|
|
22
|
+
JSON.dump({ test_id: test_id, file_path: canonical })
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def self.load_error_payload?(file_path)
|
|
@@ -145,6 +145,10 @@ module CI
|
|
|
145
145
|
redis.hgetall(key('error-reports')).transform_keys { |entry| CI::Queue::QueueEntry.test_id(entry) }
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
def failed_test_entries
|
|
149
|
+
redis.hkeys(key('error-reports'))
|
|
150
|
+
end
|
|
151
|
+
|
|
148
152
|
def flaky_reports
|
|
149
153
|
redis.smembers(key('flaky-reports')).map { |entry| CI::Queue::QueueEntry.test_id(entry) }
|
|
150
154
|
end
|
|
@@ -177,6 +181,16 @@ module CI
|
|
|
177
181
|
pipeline.hdel(key(stat_name), config.worker_id)
|
|
178
182
|
end
|
|
179
183
|
end
|
|
184
|
+
# Purge any error-report-deltas that reference this worker so that
|
|
185
|
+
# apply_error_report_delta_correction cannot double-subtract from
|
|
186
|
+
# the now-zeroed counters on a subsequent successful retry.
|
|
187
|
+
deltas = redis.hgetall(key('error-report-deltas'))
|
|
188
|
+
to_delete = deltas.filter_map do |entry, delta_json|
|
|
189
|
+
entry if JSON.parse(delta_json)['worker_id'].to_s == config.worker_id.to_s
|
|
190
|
+
rescue JSON::ParserError
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
redis.hdel(key('error-report-deltas'), *to_delete) unless to_delete.empty?
|
|
180
194
|
end
|
|
181
195
|
|
|
182
196
|
private
|
data/lib/ci/queue/redis/retry.rb
CHANGED
|
@@ -28,9 +28,24 @@ module CI
|
|
|
28
28
|
self
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Queue a Redis SADD so that BuildRecord#record_success can include this
|
|
32
|
+
# in its multi-exec transaction. Without this, Static#acknowledge returns
|
|
33
|
+
# a Ruby value (not a Redis future), shifting the result indices and
|
|
34
|
+
# breaking the stats delta correction.
|
|
35
|
+
def acknowledge(entry, error: nil, pipeline: redis)
|
|
36
|
+
@progress += 1
|
|
37
|
+
return @progress unless pipeline
|
|
38
|
+
test_id = CI::Queue::QueueEntry.test_id(entry)
|
|
39
|
+
pipeline.sadd(key('processed'), test_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
private
|
|
32
43
|
|
|
33
44
|
attr_reader :redis
|
|
45
|
+
|
|
46
|
+
def key(*args)
|
|
47
|
+
['build', config.build_id, *args].join(':')
|
|
48
|
+
end
|
|
34
49
|
end
|
|
35
50
|
end
|
|
36
51
|
end
|
|
@@ -39,6 +39,23 @@ module CI
|
|
|
39
39
|
yield if block_given?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# On retry runs (BUILDKITE_RETRY_COUNT > 0), the main queue is already
|
|
43
|
+
# exhausted from the original run. A retry worker may have found unresolved
|
|
44
|
+
# failures via the error-reports fallback and be running them via the Retry
|
|
45
|
+
# queue — but those tests are NOT in the Redis running set so active_workers?
|
|
46
|
+
# returns false and the loop above exits immediately.
|
|
47
|
+
#
|
|
48
|
+
# Wait up to inactive_workers_timeout for retry workers to clear error-reports.
|
|
49
|
+
# This prevents the summary from canceling retry workers before they finish.
|
|
50
|
+
if exhausted? && config.retry? && !rescue_connection_errors { build.failed_tests }.empty?
|
|
51
|
+
@time_left_with_no_workers = config.inactive_workers_timeout
|
|
52
|
+
until rescue_connection_errors { build.failed_tests }.empty? ||
|
|
53
|
+
@time_left_with_no_workers <= 0
|
|
54
|
+
sleep 1
|
|
55
|
+
@time_left_with_no_workers -= 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
exhausted?
|
|
43
60
|
rescue CI::Queue::Redis::LostMaster
|
|
44
61
|
false
|
|
@@ -158,6 +158,15 @@ module CI
|
|
|
158
158
|
log.select! { |entry| failures.include?(CI::Queue::QueueEntry.test_id(entry)) }
|
|
159
159
|
log.uniq! { |entry| CI::Queue::QueueEntry.test_id(entry) }
|
|
160
160
|
log.reverse!
|
|
161
|
+
|
|
162
|
+
if log.empty?
|
|
163
|
+
# Per-worker log has no matching failures — this worker didn't run
|
|
164
|
+
# the failing tests (e.g. Buildkite rebuild with new worker IDs,
|
|
165
|
+
# or a different parallel slot). Fall back to ALL unresolved
|
|
166
|
+
# failures from error-reports so any worker can retry them.
|
|
167
|
+
log = redis.hkeys(key('error-reports'))
|
|
168
|
+
end
|
|
169
|
+
|
|
161
170
|
Retry.new(log, config, redis: redis)
|
|
162
171
|
end
|
|
163
172
|
|
data/lib/ci/queue/version.rb
CHANGED
|
@@ -267,8 +267,16 @@ module Minitest
|
|
|
267
267
|
end
|
|
268
268
|
|
|
269
269
|
def aggregates
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
# error-reports is authoritative when workers die before flushing per-test stats.
|
|
271
|
+
# Floor the displayed count so the summary line is never misleadingly green.
|
|
272
|
+
known_error_count = error_reports.size
|
|
273
|
+
effective_total = [failures + errors, known_error_count].max
|
|
274
|
+
success = effective_total.zero?
|
|
275
|
+
failures_count = if failures + errors >= known_error_count
|
|
276
|
+
"#{failures} failures, #{errors} errors,"
|
|
277
|
+
else
|
|
278
|
+
"#{effective_total} failures,"
|
|
279
|
+
end
|
|
272
280
|
|
|
273
281
|
step([
|
|
274
282
|
'Ran %d tests, %d assertions,' % [progress, assertions],
|
data/lib/minitest/queue.rb
CHANGED
|
@@ -330,7 +330,16 @@ module Minitest
|
|
|
330
330
|
end
|
|
331
331
|
|
|
332
332
|
def queue_entry
|
|
333
|
-
@queue_entry ||=
|
|
333
|
+
@queue_entry ||= begin
|
|
334
|
+
unless runnable.is_a?(Module)
|
|
335
|
+
raise ArgumentError, "runnable must be a Module (got #{runnable.class}). " \
|
|
336
|
+
"Do not create SingleExample with string class names."
|
|
337
|
+
end
|
|
338
|
+
file_path = runnable.instance_method(method_name).source_location&.first
|
|
339
|
+
raise ArgumentError, "Cannot resolve source file for #{id} — " \
|
|
340
|
+
"ensure the test method is defined in a Ruby source file" if file_path.nil?
|
|
341
|
+
CI::Queue::QueueEntry.format(id, file_path)
|
|
342
|
+
end
|
|
334
343
|
end
|
|
335
344
|
|
|
336
345
|
def <=>(other)
|
|
@@ -18,13 +18,13 @@ module RSpec
|
|
|
18
18
|
|
|
19
19
|
def example_passed(notification)
|
|
20
20
|
example = notification.example
|
|
21
|
-
entry = CI::Queue::QueueEntry.format(example.id,
|
|
21
|
+
entry = CI::Queue::QueueEntry.format(example.id, example.file_path)
|
|
22
22
|
build.record_success(entry)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def example_failed(notification)
|
|
26
26
|
example = notification.example
|
|
27
|
-
entry = CI::Queue::QueueEntry.format(example.id,
|
|
27
|
+
entry = CI::Queue::QueueEntry.format(example.id, example.file_path)
|
|
28
28
|
build.record_error(entry, dump(notification))
|
|
29
29
|
end
|
|
30
30
|
|
data/lib/rspec/queue.rb
CHANGED
|
@@ -254,7 +254,10 @@ module RSpec
|
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
def queue_entry
|
|
257
|
-
@queue_entry ||=
|
|
257
|
+
@queue_entry ||= begin
|
|
258
|
+
file_path = example.metadata[:absolute_file_path] || example.file_path
|
|
259
|
+
CI::Queue::QueueEntry.format(id, file_path)
|
|
260
|
+
end
|
|
258
261
|
end
|
|
259
262
|
|
|
260
263
|
def <=>(other)
|