From a3d86c6dfe6f36738c045d138d8185a1c0c4f705 Mon Sep 17 00:00:00 2001 From: Taeknology <20297177+Taeknology@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:35:48 +0900 Subject: [PATCH] gh-132819: Clarify socket.connect() signal interruption behavior --- Doc/library/socket.rst | 12 ++++----- Lib/test/test_socket.py | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 836aa91bb0885b..51aee3bfbbf634 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -1601,12 +1601,12 @@ Socket Objects Connect to a remote socket at *address*. The format of *address* depends on the address family --- see :ref:`socket-addresses`. - If the connection is interrupted by a signal, the method waits until the - connection completes, or raises a :exc:`TimeoutError` on timeout, if the - signal handler doesn't raise an exception and the socket is blocking or has - a timeout. For non-blocking sockets, the method raises an - :exc:`InterruptedError` exception if the connection is interrupted by a - signal (or the exception raised by the signal handler). + If the connection is interrupted by a signal and the signal handler raises + an exception, the method propagates that exception. Otherwise, the method + waits until the connection completes for blocking sockets and sockets with + a timeout. If the timeout expires before the connection completes, the + method raises a :exc:`TimeoutError`. For non-blocking sockets, the method + raises an :exc:`InterruptedError` exception. .. audit-event:: socket.connect self,address socket.socket.connect diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 7bb50f7b8aa47e..8c569f0318def7 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -5190,6 +5190,61 @@ def setAlarm(self, seconds): signal.alarm(seconds) +@unittest.skipUnless(sys.platform == "linux", + "requires Linux TCP backlog behavior") +@requireAttrs(signal, "setitimer", "siginterrupt") +class InterruptedConnectTimeoutTest(unittest.TestCase): + + def setUp(self): + super().setUp() + # On Linux, listen(0) allows one connection to be queued. Filling the + # queue makes the next connect() wait without requiring an external + # TCP blackhole. + self.listener = socket.socket() + self.addCleanup(self.listener.close) + self.listener.bind((HOST, 0)) + self.listener.listen(0) + + queued = socket.socket() + self.addCleanup(queued.close) + queued.connect(self.listener.getsockname()) + + self.client = socket.socket() + self.addCleanup(self.client.close) + + def setAlarm(self, handler, delay, interval=0): + old_handler = signal.signal(signal.SIGALRM, handler) + self.addCleanup(signal.signal, signal.SIGALRM, old_handler) + signal.setitimer(signal.ITIMER_REAL, delay, interval) + self.addCleanup(signal.setitimer, signal.ITIMER_REAL, 0) + + def test_connect_retries_after_signal(self): + self.client.settimeout(0.25) + + signals = 0 + + def handler(signum, frame): + nonlocal signals + signals += 1 + + self.setAlarm(handler, 0.03, 0.03) + + with self.assertRaises(TimeoutError): + self.client.connect(self.listener.getsockname()) + self.assertGreater(signals, 0) + + def test_connect_propagates_signal_handler_exception(self): + self.client.settimeout(support.LOOPBACK_TIMEOUT) + + def handler(signum, frame): + 1 / 0 + + self.setAlarm(handler, 0.03) + + with self.assertRaises(ZeroDivisionError): + self.client.connect(self.listener.getsockname()) + + # Require siginterrupt() in order to ensure that system calls are # interrupted by default. @requireAttrs(signal, "siginterrupt")