Skip to content

Commit ffa12f7

Browse files
[3.13] gh-127057: reschedule proactor datagram read loop after ConnectionResetError (GH-156726) (#156978)
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 5602f08 commit ffa12f7

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
@@ -1454,6 +1454,60 @@ def create_socket():
14541454
transport_1.close()
14551455
transport_2.close()
14561456

1457+
def test_datagram_recvfrom_connection_reset_recovers(self):
1458+
# gh-127057: a UDP socket that sent a datagram to an address that
1459+
# wasn't listening can raise ConnectionResetError on a later
1460+
# receive. The transport must keep working afterwards.
1461+
loop = self.loop
1462+
1463+
class Protocol(asyncio.DatagramProtocol):
1464+
def connection_made(self, transport):
1465+
self.transport = transport
1466+
self.errors = []
1467+
self.received = []
1468+
self.datagram_received_event = loop.create_future()
1469+
1470+
def error_received(self, exc):
1471+
self.errors.append(exc)
1472+
1473+
def datagram_received(self, data, addr):
1474+
self.received.append(data)
1475+
if not self.datagram_received_event.done():
1476+
self.datagram_received_event.set_result(None)
1477+
1478+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1479+
sock.setblocking(False)
1480+
sock.bind(('127.0.0.1', 0))
1481+
addr = sock.getsockname()
1482+
1483+
# Bind and immediately close a second socket to get an address
1484+
# that is guaranteed not to be listening.
1485+
closed = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1486+
closed.bind(('127.0.0.1', 0))
1487+
closed_addr = closed.getsockname()
1488+
closed.close()
1489+
1490+
# Trigger the error before the socket is wrapped in a transport,
1491+
# so that the first read raises synchronously.
1492+
sock.sendto(b'x', closed_addr)
1493+
1494+
transport, protocol = loop.run_until_complete(
1495+
loop.create_datagram_endpoint(Protocol, sock=sock))
1496+
1497+
transport.sendto(b'ping', addr)
1498+
loop.run_until_complete(asyncio.wait_for(
1499+
protocol.datagram_received_event, support.SHORT_TIMEOUT))
1500+
self.assertEqual(protocol.received, [b'ping'])
1501+
1502+
if sys.platform == 'win32':
1503+
# Other platforms don't report ICMP errors on an
1504+
# unconnected UDP socket.
1505+
self.assertEqual(len(protocol.errors), 1)
1506+
self.assertIsInstance(protocol.errors[0], ConnectionResetError)
1507+
1508+
transport.close()
1509+
test_utils.run_briefly(loop)
1510+
14571511
def test_internal_fds(self):
14581512
loop = self.create_event_loop()
14591513
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)