diff --git a/CHANGES.rst b/CHANGES.rst index 8638e1c..43dd973 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,10 @@ Changelog - Fix missing teardown for session and module scoped fixtures when fixture teardown fails. Fixes `#314 `_. +- Fix ``_sock_recv`` infinite loop when the StatusDB TCP connection drops. + ``recv(1)`` returning empty bytes (closed connection) was not handled, + causing workers to spin at 100% CPU indefinitely during xdist runs. + 16.1 (2025-10-10) ----------------- diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index 63223ae..9e0ffb0 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -436,6 +436,8 @@ def _sock_recv(self, conn) -> str: buf = b"" while True: b = conn.recv(1) + if not b: + raise ConnectionError("StatusDB connection closed unexpectedly") if b == self.delim: break buf += b diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index 7156bc9..088aea5 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -1,10 +1,11 @@ import random +import socket import time from unittest import mock import pytest -from pytest_rerunfailures import HAS_PYTEST_HANDLECRASHITEM +from pytest_rerunfailures import HAS_PYTEST_HANDLECRASHITEM, SocketDB pytest_plugins = "pytester" @@ -1407,3 +1408,21 @@ def test_fail(): result = testdir.runpytest("--force-reruns", "3") assert_outcomes(result, passed=0, failed=1, rerun=3) + + +def test_sock_recv_raises_on_closed_connection(): + """_sock_recv should raise ConnectionError when recv returns empty bytes + (connection closed), not loop forever. + + Previously, _sock_recv had no check for empty bytes from recv(1), causing + an infinite CPU-spinning loop when the server-side connection dropped. + This manifested as indefinite hangs during xdist test runs. + """ + s1, s2 = socket.socketpair() + s2.close() # Close one end — recv on s1 will return b"" + + db = SocketDB() + with pytest.raises(ConnectionError, match="closed unexpectedly"): + db._sock_recv(s1) + + s1.close()