Skip to content

Fix parallel test shutdown when a worker dies with queued tests - #59038

Open
maxazure wants to merge 1 commit into
rails:mainfrom
maxazure:fix-59037-dead-workers
Open

maxazure wants to merge 1 commit into
rails:mainfrom
maxazure:fix-59037-dead-workers

Conversation

@maxazure

Copy link
Copy Markdown

Motivation / Background

Fixes #59037. When a round-robin worker dies, its queued tests can never drain, so shutdown hangs before reporting unfinished work.

Detail

Close the distributor first, wait for the actual worker PIDs (including workers that have not registered yet), then report remaining queued and in-flight tests through the existing missing-result path. Surviving workers can finish their work, and no production timeout is added. This addresses dead workers; #57086 concerns workers that remain alive but get stuck.

Regression tests cover queued/in-flight work, early exits, delayed registration, work stealing, interrupt cancellation, and the real worker-crash reproduction.

Validation

  • macOS 26.6.2 arm64, Ruby 4.0.7: real DRb integration passes, 14 tests / 30 assertions, plus ten seeds. The issue reproduction finishes in 0.24s with the expected 10 tests / 5 assertions / 5 errors; the original baseline hangs until a 15s test-only watchdog.
  • Focused suite: 16 tests / 59 assertions. Another 82 Unix-socket scenarios checked 984 results for duplicates/omissions, including startup races and healthy six-second tests. RuboCop passes on all four changed Ruby files.
  • Earlier cloud checks: the same focused suite on Ruby 3.3.12, 3.4.11, and 4.0.7, 70 seeded server runs, and 30 server stress scenarios passed. Those checks could not use Unix sockets.
  • Full Active Support with strict warnings and two workers: 6804 tests / 19944 assertions / 2 failures / 0 errors / 1765 skips. The failures are BacktraceCleanerCleanLocationsTest#test_returns_all_clean_locations_(defaults) and #test_returns_all_clean_locations_(:silent): they expect only the test-file location but retain worker frames. Both reproduce on the original baseline. A timing-sensitive CI test failed on the first full run and passed on both baseline/patched focused reruns and the final full run.
  • Skips: Redis unavailable (1363), memcached unavailable (342), JSON extension already loaded (60). Cache services were not started; this is not a fully green component run.

Checklist

  • One focused change.
  • Commit explains the change and references the issue.
  • Regression tests added.
  • Active Support changelog updated.

Close the distributor before waiting for the worker PID pool so a dead round-robin worker cannot leave shutdown waiting forever on its private queue. Surviving workers can finish queued work, including long tests and workers that register late. After every child exits, report remaining queued and in-flight tests through the existing missing-result path without adding a production timeout.
Copilot AI balanced review requested due to automatic review settings October 12, 2026 06:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes core, timing-sensitive parallel-test shutdown behavior involving fork/DRb/signal handling that affects every Rails parallel test run, so it warrants final human review despite appearing correct and well-tested.

0 open findings

What changed in this PR

This PR fixes a hang in Active Support's parallel test runner (issue #59037): when a round-robin worker process dies while it still has queued tests, those tests can never be drained, so Server#shutdown's old while @distributor.pending? loop spins forever and the run never reports the unfinished work.

The fix reworks shutdown to be driven by the actual worker PIDs rather than queue state: close the distributor first, block on Process.waitpid for every forked PID (including workers that never registered or crashed early), then drain any tests the dead workers left in their queues and report them (plus still-in-flight tests) through the existing "result not reported" path. This removes the reliance on active_workers?/reap_dead_workers polling and the pre-close pending? drain.

Changes:

  • Rework Server#shutdown to accept worker_pids, wait on real PIDs via a new wait_for_workers, then drain leftover queued tests from dead workers and report them as unreported.
  • Simplify Parallelization#shutdown to delegate PID waiting to the server by passing @worker_pool.
  • Add regression tests covering queued/in-flight work, early exits, delayed registration, work stealing, and interrupt cancellation, plus a real worker-crash reproduction; update the changelog.
File Description
activesupport/​lib/​active_support/​testing/​parallelization/​server.rb Replaces the pending? drain + wait_for_active_workers/reap_dead_workers with shutdown(worker_pids) that waits on real PIDs and drains dead workers' queues.
activesupport/​lib/​active_support/​testing/​parallelization.rb Delegates worker-PID waiting to @queue_server.shutdown(@worker_pool).
activesupport/​test/​parallelization/​server_test.rb Adds unit tests for dead-worker draining, early exit, work stealing, interrupt, and delayed registration, with PID cleanup in setup/teardown.
activesupport/​test/​parallelization_test.rb Adds an end-to-end worker-crash regression test and updates a stale comment to match the new flow.
activesupport/​CHANGELOG.md Documents the fix referencing #59037.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parallel tests hang forever when a worker process dies

2 participants