Skip to content

Commit 4387744

Browse files
[3.15] gh-127057: reschedule proactor datagram read loop after ConnectionResetError (GH-156726) (#156976)
gh-127057: reschedule proactor datagram read loop after ConnectionResetError (GH-156726) (cherry picked from commit 0ea2971) Co-authored-by: Thomas Grainger <tagrain@gmail.com>
1 parent 7ad4634 commit 4387744

3 files changed

Lines changed: 66 additions & 0 deletions

File tree

Lib/asyncio/proactor_events.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,15 @@ def _loop_reading(self, fut=None):
571571
else:
572572
self._read_fut = self._loop._proactor.recvfrom(self._sock,
573573
self.max_size)
574+
except ConnectionResetError as exc:
575+
# WSARecvFrom() reports a stale ICMP port unreachable
576+
# notification as a synchronous ConnectionResetError when the
577+
# same socket was used to send to an address that is not
578+
# listening. This is transient, so reschedule the read loop
579+
# instead of leaving it dead.
580+
self._protocol.error_received(exc)
581+
if not self._closing:
582+
self._loop.call_soon(self._loop_reading)
574583
except OSError as exc:
575584
self._protocol.error_received(exc)
576585
except exceptions.CancelledError:

Lib/test/test_asyncio/test_events.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,60 @@ def create_socket():
15831583
transport_1.close()
15841584
transport_2.close()
15851585

1586+
def test_datagram_recvfrom_connection_reset_recovers(self):
1587+
# gh-127057: a UDP socket that sent a datagram to an address that
1588+
# wasn't listening can raise ConnectionResetError on a later
1589+
# receive. The transport must keep working afterwards.
1590+
loop = self.loop
1591+
1592+
class Protocol(asyncio.DatagramProtocol):
1593+
def connection_made(self, transport):
1594+
self.transport = transport
1595+
self.errors = []
1596+
self.received = []
1597+
self.datagram_received_event = loop.create_future()
1598+
1599+
def error_received(self, exc):
1600+
self.errors.append(exc)
1601+
1602+
def datagram_received(self, data, addr):
1603+
self.received.append(data)
1604+
if not self.datagram_received_event.done():
1605+
self.datagram_received_event.set_result(None)
1606+
1607+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1608+
sock.setblocking(False)
1609+
sock.bind(('127.0.0.1', 0))
1610+
addr = sock.getsockname()
1611+
1612+
# Bind and immediately close a second socket to get an address
1613+
# that is guaranteed not to be listening.
1614+
closed = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1615+
closed.bind(('127.0.0.1', 0))
1616+
closed_addr = closed.getsockname()
1617+
closed.close()
1618+
1619+
# Trigger the error before the socket is wrapped in a transport,
1620+
# so that the first read raises synchronously.
1621+
sock.sendto(b'x', closed_addr)
1622+
1623+
transport, protocol = loop.run_until_complete(
1624+
loop.create_datagram_endpoint(Protocol, sock=sock))
1625+
1626+
transport.sendto(b'ping', addr)
1627+
loop.run_until_complete(asyncio.wait_for(
1628+
protocol.datagram_received_event, support.SHORT_TIMEOUT))
1629+
self.assertEqual(protocol.received, [b'ping'])
1630+
1631+
if sys.platform == 'win32':
1632+
# Other platforms don't report ICMP errors on an
1633+
# unconnected UDP socket.
1634+
self.assertEqual(len(protocol.errors), 1)
1635+
self.assertIsInstance(protocol.errors[0], ConnectionResetError)
1636+
1637+
transport.close()
1638+
test_utils.run_briefly(loop)
1639+
15861640
def test_internal_fds(self):
15871641
loop = self.create_event_loop()
15881642
if not isinstance(loop, selector_events.BaseSelectorEventLoop):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :class:`asyncio.ProactorEventLoop` UDP transports so that a
2+
:exc:`ConnectionResetError` raised by ``WSARecvFrom`` no longer stops the
3+
read loop.

0 commit comments

Comments
 (0)