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
12 changes: 6 additions & 6 deletions Doc/library/socket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading