Skip to content

Commit 6abcdfc

Browse files
committed
Fix hang caused by steal command with empty test queue
Fixes #884
1 parent 58fd7cc commit 6abcdfc

File tree

3 files changed

+50
-2
lines changed

3 files changed

+50
-2
lines changed

changelog/884.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed hang in ``worksteal`` scheduler.

src/xdist/remote.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def worker_title(title):
5858

5959
class WorkerInteractor:
6060
SHUTDOWN_MARK = object()
61+
QUEUE_REPLACED_MARK = object()
6162

6263
def __init__(self, config, channel):
6364
self.config = config
@@ -72,6 +73,15 @@ def __init__(self, config, channel):
7273
def _make_queue(self):
7374
return self.channel.gateway.execmodel.queue.Queue()
7475

76+
def _get_next_item_index(self):
77+
"""Gets the next item from test queue. Handles the case when the queue
78+
is replaced concurrently in another thread.
79+
"""
80+
result = self.torun.get()
81+
while result is self.QUEUE_REPLACED_MARK:
82+
result = self.torun.get()
83+
return result
84+
7585
def sendevent(self, name, **kwargs):
7686
self.log("sending", name, kwargs)
7787
self.channel.send((name, kwargs))
@@ -136,19 +146,22 @@ def old_queue_get_nowait_noraise():
136146
self.torun.put(i)
137147

138148
self.sendevent("unscheduled", indices=stolen)
149+
old_queue.put(self.QUEUE_REPLACED_MARK)
139150

140151
@pytest.hookimpl
141152
def pytest_runtestloop(self, session):
142153
self.log("entering main loop")
143154
self.channel.setcallback(self.handle_command, endmarker=self.SHUTDOWN_MARK)
144-
self.nextitem_index = self.torun.get()
155+
self.nextitem_index = self._get_next_item_index()
145156
while self.nextitem_index is not self.SHUTDOWN_MARK:
146157
self.run_one_test()
147158
return True
148159

149160
def run_one_test(self):
161+
self.item_index = self.nextitem_index
162+
self.nextitem_index = self._get_next_item_index()
163+
150164
items = self.session.items
151-
self.item_index, self.nextitem_index = self.nextitem_index, self.torun.get()
152165
item = items[self.item_index]
153166
if self.nextitem_index is self.SHUTDOWN_MARK:
154167
nextitem = None

testing/test_remote.py

+34
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,40 @@ def test_func4(): pass
271271
ev = worker.popevent("workerfinished")
272272
assert "workeroutput" in ev.kwargs
273273

274+
def test_steal_empty_queue(self, worker: WorkerSetup, unserialize_report) -> None:
275+
worker.pytester.makepyfile(
276+
"""
277+
def test_func(): pass
278+
def test_func2(): pass
279+
"""
280+
)
281+
worker.setup()
282+
ev = worker.popevent("collectionfinish")
283+
ids = ev.kwargs["ids"]
284+
assert len(ids) == 2
285+
worker.sendcommand("runtests_all")
286+
287+
for when in ["setup", "call", "teardown"]:
288+
ev = worker.popevent("testreport")
289+
rep = unserialize_report(ev.kwargs["data"])
290+
assert rep.nodeid.endswith("::test_func")
291+
assert rep.when == when
292+
293+
worker.sendcommand("steal", indices=[0, 1])
294+
ev = worker.popevent("unscheduled")
295+
assert ev.kwargs["indices"] == []
296+
297+
worker.sendcommand("shutdown")
298+
299+
for when in ["setup", "call", "teardown"]:
300+
ev = worker.popevent("testreport")
301+
rep = unserialize_report(ev.kwargs["data"])
302+
assert rep.nodeid.endswith("::test_func2")
303+
assert rep.when == when
304+
305+
ev = worker.popevent("workerfinished")
306+
assert "workeroutput" in ev.kwargs
307+
274308

275309
def test_remote_env_vars(pytester: pytest.Pytester) -> None:
276310
pytester.makepyfile(

0 commit comments

Comments
 (0)