From 19c8ac709740ecb8688fa786d2b5f31fdb16846b Mon Sep 17 00:00:00 2001 From: Yicheng Fang Date: Mon, 10 Aug 2026 21:03:02 +0000 Subject: [PATCH 1/5] ruby: Replay worker history with retry queue --- ruby/README.md | 17 +++++ ruby/lib/ci/queue/configuration.rb | 10 ++- ruby/lib/ci/queue/redis.rb | 1 + ruby/lib/ci/queue/redis/retry.rb | 21 +++++- ruby/lib/ci/queue/redis/worker.rb | 44 +++++++++++- ruby/lib/ci/queue/static.rb | 7 +- ruby/lib/minitest/queue.rb | 1 + ruby/lib/minitest/queue/recovery_reporter.rb | 56 ++++++++++++++++ ruby/lib/minitest/queue/runner.rb | 49 +++++++++++++- ruby/test/ci/queue/configuration_test.rb | 3 + ruby/test/ci/queue/redis/worker_chunk_test.rb | 56 ++++++++++++++++ ruby/test/ci/queue/static_test.rb | 12 ++++ ruby/test/integration/minitest_redis_test.rb | 67 +++++++++++++++++++ .../minitest/queue/recovery_reporter_test.rb | 63 +++++++++++++++++ 14 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 ruby/lib/minitest/queue/recovery_reporter.rb create mode 100644 ruby/test/minitest/queue/recovery_reporter_test.rb diff --git a/ruby/README.md b/ruby/README.md index 92ba5b99..849ff0aa 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -38,6 +38,23 @@ minitest-queue --queue redis://example.com run -Itest test/**/*_test.rb Additionally you can configure the requeue settings (see main README) with `--max-requeues` and `--requeue-tolerance`. +#### Replaying a retried worker for coverage + +By default, a retried worker runs only tests recorded as failed. For process-local +artifacts such as SimpleCov output, use worker-history mode to replay every test +previously reserved by that worker: + +```bash +minitest-queue --queue redis://example.com run \ + --retry-mode worker-history \ + --recovery-manifest tmp/ci-queue-recovery.json \ + -Itest test/**/*_test.rb +``` + +The replay uses the existing local retry queue and exits after the worker history +is exhausted. It does not claim additional work from the shared Redis queue. The +manifest is written atomically only after a complete run. + If you'd like to centralize the error reporting you can do so with: diff --git a/ruby/lib/ci/queue/configuration.rb b/ruby/lib/ci/queue/configuration.rb index 9e9c1339..522d42eb 100644 --- a/ruby/lib/ci/queue/configuration.rb +++ b/ruby/lib/ci/queue/configuration.rb @@ -15,6 +15,7 @@ class Configuration attr_accessor :timing_redis_url attr_accessor :write_duration_averages attr_accessor :heartbeat_grace_period, :heartbeat_interval + attr_accessor :retry_mode, :recovery_manifest, :retry_count attr_reader :circuit_breakers attr_writer :seed, :build_id attr_writer :queue_init_timeout, :report_timeout, :inactive_workers_timeout @@ -30,6 +31,7 @@ def from_env(env) redis_ttl: env['CI_QUEUE_REDIS_TTL']&.to_i || 8 * 60 * 60, known_flaky_tests: load_known_flaky_tests(env['CI_QUEUE_KNOWN_FLAKY_TESTS']), branch: env['BUILDKITE_BRANCH'], + retry_count: env['BUILDKITE_RETRY_COUNT'].to_i, ) end @@ -66,7 +68,10 @@ def initialize( branch: nil, timing_redis_url: nil, heartbeat_grace_period: 30, - heartbeat_interval: 10 + heartbeat_interval: 10, + retry_mode: :failures, + recovery_manifest: nil, + retry_count: 0 ) @build_id = build_id @circuit_breakers = [CircuitBreaker::Disabled] @@ -105,6 +110,9 @@ def initialize( @write_duration_averages = false @heartbeat_grace_period = heartbeat_grace_period @heartbeat_interval = heartbeat_interval + @retry_mode = retry_mode + @recovery_manifest = recovery_manifest + @retry_count = retry_count end def queue_init_timeout diff --git a/ruby/lib/ci/queue/redis.rb b/ruby/lib/ci/queue/redis.rb index c3876efe..6602e14a 100644 --- a/ruby/lib/ci/queue/redis.rb +++ b/ruby/lib/ci/queue/redis.rb @@ -18,6 +18,7 @@ module Queue module Redis Error = Class.new(StandardError) LostMaster = Class.new(Error) + WorkerHistoryError = Class.new(Error) class << self diff --git a/ruby/lib/ci/queue/redis/retry.rb b/ruby/lib/ci/queue/redis/retry.rb index 85bcc27e..8e5aaf9e 100644 --- a/ruby/lib/ci/queue/redis/retry.rb +++ b/ruby/lib/ci/queue/redis/retry.rb @@ -3,11 +3,30 @@ module CI module Queue module Redis class Retry < Static - def initialize(tests, config, redis:) + attr_reader :history_items, :replayed_tests + + def initialize(tests, config, redis:, history_items: 0, worker_history: false) @redis = redis + @history_items = history_items + @replayed_tests = worker_history ? tests.size : 0 + @worker_history = worker_history + @worker_history_complete = false super(tests, config) end + def worker_history? + @worker_history + end + + def worker_history_complete? + @worker_history_complete + end + + def poll(&block) + super + @worker_history_complete = exhausted? if worker_history? + end + def build @build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config) end diff --git a/ruby/lib/ci/queue/redis/worker.rb b/ruby/lib/ci/queue/redis/worker.rb index 7de1f17c..47425c0b 100644 --- a/ruby/lib/ci/queue/redis/worker.rb +++ b/ruby/lib/ci/queue/redis/worker.rb @@ -145,7 +145,10 @@ def retrying? end end - def retry_queue + def retry_queue(scope: :failures) + return worker_history_retry_queue if scope == :worker_history + raise ArgumentError, "Unknown retry scope: #{scope}" unless scope == :failures + failures = build.failed_tests.to_set log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1) log.select! { |id| failures.include?(id) } @@ -264,6 +267,31 @@ def heartbeat(test_or_id = nil) attr_reader :index + def worker_history_retry_queue + reservations = redis.lrange(key('worker', worker_id, 'queue'), 0, -1) + if reservations.empty? + raise WorkerHistoryError, "Reservation history is missing for worker #{worker_id}" + end + + seen = {} + test_ids = reservations.reverse_each.each_with_object([]) do |reservation_id, ids| + expand_reservation(reservation_id).each do |test_id| + next if seen[test_id] + + seen[test_id] = true + ids << test_id + end + end + + Retry.new( + test_ids, + config, + redis: redis, + history_items: reservations.size, + worker_history: true + ) + end + # Runs a block while sending periodic heartbeats in a background thread. # This prevents other workers from stealing the test while it's being executed. def with_heartbeat(test_id) @@ -525,6 +553,20 @@ def chunk_id?(id) id.include?(':chunk_') end + def expand_reservation(id) + return [id] unless chunk_id?(id) + + chunk_json = redis.get(key('chunk', id)) + raise WorkerHistoryError, "Chunk metadata is missing for #{id}" unless chunk_json + + test_ids = CI::Queue::TestChunk.from_json(id, chunk_json).test_ids + raise WorkerHistoryError, "Chunk metadata contains no tests for #{id}" if test_ids.empty? + + test_ids + rescue JSON::ParserError => error + raise WorkerHistoryError, "Chunk metadata is invalid for #{id}: #{error.message}" + end + def resolve_executable(id) # Detect chunk by ID pattern if chunk_id?(id) diff --git a/ruby/lib/ci/queue/static.rb b/ruby/lib/ci/queue/static.rb index 110e8c17..1fd2d56e 100644 --- a/ruby/lib/ci/queue/static.rb +++ b/ruby/lib/ci/queue/static.rb @@ -22,6 +22,7 @@ def initialize(tests, config) @config = config @progress = 0 @total = tests.size + @shutdown_required = false end def distributed? @@ -66,11 +67,15 @@ def size end def poll - while config.circuit_breakers.none?(&:open?) && !max_test_failed? && test = @queue.shift + while !@shutdown_required && config.circuit_breakers.none?(&:open?) && !max_test_failed? && test = @queue.shift yield index.fetch(test) end end + def shutdown! + @shutdown_required = true + end + def exhausted? @queue.empty? end diff --git a/ruby/lib/minitest/queue.rb b/ruby/lib/minitest/queue.rb index 9b0770c1..59a5487e 100644 --- a/ruby/lib/minitest/queue.rb +++ b/ruby/lib/minitest/queue.rb @@ -15,6 +15,7 @@ require 'minitest/queue/grind_reporter' require 'minitest/queue/test_time_recorder' require 'minitest/queue/test_time_reporter' +require 'minitest/queue/recovery_reporter' module Minitest class Requeue < Skip diff --git a/ruby/lib/minitest/queue/recovery_reporter.rb b/ruby/lib/minitest/queue/recovery_reporter.rb new file mode 100644 index 00000000..c0f7f509 --- /dev/null +++ b/ruby/lib/minitest/queue/recovery_reporter.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'json' +require 'minitest/reporters' +require 'tempfile' + +module Minitest + module Queue + class RecoveryReporter < Minitest::Reporters::BaseReporter + def initialize(path:, queue:, config:) + super({}) + @path = path + @queue = queue + @config = config + end + + def report + super + return unless queue.exhausted? + return if worker_history_retry? && !queue.worker_history_complete? + + directory = File.dirname(path) + FileUtils.mkdir_p(directory) + temporary = Tempfile.new(['recovery', '.json'], directory) + temporary.write(JSON.pretty_generate(manifest)) + temporary.flush + temporary.fsync + temporary.close + File.rename(temporary.path, path) + ensure + temporary&.close! + end + + private + + attr_reader :config, :path, :queue + + def worker_history_retry? + queue.respond_to?(:worker_history?) && queue.worker_history? + end + + def manifest + { + schema_version: 1, + worker_id: config.worker_id.to_s, + retry_count: config.retry_count, + history_items: worker_history_retry? ? queue.history_items : 0, + replayed_tests: worker_history_retry? ? queue.replayed_tests : 0, + resumed_shared_queue: false, + replay_completed: !worker_history_retry? || queue.worker_history_complete? + } + end + end + end +end diff --git a/ruby/lib/minitest/queue/runner.rb b/ruby/lib/minitest/queue/runner.rb index cc7c2045..0c1576de 100644 --- a/ruby/lib/minitest/queue/runner.rb +++ b/ruby/lib/minitest/queue/runner.rb @@ -4,6 +4,7 @@ require 'minitest/queue' require 'ci/queue' require 'digest/md5' +require 'fileutils' require 'minitest/reporters/bisect_reporter' require 'minitest/reporters/statsd_reporter' @@ -49,16 +50,21 @@ def retry_command def run_command require_worker_id! + FileUtils.rm_f(queue_config.recovery_manifest) if queue_config.recovery_manifest if queue.retrying? || retry? if queue.expired? abort! "The test run is too old and can't be retried" end reset_counters - retry_queue = queue.retry_queue + retry_queue = retry_queue_for_retry if retry_queue.exhausted? puts "The retry queue does not contain any failure, we'll process the main queue instead." else - puts "Retrying failed tests." + if worker_history_retry? + puts "Replaying #{retry_queue.replayed_tests} tests from #{retry_queue.history_items} worker reservations." + else + puts "Retrying failed tests." + end self.queue = retry_queue end end @@ -81,6 +87,13 @@ def run_command if queue_config.statsd_endpoint reporters << Minitest::Reporters::StatsdReporter.new(statsd_endpoint: queue_config.statsd_endpoint) end + if queue_config.recovery_manifest + reporters << RecoveryReporter.new( + path: queue_config.recovery_manifest, + queue: queue, + config: queue_config + ) + end Minitest.queue_reporters = reporters trap('TERM') { Minitest.queue.shutdown! } @@ -324,6 +337,22 @@ def reset_counters queue.build.reset_stats(BuildStatusRecorder::COUNTERS) end + def worker_history_retry? + queue_config.retry_mode == :worker_history + end + + def retry_queue_for_retry + if worker_history_retry? + abort! 'Worker history recovery requires a distributed Redis queue' unless queue.distributed? + + queue.retry_queue(scope: :worker_history) + else + queue.retry_queue + end + rescue CI::Queue::Redis::WorkerHistoryError => error + abort! error.message + end + def populate_queue Minitest.queue.populate(Minitest.loaded_tests, random: ordering_seed, &:id) end @@ -494,6 +523,22 @@ def parser queue_config.worker_id = worker_id end + help = <<~EOS + Retry behavior: failures (default) or worker-history. + EOS + opts.separator "" + opts.on('--retry-mode MODE', %w[failures worker-history], help) do |mode| + queue_config.retry_mode = mode.tr('-', '_').to_sym + end + + help = <<~EOS + Write worker-history recovery details to a JSON file after a completed run. + EOS + opts.separator "" + opts.on('--recovery-manifest PATH', help) do |path| + queue_config.recovery_manifest = path + end + help = <<~EOS Defines how many time a single test can be requeued. Defaults to 0. diff --git a/ruby/test/ci/queue/configuration_test.rb b/ruby/test/ci/queue/configuration_test.rb index 64d74105..1402d043 100644 --- a/ruby/test/ci/queue/configuration_test.rb +++ b/ruby/test/ci/queue/configuration_test.rb @@ -33,10 +33,13 @@ def test_buildkite_defaults 'BUILDKITE_BUILD_ID' => '9e08ef3c-d6e6-4a86-91dd-577ce5205b8e', 'BUILDKITE_PARALLEL_JOB' => '12', 'BUILDKITE_COMMIT' => 'faa647bbb8168a77cf338e7488c3f8445c3e6554', + 'BUILDKITE_RETRY_COUNT' => '2', ) assert_equal '9e08ef3c-d6e6-4a86-91dd-577ce5205b8e', config.build_id assert_equal '12', config.worker_id assert_equal 'faa647bbb8168a77cf338e7488c3f8445c3e6554', config.seed + assert_equal 2, config.retry_count + assert_equal :failures, config.retry_mode end def test_travis_defaults diff --git a/ruby/test/ci/queue/redis/worker_chunk_test.rb b/ruby/test/ci/queue/redis/worker_chunk_test.rb index e0b2592f..9d17566e 100644 --- a/ruby/test/ci/queue/redis/worker_chunk_test.rb +++ b/ruby/test/ci/queue/redis/worker_chunk_test.rb @@ -243,6 +243,62 @@ def test_populate_with_many_chunks_uses_batching end end + def test_worker_history_retry_expands_chunks_and_deduplicates_tests + tests = create_mock_tests(['TestA#test_1', 'TestA#test_2', 'TestB#test_1']) + chunk = CI::Queue::TestChunk.new( + 'TestA:chunk_0', + 'TestA', + ['TestA#test_1', 'TestA#test_2'], + 2000.0 + ) + + @worker.stub(:reorder_tests, [chunk, tests.last]) do + @worker.populate(tests) + end + + history_key = 'build:42:worker:1:queue' + @redis.del(history_key) + [chunk.id, tests.last.id, chunk.id, tests[1].id].each do |id| + @redis.lpush(history_key, id) + end + + retry_queue = @worker.retry_queue(scope: :worker_history) + retry_queue.populate(tests) + + assert_equal ['TestA#test_1', 'TestA#test_2', 'TestB#test_1'], retry_queue.to_a.map(&:id) + assert_equal 4, retry_queue.history_items + assert_equal 3, retry_queue.replayed_tests + assert retry_queue.worker_history? + end + + def test_worker_history_retry_requires_reservations + error = assert_raises(CI::Queue::Redis::WorkerHistoryError) do + @worker.retry_queue(scope: :worker_history) + end + + assert_equal 'Reservation history is missing for worker 1', error.message + end + + def test_worker_history_retry_requires_chunk_metadata + @redis.lpush('build:42:worker:1:queue', 'TestA:chunk_0') + + error = assert_raises(CI::Queue::Redis::WorkerHistoryError) do + @worker.retry_queue(scope: :worker_history) + end + + assert_equal 'Chunk metadata is missing for TestA:chunk_0', error.message + end + + def test_worker_history_retry_is_incomplete_when_a_test_cannot_be_resolved + @redis.lpush('build:42:worker:1:queue', 'MissingTest#test_1') + retry_queue = @worker.retry_queue(scope: :worker_history) + retry_queue.populate([]) + + assert_raises(KeyError) { retry_queue.poll { |_test| } } + assert retry_queue.exhausted? + refute retry_queue.worker_history_complete? + end + private def create_mock_tests(test_ids) diff --git a/ruby/test/ci/queue/static_test.rb b/ruby/test/ci/queue/static_test.rb index 78e81fcf..d4043169 100644 --- a/ruby/test/ci/queue/static_test.rb +++ b/ruby/test/ci/queue/static_test.rb @@ -4,6 +4,18 @@ class CI::Queue::StaticTest < Minitest::Test include SharedQueueAssertions + def test_shutdown_stops_polling + order = [] + + @queue.poll do |test| + order << test + @queue.shutdown! + end + + assert_equal 1, order.size + refute @queue.exhausted? + end + private def build_queue diff --git a/ruby/test/integration/minitest_redis_test.rb b/ruby/test/integration/minitest_redis_test.rb index ab186736..01b00096 100644 --- a/ruby/test/integration/minitest_redis_test.rb +++ b/ruby/test/integration/minitest_redis_test.rb @@ -264,6 +264,51 @@ def test_retry_fails_when_test_run_is_expired assert_equal "The test run is too old and can't be retried", output end + def test_worker_history_retry_writes_recovery_manifest + Dir.mktmpdir do |directory| + manifest_path = File.join(directory, 'recovery.json') + run_worker_history_worker(manifest_path, retry_count: 0) + + first_manifest = JSON.parse(File.read(manifest_path)) + assert_equal 0, first_manifest['retry_count'] + assert_equal 0, first_manifest['history_items'] + assert_equal 0, first_manifest['replayed_tests'] + refute first_manifest['resumed_shared_queue'] + assert first_manifest['replay_completed'] + + out, = run_worker_history_worker(manifest_path, retry_count: 1) + + assert_includes out, 'Replaying 100 tests from 100 worker reservations.' + retry_manifest = JSON.parse(File.read(manifest_path)) + assert_equal 1, retry_manifest['schema_version'] + assert_equal '1', retry_manifest['worker_id'] + assert_equal 1, retry_manifest['retry_count'] + assert_equal 100, retry_manifest['history_items'] + assert_equal 100, retry_manifest['replayed_tests'] + refute retry_manifest['resumed_shared_queue'] + assert retry_manifest['replay_completed'] + end + end + + def test_worker_history_retry_removes_stale_manifest_when_history_is_missing + run_worker_history_worker(nil, retry_count: 0, build_id: 'missing-history', worker_id: '1') + + Dir.mktmpdir do |directory| + manifest_path = File.join(directory, 'recovery.json') + File.write(manifest_path, '{"stale":true}') + out, = run_worker_history_worker( + manifest_path, + retry_count: 1, + build_id: 'missing-history', + worker_id: '2' + ) + + refute_predicate $?, :success? + assert_includes out, 'Reservation history is missing for worker 2' + refute_path_exists manifest_path + end + end + def test_retry_report # Run first worker, failing all tests out, err = capture_subprocess_io do @@ -788,6 +833,28 @@ def test_utf8_tests_and_marshal private + def run_worker_history_worker(manifest_path, retry_count:, build_id: 'worker-history', worker_id: '1') + args = [ + @exe, 'run', + '--queue', @redis_url, + '--seed', 'foobar', + '--build', build_id, + '--worker', worker_id, + '--timeout', '1', + '--retry-mode', 'worker-history' + ] + args.push('--recovery-manifest', manifest_path) if manifest_path + args.push('-Itest', 'test/passing_test.rb') + + capture_subprocess_io do + system( + { 'BUILDKITE_RETRY_COUNT' => retry_count.to_s }, + *args, + chdir: 'test/fixtures/' + ) + end + end + def normalize_xml(output) freeze_xml_timing(rewrite_paths(output)) end diff --git a/ruby/test/minitest/queue/recovery_reporter_test.rb b/ruby/test/minitest/queue/recovery_reporter_test.rb new file mode 100644 index 00000000..5662c8c4 --- /dev/null +++ b/ruby/test/minitest/queue/recovery_reporter_test.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Minitest + module Queue + class RecoveryReporterTest < Minitest::Test + Queue = Struct.new( + :exhausted?, + :worker_history?, + :worker_history_complete?, + :history_items, + :replayed_tests + ) + + def test_writes_manifest_for_completed_worker_history_retry + Dir.mktmpdir do |directory| + path = File.join(directory, 'recovery.json') + queue = Queue.new(true, true, true, 4, 3) + config = CI::Queue::Configuration.new(worker_id: '17', retry_count: 2) + reporter = RecoveryReporter.new(path: path, queue: queue, config: config) + + reporter.start + reporter.report + + manifest = JSON.parse(File.read(path)) + assert_equal 4, manifest['history_items'] + assert_equal 3, manifest['replayed_tests'] + assert manifest['replay_completed'] + refute manifest['resumed_shared_queue'] + end + end + + def test_does_not_write_manifest_for_incomplete_retry + Dir.mktmpdir do |directory| + path = File.join(directory, 'recovery.json') + queue = Queue.new(false, true, false, 4, 3) + config = CI::Queue::Configuration.new(worker_id: '17') + reporter = RecoveryReporter.new(path: path, queue: queue, config: config) + + reporter.start + reporter.report + + refute_path_exists path + end + end + + def test_does_not_write_manifest_when_replay_did_not_complete + Dir.mktmpdir do |directory| + path = File.join(directory, 'recovery.json') + queue = Queue.new(true, true, false, 4, 3) + config = CI::Queue::Configuration.new(worker_id: '17') + reporter = RecoveryReporter.new(path: path, queue: queue, config: config) + + reporter.start + reporter.report + + refute_path_exists path + end + end + end + end +end From 49a0560c2ac937cbbdf0353b8cc3a6e382d590d6 Mon Sep 17 00:00:00 2001 From: Yicheng Fang Date: Mon, 10 Aug 2026 21:12:31 +0000 Subject: [PATCH 2/5] Simplify worker history retry metadata --- ruby/lib/ci/queue/redis/retry.rb | 21 ++----- ruby/lib/ci/queue/redis/worker.rb | 8 +-- ruby/lib/minitest/queue/recovery_reporter.rb | 8 +-- ruby/lib/minitest/queue/runner.rb | 2 +- ruby/test/ci/queue/configuration_test.rb | 3 - ruby/test/ci/queue/redis/worker_chunk_test.rb | 56 ----------------- ruby/test/ci/queue/static_test.rb | 12 ---- ruby/test/integration/minitest_redis_test.rb | 6 +- .../minitest/queue/recovery_reporter_test.rb | 63 ------------------- 9 files changed, 12 insertions(+), 167 deletions(-) delete mode 100644 ruby/test/minitest/queue/recovery_reporter_test.rb diff --git a/ruby/lib/ci/queue/redis/retry.rb b/ruby/lib/ci/queue/redis/retry.rb index 8e5aaf9e..659e57ca 100644 --- a/ruby/lib/ci/queue/redis/retry.rb +++ b/ruby/lib/ci/queue/redis/retry.rb @@ -3,28 +3,19 @@ module CI module Queue module Redis class Retry < Static - attr_reader :history_items, :replayed_tests - - def initialize(tests, config, redis:, history_items: 0, worker_history: false) + def initialize(tests, config, redis:) @redis = redis - @history_items = history_items - @replayed_tests = worker_history ? tests.size : 0 - @worker_history = worker_history - @worker_history_complete = false + @poll_completed = false super(tests, config) end - def worker_history? - @worker_history - end - - def worker_history_complete? - @worker_history_complete + def poll_completed? + @poll_completed end - def poll(&block) + def poll super - @worker_history_complete = exhausted? if worker_history? + @poll_completed = exhausted? end def build diff --git a/ruby/lib/ci/queue/redis/worker.rb b/ruby/lib/ci/queue/redis/worker.rb index 47425c0b..200d8e43 100644 --- a/ruby/lib/ci/queue/redis/worker.rb +++ b/ruby/lib/ci/queue/redis/worker.rb @@ -283,13 +283,7 @@ def worker_history_retry_queue end end - Retry.new( - test_ids, - config, - redis: redis, - history_items: reservations.size, - worker_history: true - ) + Retry.new(test_ids, config, redis: redis) end # Runs a block while sending periodic heartbeats in a background thread. diff --git a/ruby/lib/minitest/queue/recovery_reporter.rb b/ruby/lib/minitest/queue/recovery_reporter.rb index c0f7f509..466f5476 100644 --- a/ruby/lib/minitest/queue/recovery_reporter.rb +++ b/ruby/lib/minitest/queue/recovery_reporter.rb @@ -18,7 +18,7 @@ def initialize(path:, queue:, config:) def report super return unless queue.exhausted? - return if worker_history_retry? && !queue.worker_history_complete? + return if worker_history_retry? && !queue.poll_completed? directory = File.dirname(path) FileUtils.mkdir_p(directory) @@ -37,7 +37,7 @@ def report attr_reader :config, :path, :queue def worker_history_retry? - queue.respond_to?(:worker_history?) && queue.worker_history? + config.retry_mode == :worker_history && queue.respond_to?(:poll_completed?) end def manifest @@ -45,10 +45,8 @@ def manifest schema_version: 1, worker_id: config.worker_id.to_s, retry_count: config.retry_count, - history_items: worker_history_retry? ? queue.history_items : 0, - replayed_tests: worker_history_retry? ? queue.replayed_tests : 0, resumed_shared_queue: false, - replay_completed: !worker_history_retry? || queue.worker_history_complete? + replay_completed: !worker_history_retry? || queue.poll_completed? } end end diff --git a/ruby/lib/minitest/queue/runner.rb b/ruby/lib/minitest/queue/runner.rb index 0c1576de..64b7c10d 100644 --- a/ruby/lib/minitest/queue/runner.rb +++ b/ruby/lib/minitest/queue/runner.rb @@ -61,7 +61,7 @@ def run_command puts "The retry queue does not contain any failure, we'll process the main queue instead." else if worker_history_retry? - puts "Replaying #{retry_queue.replayed_tests} tests from #{retry_queue.history_items} worker reservations." + puts "Replaying this worker's reservation history." else puts "Retrying failed tests." end diff --git a/ruby/test/ci/queue/configuration_test.rb b/ruby/test/ci/queue/configuration_test.rb index 1402d043..64d74105 100644 --- a/ruby/test/ci/queue/configuration_test.rb +++ b/ruby/test/ci/queue/configuration_test.rb @@ -33,13 +33,10 @@ def test_buildkite_defaults 'BUILDKITE_BUILD_ID' => '9e08ef3c-d6e6-4a86-91dd-577ce5205b8e', 'BUILDKITE_PARALLEL_JOB' => '12', 'BUILDKITE_COMMIT' => 'faa647bbb8168a77cf338e7488c3f8445c3e6554', - 'BUILDKITE_RETRY_COUNT' => '2', ) assert_equal '9e08ef3c-d6e6-4a86-91dd-577ce5205b8e', config.build_id assert_equal '12', config.worker_id assert_equal 'faa647bbb8168a77cf338e7488c3f8445c3e6554', config.seed - assert_equal 2, config.retry_count - assert_equal :failures, config.retry_mode end def test_travis_defaults diff --git a/ruby/test/ci/queue/redis/worker_chunk_test.rb b/ruby/test/ci/queue/redis/worker_chunk_test.rb index 9d17566e..e0b2592f 100644 --- a/ruby/test/ci/queue/redis/worker_chunk_test.rb +++ b/ruby/test/ci/queue/redis/worker_chunk_test.rb @@ -243,62 +243,6 @@ def test_populate_with_many_chunks_uses_batching end end - def test_worker_history_retry_expands_chunks_and_deduplicates_tests - tests = create_mock_tests(['TestA#test_1', 'TestA#test_2', 'TestB#test_1']) - chunk = CI::Queue::TestChunk.new( - 'TestA:chunk_0', - 'TestA', - ['TestA#test_1', 'TestA#test_2'], - 2000.0 - ) - - @worker.stub(:reorder_tests, [chunk, tests.last]) do - @worker.populate(tests) - end - - history_key = 'build:42:worker:1:queue' - @redis.del(history_key) - [chunk.id, tests.last.id, chunk.id, tests[1].id].each do |id| - @redis.lpush(history_key, id) - end - - retry_queue = @worker.retry_queue(scope: :worker_history) - retry_queue.populate(tests) - - assert_equal ['TestA#test_1', 'TestA#test_2', 'TestB#test_1'], retry_queue.to_a.map(&:id) - assert_equal 4, retry_queue.history_items - assert_equal 3, retry_queue.replayed_tests - assert retry_queue.worker_history? - end - - def test_worker_history_retry_requires_reservations - error = assert_raises(CI::Queue::Redis::WorkerHistoryError) do - @worker.retry_queue(scope: :worker_history) - end - - assert_equal 'Reservation history is missing for worker 1', error.message - end - - def test_worker_history_retry_requires_chunk_metadata - @redis.lpush('build:42:worker:1:queue', 'TestA:chunk_0') - - error = assert_raises(CI::Queue::Redis::WorkerHistoryError) do - @worker.retry_queue(scope: :worker_history) - end - - assert_equal 'Chunk metadata is missing for TestA:chunk_0', error.message - end - - def test_worker_history_retry_is_incomplete_when_a_test_cannot_be_resolved - @redis.lpush('build:42:worker:1:queue', 'MissingTest#test_1') - retry_queue = @worker.retry_queue(scope: :worker_history) - retry_queue.populate([]) - - assert_raises(KeyError) { retry_queue.poll { |_test| } } - assert retry_queue.exhausted? - refute retry_queue.worker_history_complete? - end - private def create_mock_tests(test_ids) diff --git a/ruby/test/ci/queue/static_test.rb b/ruby/test/ci/queue/static_test.rb index d4043169..78e81fcf 100644 --- a/ruby/test/ci/queue/static_test.rb +++ b/ruby/test/ci/queue/static_test.rb @@ -4,18 +4,6 @@ class CI::Queue::StaticTest < Minitest::Test include SharedQueueAssertions - def test_shutdown_stops_polling - order = [] - - @queue.poll do |test| - order << test - @queue.shutdown! - end - - assert_equal 1, order.size - refute @queue.exhausted? - end - private def build_queue diff --git a/ruby/test/integration/minitest_redis_test.rb b/ruby/test/integration/minitest_redis_test.rb index 01b00096..68abed3f 100644 --- a/ruby/test/integration/minitest_redis_test.rb +++ b/ruby/test/integration/minitest_redis_test.rb @@ -271,20 +271,16 @@ def test_worker_history_retry_writes_recovery_manifest first_manifest = JSON.parse(File.read(manifest_path)) assert_equal 0, first_manifest['retry_count'] - assert_equal 0, first_manifest['history_items'] - assert_equal 0, first_manifest['replayed_tests'] refute first_manifest['resumed_shared_queue'] assert first_manifest['replay_completed'] out, = run_worker_history_worker(manifest_path, retry_count: 1) - assert_includes out, 'Replaying 100 tests from 100 worker reservations.' + assert_includes out, "Replaying this worker's reservation history." retry_manifest = JSON.parse(File.read(manifest_path)) assert_equal 1, retry_manifest['schema_version'] assert_equal '1', retry_manifest['worker_id'] assert_equal 1, retry_manifest['retry_count'] - assert_equal 100, retry_manifest['history_items'] - assert_equal 100, retry_manifest['replayed_tests'] refute retry_manifest['resumed_shared_queue'] assert retry_manifest['replay_completed'] end diff --git a/ruby/test/minitest/queue/recovery_reporter_test.rb b/ruby/test/minitest/queue/recovery_reporter_test.rb deleted file mode 100644 index 5662c8c4..00000000 --- a/ruby/test/minitest/queue/recovery_reporter_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -module Minitest - module Queue - class RecoveryReporterTest < Minitest::Test - Queue = Struct.new( - :exhausted?, - :worker_history?, - :worker_history_complete?, - :history_items, - :replayed_tests - ) - - def test_writes_manifest_for_completed_worker_history_retry - Dir.mktmpdir do |directory| - path = File.join(directory, 'recovery.json') - queue = Queue.new(true, true, true, 4, 3) - config = CI::Queue::Configuration.new(worker_id: '17', retry_count: 2) - reporter = RecoveryReporter.new(path: path, queue: queue, config: config) - - reporter.start - reporter.report - - manifest = JSON.parse(File.read(path)) - assert_equal 4, manifest['history_items'] - assert_equal 3, manifest['replayed_tests'] - assert manifest['replay_completed'] - refute manifest['resumed_shared_queue'] - end - end - - def test_does_not_write_manifest_for_incomplete_retry - Dir.mktmpdir do |directory| - path = File.join(directory, 'recovery.json') - queue = Queue.new(false, true, false, 4, 3) - config = CI::Queue::Configuration.new(worker_id: '17') - reporter = RecoveryReporter.new(path: path, queue: queue, config: config) - - reporter.start - reporter.report - - refute_path_exists path - end - end - - def test_does_not_write_manifest_when_replay_did_not_complete - Dir.mktmpdir do |directory| - path = File.join(directory, 'recovery.json') - queue = Queue.new(true, true, false, 4, 3) - config = CI::Queue::Configuration.new(worker_id: '17') - reporter = RecoveryReporter.new(path: path, queue: queue, config: config) - - reporter.start - reporter.report - - refute_path_exists path - end - end - end - end -end From 4c71c48bfebcfd35051b67b6a5af323515daf82e Mon Sep 17 00:00:00 2001 From: Yicheng Fang Date: Mon, 10 Aug 2026 21:23:14 +0000 Subject: [PATCH 3/5] Remove recovery manifest --- ruby/README.md | 4 +- ruby/lib/ci/queue/configuration.rb | 9 +--- ruby/lib/ci/queue/redis/retry.rb | 10 ---- ruby/lib/ci/queue/redis/worker.rb | 5 +- ruby/lib/minitest/queue.rb | 1 - ruby/lib/minitest/queue/recovery_reporter.rb | 54 -------------------- ruby/lib/minitest/queue/runner.rb | 37 +------------- ruby/test/integration/minitest_redis_test.rb | 50 +++++------------- 8 files changed, 19 insertions(+), 151 deletions(-) delete mode 100644 ruby/lib/minitest/queue/recovery_reporter.rb diff --git a/ruby/README.md b/ruby/README.md index 849ff0aa..38e4a599 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -47,13 +47,11 @@ previously reserved by that worker: ```bash minitest-queue --queue redis://example.com run \ --retry-mode worker-history \ - --recovery-manifest tmp/ci-queue-recovery.json \ -Itest test/**/*_test.rb ``` The replay uses the existing local retry queue and exits after the worker history -is exhausted. It does not claim additional work from the shared Redis queue. The -manifest is written atomically only after a complete run. +is exhausted. It does not claim additional work from the shared Redis queue. If you'd like to centralize the error reporting you can do so with: diff --git a/ruby/lib/ci/queue/configuration.rb b/ruby/lib/ci/queue/configuration.rb index 522d42eb..50c89760 100644 --- a/ruby/lib/ci/queue/configuration.rb +++ b/ruby/lib/ci/queue/configuration.rb @@ -15,7 +15,7 @@ class Configuration attr_accessor :timing_redis_url attr_accessor :write_duration_averages attr_accessor :heartbeat_grace_period, :heartbeat_interval - attr_accessor :retry_mode, :recovery_manifest, :retry_count + attr_accessor :retry_mode attr_reader :circuit_breakers attr_writer :seed, :build_id attr_writer :queue_init_timeout, :report_timeout, :inactive_workers_timeout @@ -31,7 +31,6 @@ def from_env(env) redis_ttl: env['CI_QUEUE_REDIS_TTL']&.to_i || 8 * 60 * 60, known_flaky_tests: load_known_flaky_tests(env['CI_QUEUE_KNOWN_FLAKY_TESTS']), branch: env['BUILDKITE_BRANCH'], - retry_count: env['BUILDKITE_RETRY_COUNT'].to_i, ) end @@ -69,9 +68,7 @@ def initialize( timing_redis_url: nil, heartbeat_grace_period: 30, heartbeat_interval: 10, - retry_mode: :failures, - recovery_manifest: nil, - retry_count: 0 + retry_mode: :failures ) @build_id = build_id @circuit_breakers = [CircuitBreaker::Disabled] @@ -111,8 +108,6 @@ def initialize( @heartbeat_grace_period = heartbeat_grace_period @heartbeat_interval = heartbeat_interval @retry_mode = retry_mode - @recovery_manifest = recovery_manifest - @retry_count = retry_count end def queue_init_timeout diff --git a/ruby/lib/ci/queue/redis/retry.rb b/ruby/lib/ci/queue/redis/retry.rb index 659e57ca..85bcc27e 100644 --- a/ruby/lib/ci/queue/redis/retry.rb +++ b/ruby/lib/ci/queue/redis/retry.rb @@ -5,19 +5,9 @@ module Redis class Retry < Static def initialize(tests, config, redis:) @redis = redis - @poll_completed = false super(tests, config) end - def poll_completed? - @poll_completed - end - - def poll - super - @poll_completed = exhausted? - end - def build @build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config) end diff --git a/ruby/lib/ci/queue/redis/worker.rb b/ruby/lib/ci/queue/redis/worker.rb index 200d8e43..85237a21 100644 --- a/ruby/lib/ci/queue/redis/worker.rb +++ b/ruby/lib/ci/queue/redis/worker.rb @@ -145,9 +145,8 @@ def retrying? end end - def retry_queue(scope: :failures) - return worker_history_retry_queue if scope == :worker_history - raise ArgumentError, "Unknown retry scope: #{scope}" unless scope == :failures + def retry_queue + return worker_history_retry_queue if config.retry_mode == :worker_history failures = build.failed_tests.to_set log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1) diff --git a/ruby/lib/minitest/queue.rb b/ruby/lib/minitest/queue.rb index 59a5487e..9b0770c1 100644 --- a/ruby/lib/minitest/queue.rb +++ b/ruby/lib/minitest/queue.rb @@ -15,7 +15,6 @@ require 'minitest/queue/grind_reporter' require 'minitest/queue/test_time_recorder' require 'minitest/queue/test_time_reporter' -require 'minitest/queue/recovery_reporter' module Minitest class Requeue < Skip diff --git a/ruby/lib/minitest/queue/recovery_reporter.rb b/ruby/lib/minitest/queue/recovery_reporter.rb deleted file mode 100644 index 466f5476..00000000 --- a/ruby/lib/minitest/queue/recovery_reporter.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' -require 'json' -require 'minitest/reporters' -require 'tempfile' - -module Minitest - module Queue - class RecoveryReporter < Minitest::Reporters::BaseReporter - def initialize(path:, queue:, config:) - super({}) - @path = path - @queue = queue - @config = config - end - - def report - super - return unless queue.exhausted? - return if worker_history_retry? && !queue.poll_completed? - - directory = File.dirname(path) - FileUtils.mkdir_p(directory) - temporary = Tempfile.new(['recovery', '.json'], directory) - temporary.write(JSON.pretty_generate(manifest)) - temporary.flush - temporary.fsync - temporary.close - File.rename(temporary.path, path) - ensure - temporary&.close! - end - - private - - attr_reader :config, :path, :queue - - def worker_history_retry? - config.retry_mode == :worker_history && queue.respond_to?(:poll_completed?) - end - - def manifest - { - schema_version: 1, - worker_id: config.worker_id.to_s, - retry_count: config.retry_count, - resumed_shared_queue: false, - replay_completed: !worker_history_retry? || queue.poll_completed? - } - end - end - end -end diff --git a/ruby/lib/minitest/queue/runner.rb b/ruby/lib/minitest/queue/runner.rb index 64b7c10d..d1c77a73 100644 --- a/ruby/lib/minitest/queue/runner.rb +++ b/ruby/lib/minitest/queue/runner.rb @@ -4,7 +4,6 @@ require 'minitest/queue' require 'ci/queue' require 'digest/md5' -require 'fileutils' require 'minitest/reporters/bisect_reporter' require 'minitest/reporters/statsd_reporter' @@ -50,17 +49,16 @@ def retry_command def run_command require_worker_id! - FileUtils.rm_f(queue_config.recovery_manifest) if queue_config.recovery_manifest if queue.retrying? || retry? if queue.expired? abort! "The test run is too old and can't be retried" end reset_counters - retry_queue = retry_queue_for_retry + retry_queue = queue.retry_queue if retry_queue.exhausted? puts "The retry queue does not contain any failure, we'll process the main queue instead." else - if worker_history_retry? + if queue_config.retry_mode == :worker_history puts "Replaying this worker's reservation history." else puts "Retrying failed tests." @@ -87,13 +85,6 @@ def run_command if queue_config.statsd_endpoint reporters << Minitest::Reporters::StatsdReporter.new(statsd_endpoint: queue_config.statsd_endpoint) end - if queue_config.recovery_manifest - reporters << RecoveryReporter.new( - path: queue_config.recovery_manifest, - queue: queue, - config: queue_config - ) - end Minitest.queue_reporters = reporters trap('TERM') { Minitest.queue.shutdown! } @@ -337,22 +328,6 @@ def reset_counters queue.build.reset_stats(BuildStatusRecorder::COUNTERS) end - def worker_history_retry? - queue_config.retry_mode == :worker_history - end - - def retry_queue_for_retry - if worker_history_retry? - abort! 'Worker history recovery requires a distributed Redis queue' unless queue.distributed? - - queue.retry_queue(scope: :worker_history) - else - queue.retry_queue - end - rescue CI::Queue::Redis::WorkerHistoryError => error - abort! error.message - end - def populate_queue Minitest.queue.populate(Minitest.loaded_tests, random: ordering_seed, &:id) end @@ -531,14 +506,6 @@ def parser queue_config.retry_mode = mode.tr('-', '_').to_sym end - help = <<~EOS - Write worker-history recovery details to a JSON file after a completed run. - EOS - opts.separator "" - opts.on('--recovery-manifest PATH', help) do |path| - queue_config.recovery_manifest = path - end - help = <<~EOS Defines how many time a single test can be requeued. Defaults to 0. diff --git a/ruby/test/integration/minitest_redis_test.rb b/ruby/test/integration/minitest_redis_test.rb index 68abed3f..c2ce980a 100644 --- a/ruby/test/integration/minitest_redis_test.rb +++ b/ruby/test/integration/minitest_redis_test.rb @@ -264,45 +264,20 @@ def test_retry_fails_when_test_run_is_expired assert_equal "The test run is too old and can't be retried", output end - def test_worker_history_retry_writes_recovery_manifest - Dir.mktmpdir do |directory| - manifest_path = File.join(directory, 'recovery.json') - run_worker_history_worker(manifest_path, retry_count: 0) - - first_manifest = JSON.parse(File.read(manifest_path)) - assert_equal 0, first_manifest['retry_count'] - refute first_manifest['resumed_shared_queue'] - assert first_manifest['replay_completed'] - - out, = run_worker_history_worker(manifest_path, retry_count: 1) - - assert_includes out, "Replaying this worker's reservation history." - retry_manifest = JSON.parse(File.read(manifest_path)) - assert_equal 1, retry_manifest['schema_version'] - assert_equal '1', retry_manifest['worker_id'] - assert_equal 1, retry_manifest['retry_count'] - refute retry_manifest['resumed_shared_queue'] - assert retry_manifest['replay_completed'] - end + def test_worker_history_retry_replays_worker_reservations + run_worker_history_worker(retry_count: 0) + out, = run_worker_history_worker(retry_count: 1) + + assert_predicate $?, :success? + assert_includes out, "Replaying this worker's reservation history." end - def test_worker_history_retry_removes_stale_manifest_when_history_is_missing - run_worker_history_worker(nil, retry_count: 0, build_id: 'missing-history', worker_id: '1') - - Dir.mktmpdir do |directory| - manifest_path = File.join(directory, 'recovery.json') - File.write(manifest_path, '{"stale":true}') - out, = run_worker_history_worker( - manifest_path, - retry_count: 1, - build_id: 'missing-history', - worker_id: '2' - ) + def test_worker_history_retry_fails_when_history_is_missing + run_worker_history_worker(retry_count: 0, build_id: 'missing-history', worker_id: '1') + out, err = run_worker_history_worker(retry_count: 1, build_id: 'missing-history', worker_id: '2') - refute_predicate $?, :success? - assert_includes out, 'Reservation history is missing for worker 2' - refute_path_exists manifest_path - end + refute_predicate $?, :success? + assert_includes out + err, 'Reservation history is missing for worker 2' end def test_retry_report @@ -829,7 +804,7 @@ def test_utf8_tests_and_marshal private - def run_worker_history_worker(manifest_path, retry_count:, build_id: 'worker-history', worker_id: '1') + def run_worker_history_worker(retry_count:, build_id: 'worker-history', worker_id: '1') args = [ @exe, 'run', '--queue', @redis_url, @@ -839,7 +814,6 @@ def run_worker_history_worker(manifest_path, retry_count:, build_id: 'worker-his '--timeout', '1', '--retry-mode', 'worker-history' ] - args.push('--recovery-manifest', manifest_path) if manifest_path args.push('-Itest', 'test/passing_test.rb') capture_subprocess_io do From 6f299fbf24f342380dd533bac1933a58eb0fce90 Mon Sep 17 00:00:00 2001 From: Yicheng Fang Date: Mon, 10 Aug 2026 22:08:41 +0000 Subject: [PATCH 4/5] Fail incomplete worker history retries --- ruby/lib/ci/queue/redis.rb | 1 + ruby/lib/ci/queue/redis/retry.rb | 8 ++++++ ruby/test/integration/minitest_redis_test.rb | 28 ++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/ruby/lib/ci/queue/redis.rb b/ruby/lib/ci/queue/redis.rb index 6602e14a..0e4e4606 100644 --- a/ruby/lib/ci/queue/redis.rb +++ b/ruby/lib/ci/queue/redis.rb @@ -17,6 +17,7 @@ module CI module Queue module Redis Error = Class.new(StandardError) + IncompleteRetry = Class.new(Error) LostMaster = Class.new(Error) WorkerHistoryError = Class.new(Error) diff --git a/ruby/lib/ci/queue/redis/retry.rb b/ruby/lib/ci/queue/redis/retry.rb index 85bcc27e..e101df20 100644 --- a/ruby/lib/ci/queue/redis/retry.rb +++ b/ruby/lib/ci/queue/redis/retry.rb @@ -12,6 +12,14 @@ def build @build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config) end + def poll + super + return unless config.retry_mode == :worker_history + return if exhausted? + + raise IncompleteRetry, 'Worker history replay stopped before completion' + end + private attr_reader :redis diff --git a/ruby/test/integration/minitest_redis_test.rb b/ruby/test/integration/minitest_redis_test.rb index c2ce980a..e74ea8d6 100644 --- a/ruby/test/integration/minitest_redis_test.rb +++ b/ruby/test/integration/minitest_redis_test.rb @@ -280,6 +280,23 @@ def test_worker_history_retry_fails_when_history_is_missing assert_includes out + err, 'Reservation history is missing for worker 2' end + def test_worker_history_retry_fails_when_replay_stops_early + run_worker_history_worker( + retry_count: 0, + build_id: 'incomplete-history', + test_file: 'test/failing_test.rb' + ) + out, err = run_worker_history_worker( + retry_count: 1, + build_id: 'incomplete-history', + test_file: 'test/failing_test.rb', + extra_args: ['--max-consecutive-failures', '1'] + ) + + refute_predicate $?, :success? + assert_includes out + err, 'Worker history replay stopped before completion' + end + def test_retry_report # Run first worker, failing all tests out, err = capture_subprocess_io do @@ -804,7 +821,13 @@ def test_utf8_tests_and_marshal private - def run_worker_history_worker(retry_count:, build_id: 'worker-history', worker_id: '1') + def run_worker_history_worker( + retry_count:, + build_id: 'worker-history', + worker_id: '1', + test_file: 'test/passing_test.rb', + extra_args: [] + ) args = [ @exe, 'run', '--queue', @redis_url, @@ -814,7 +837,8 @@ def run_worker_history_worker(retry_count:, build_id: 'worker-history', worker_i '--timeout', '1', '--retry-mode', 'worker-history' ] - args.push('-Itest', 'test/passing_test.rb') + args.concat(extra_args) + args.push('-Itest', test_file) capture_subprocess_io do system( From 79579ac646c18abef204ce99292a9b2ac759a07b Mon Sep 17 00:00:00 2001 From: Yicheng Fang Date: Mon, 10 Aug 2026 22:16:13 +0000 Subject: [PATCH 5/5] Remove worker history docs and tests --- ruby/README.md | 15 ----- ruby/test/integration/minitest_redis_test.rb | 61 -------------------- 2 files changed, 76 deletions(-) diff --git a/ruby/README.md b/ruby/README.md index 38e4a599..92ba5b99 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -38,21 +38,6 @@ minitest-queue --queue redis://example.com run -Itest test/**/*_test.rb Additionally you can configure the requeue settings (see main README) with `--max-requeues` and `--requeue-tolerance`. -#### Replaying a retried worker for coverage - -By default, a retried worker runs only tests recorded as failed. For process-local -artifacts such as SimpleCov output, use worker-history mode to replay every test -previously reserved by that worker: - -```bash -minitest-queue --queue redis://example.com run \ - --retry-mode worker-history \ - -Itest test/**/*_test.rb -``` - -The replay uses the existing local retry queue and exits after the worker history -is exhausted. It does not claim additional work from the shared Redis queue. - If you'd like to centralize the error reporting you can do so with: diff --git a/ruby/test/integration/minitest_redis_test.rb b/ruby/test/integration/minitest_redis_test.rb index e74ea8d6..ab186736 100644 --- a/ruby/test/integration/minitest_redis_test.rb +++ b/ruby/test/integration/minitest_redis_test.rb @@ -264,39 +264,6 @@ def test_retry_fails_when_test_run_is_expired assert_equal "The test run is too old and can't be retried", output end - def test_worker_history_retry_replays_worker_reservations - run_worker_history_worker(retry_count: 0) - out, = run_worker_history_worker(retry_count: 1) - - assert_predicate $?, :success? - assert_includes out, "Replaying this worker's reservation history." - end - - def test_worker_history_retry_fails_when_history_is_missing - run_worker_history_worker(retry_count: 0, build_id: 'missing-history', worker_id: '1') - out, err = run_worker_history_worker(retry_count: 1, build_id: 'missing-history', worker_id: '2') - - refute_predicate $?, :success? - assert_includes out + err, 'Reservation history is missing for worker 2' - end - - def test_worker_history_retry_fails_when_replay_stops_early - run_worker_history_worker( - retry_count: 0, - build_id: 'incomplete-history', - test_file: 'test/failing_test.rb' - ) - out, err = run_worker_history_worker( - retry_count: 1, - build_id: 'incomplete-history', - test_file: 'test/failing_test.rb', - extra_args: ['--max-consecutive-failures', '1'] - ) - - refute_predicate $?, :success? - assert_includes out + err, 'Worker history replay stopped before completion' - end - def test_retry_report # Run first worker, failing all tests out, err = capture_subprocess_io do @@ -821,34 +788,6 @@ def test_utf8_tests_and_marshal private - def run_worker_history_worker( - retry_count:, - build_id: 'worker-history', - worker_id: '1', - test_file: 'test/passing_test.rb', - extra_args: [] - ) - args = [ - @exe, 'run', - '--queue', @redis_url, - '--seed', 'foobar', - '--build', build_id, - '--worker', worker_id, - '--timeout', '1', - '--retry-mode', 'worker-history' - ] - args.concat(extra_args) - args.push('-Itest', test_file) - - capture_subprocess_io do - system( - { 'BUILDKITE_RETRY_COUNT' => retry_count.to_s }, - *args, - chdir: 'test/fixtures/' - ) - end - end - def normalize_xml(output) freeze_xml_timing(rewrite_paths(output)) end