Fix loadscope-family schedulers hanging the run after a worker crash - #1371
Open
davidheff wants to merge 1 commit into
Open
Fix loadscope-family schedulers hanging the run after a worker crash#1371davidheff wants to merge 1 commit into
davidheff wants to merge 1 commit into
Conversation
When a worker crashed, LoadScopeScheduling.remove_node returned the dead worker's entire workload to the workqueue, completed work units included, because mark_test_complete only flips a flag and nothing prunes finished units. Assigning such a unit sends a node an empty "runtests" command, so the node never produces a test report, and a report is the only thing that drives scheduling onwards: the run hung with every node idle and work still queued. It strands tests at the end of a run, because re-queued units join the back of the queue. A second fault compounded it: a worker holds its final pending test in reserve, not starting it until further work or a shutdown notice arrives, so a node given a single re-scheduled test also fell silent. Fixes pytest-dev#784. remove_node now returns only work units that still hold pending tests, and _reschedule tops a node up until it holds at least two pending tests, shutting it down instead when the queue runs dry, which is what releases the reserved test. The crashed test itself is no longer re-queued either: it is reported as failed by handle_crashitem, and re-running it just crashed the replacement worker in turn. Marking it complete matches LoadScheduling, which pops the crashed item. All three changes apply to loadscope, loadfile and loadgroup alike, which share this scheduler. Unit tests cover both scheduler faults, and an acceptance test crashes a real worker after it has completed a whole scope: without the fix it hangs indefinitely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #784.
The hang
When a worker crashes under
--dist=loadscope,loadfileorloadgroup, the run can hang indefinitely with every worker idle, the controller waiting on its queue, and a handful of tests never run. We hit this repeatedly in a ~14,000-test suite under--dist=loadgroup: the run stopped at 99% with all sixteen workers parked intorun.get(), the controller parked in its ownqueue.get(), and nineteen collected tests stranded.Two faults in
LoadScopeSchedulingcombine to cause it:remove_nodere-queues completed work units. A node's workload keeps every work unit it has ever been given —mark_test_completeonly flips a flag, and nothing prunes finished units. When a worker crashes,remove_nodeputs the entire workload back withself.workqueue.update(workload), so the queue gains one already-finished work unit for every scope the dead worker got through. Assigning one of those to a node sends it an emptyruntestscommand (_assign_work_unitfilters completed tests out of the indices), so the node reports nothing and waits forever. Since a test report is the only thing that drives scheduling onwards, the run wedges — at the end of a run, because re-queued units join the back of the queue.A node holding a single test cannot make progress. A worker holds its final pending test in reserve, not starting it until further work or a shutdown notice arrives (the same behaviour that Ensure loadscope and loadfile work when the first scope has one work unit #277 addressed for initial distribution).
_rescheduleassigns exactly one work unit, so a previously idle node (e.g. the replacement worker) topped up with a single-test unit never starts it, never reports, and is never scheduled again.The fix
remove_nodereturns only work units that still hold pending tests to the queue._reschedulekeeps assigning until the node holds at least two pending tests, and shuts the node down when the queue runs dry — which is what releases its reserved test.handle_crashitem, and re-running it just crashed the replacement worker in turn. Marking it complete matchesLoadScheduling, which pops the crashed item.All three changes live in
LoadScopeScheduling, soloadscope,loadfileandloadgroupare all covered.Tests
test_dsession.py(the first such tests forLoadScopeScheduling): one pins the re-queue behaviour after a crash (completed units dropped, crashed test marked complete, pending work preserved), the other pins the top-up guarantee for a node that cannot otherwise report.TestNodeFailurecrashes a real worker after it has completed a whole scope, under-n1 --dist=loadfile. Without the fix it hangs indefinitely (guarded by a 120 s subprocess timeout); with it, the replacement worker runs the remaining tests and the run completes with1 failed, 3 passed.All three new tests were verified to fail against the unfixed scheduler, and the full
testing/suite passes with the fix (one pre-existing non-strict xfail,TestNodeFailure::test_each_multiple"#20: xdist race condition on node restart", xpasses on my machine).We have been running the
_reschedulelogic here as a monkeypatch in production for a while: a crash injected into the real 14,000-test suite now recovers cleanly where it previously wedged the run.🤖 Generated with Claude Code