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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac982ecc1b30c78d14e45bbbe7d8e26841879eae6fec791dd82508159ce6c8dd
4
- data.tar.gz: 7f960c1a77b8e93bd8c18894b86f428df16f181f671bb7e000063a968e50374c
3
+ metadata.gz: c5233596f5e87ddd8954a44a2b7c2be3d258ade0c567ff808b32d99194d89a7b
4
+ data.tar.gz: 1026734a1076455a5ac95821ef8a1dbba83cb8ed445720fab8ca93cdd53eb693
5
5
  SHA512:
6
- metadata.gz: 1582e95243e4939268acc7bbaced2fc90471b0b86934d35169647ccde1e19c8d03027650e1f9efa909c2d6aafc85c8cce8a251c65de3cecdc906e64da8bb2a0b
7
- data.tar.gz: d8c67176e15c7d78b175af35309bab1136a3053da0db9404449d5770b764bba1e0d25c6671607c090d5e8951e65711ea94eded4cd59f50032b8336fea1eeed7b
6
+ metadata.gz: 68b5ce15acc1b90b3c75fb861ad8ea2ff6599c44d096a96ed5d018fa8d4eb4c6c20a9949950e31284436241d9d1df5c2917430eadef673872c9e4a693893ed20
7
+ data.tar.gz: a1ba50c8998de99d54620efd899bc1a471b0bf037a3cb9a9e056b36d45f551f9430a9bac1b4f46713c5e17426fc7e409be4b307f77071321a9f0789c0eca5ee8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ci-queue (0.88.0)
4
+ ci-queue (0.90.0)
5
5
  logger
6
6
 
7
7
  GEM
@@ -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
@@ -17,7 +17,9 @@ module CI
17
17
  end
18
18
 
19
19
  def self.format(test_id, file_path)
20
- JSON.dump({ test_id: test_id, file_path: file_path })
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
@@ -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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CI
4
4
  module Queue
5
- VERSION = '0.88.0'
5
+ VERSION = '0.90.0'
6
6
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
7
7
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
8
8
  end
@@ -267,8 +267,16 @@ module Minitest
267
267
  end
268
268
 
269
269
  def aggregates
270
- success = failures.zero? && errors.zero?
271
- failures_count = "#{failures} failures, #{errors} errors,"
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],
@@ -330,7 +330,16 @@ module Minitest
330
330
  end
331
331
 
332
332
  def queue_entry
333
- @queue_entry ||= CI::Queue::QueueEntry.format(id, nil)
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, nil)
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, nil)
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 ||= CI::Queue::QueueEntry.format(id, nil)
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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ci-queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.88.0
4
+ version: 0.90.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier