Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog/1314.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Workers that finish all their tests are now torn down immediately, freeing
their subprocess memory instead of keeping them alive until the session ends.
2 changes: 2 additions & 0 deletions src/xdist/dsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def worker_workerfinished(self, node: WorkerController) -> None:
crashitem = self.sched.remove_node(node)
assert not crashitem, (crashitem, node)
self._active_nodes.remove(node)
node.ensure_teardown()

def worker_internal_error(
self, node: WorkerController, formatted_error: str
Expand All @@ -228,6 +229,7 @@ def worker_internal_error(
here ourselves using the formatted message.
"""
self._active_nodes.remove(node)
node.ensure_teardown()
try:
assert False, formatted_error
except AssertionError:
Expand Down
20 changes: 16 additions & 4 deletions src/xdist/workermanage.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ def setup_node(
return node

def teardown_nodes(self) -> None:
self.group.terminate(self.EXIT_TIMEOUT)
try:
self.group.terminate(self.EXIT_TIMEOUT)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This part shouldn't error

except OSError:
pass

def _gettxspecs(self) -> list[execnet.XSpec]:
return [execnet.XSpec(x) for x in parse_tx_spec_config(self.config)]
Expand Down Expand Up @@ -357,11 +360,20 @@ def ensure_teardown(self) -> None:
if not self.channel.isclosed():
self.log("closing", self.channel)
self.channel.close()
# del self.channel
if hasattr(self, "gateway"):
self.log("exiting", self.gateway)
self.gateway.exit()
# del self.gateway
try:
self.gateway.exit()
except OSError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This error chain is terrifying we may need to fix execnet

pass
try:
self.gateway.join(timeout=1)
except Exception:
pass
try:
self.gateway._io.wait()
except Exception:
pass

def send_runtest_some(self, indices: Sequence[int]) -> None:
self.sendcommand("runtests", indices=indices)
Expand Down
38 changes: 38 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,44 @@ def pytest_testnodedown(node, error):
assert "s2call" in s
assert "Interrupted" in s

def test_idle_worker_freed_early(self, pytester: pytest.Pytester) -> None:
"""Workers that finish all their tests should have their gateway
process terminated early, before the session ends (#1314)."""
pytester.makepyfile(
test_fast="""
import os
import pathlib

def test_fast(request):
pid_file = request.config.rootpath / "fast_worker_pid.txt"
pid_file.write_text(str(os.getpid()))
""",
test_slow="""
import os
import time
import pathlib

def test_slow(request):
pid_file = request.config.rootpath / "fast_worker_pid.txt"
# Wait for the fast worker to write its PID and be torn down.
for _ in range(40):
time.sleep(0.1)
if pid_file.exists():
pid = int(pid_file.read_text())
# Check if the process is gone (killed by ensure_teardown).
try:
os.kill(pid, 0)
except OSError:
# Process is dead - early teardown worked.
return
raise AssertionError(
"Fast worker process was not terminated during slow test"
)
""",
)
result = pytester.runpytest("-n2", "--dist=load")
result.assert_outcomes(passed=2)

def test_keyboard_interrupt_dist(self, pytester: pytest.Pytester) -> None:
# xxx could be refined to check for return code
pytester.makepyfile(
Expand Down
3 changes: 3 additions & 0 deletions testing/test_dsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def send_steal(self, indices: Sequence[int]) -> None:
def shutdown(self) -> None:
self._shutdown = True

def ensure_teardown(self) -> None:
pass

@property
def shutting_down(self) -> bool:
return self._shutdown
Expand Down
Loading