Skip to content

Commit d57cb23

Browse files
committed
fix DatagramTransport when errror_recieved calls sendto
1 parent 60187dd commit d57cb23

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

Lib/asyncio/proactor_events.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,10 +534,16 @@ def _loop_writing(self, fut=None):
534534
addr=addr)
535535
except OSError as exc:
536536
self._protocol.error_received(exc)
537-
if self._buffer and not self._conn_lost:
537+
if self._buffer:
538538
# Re-arm the write loop so buffered data isn't stranded and
539539
# a paused protocol is eventually resumed (gh-156698).
540-
self._loop.call_soon(self._loop_writing)
540+
def resume_writing():
541+
# a sendto() may have armed a write in the meantime;
542+
# its own callback will drain the rest of the buffer.
543+
if self._write_fut is None:
544+
self._loop_writing()
545+
546+
self._loop.call_soon(resume_writing)
541547
else:
542548
self._maybe_resume_protocol()
543549
except Exception as exc:

Lib/test/test_asyncio/test_events.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,62 @@ def test_datagram_write_error_resumes_paused_protocol_from_callback(self):
16561656
self._test_datagram_write_error_resumes_paused_protocol(
16571657
b'ok', oversized)
16581658

1659+
def test_datagram_write_error_reentrant_sendto(self):
1660+
# See https://github.com/python/cpython/issues/156698: an
1661+
# error_received() callback that sends more data synchronously
1662+
# can itself arm a new write. The write-loop restart scheduled
1663+
# for the failed write must notice that and not try to start a
1664+
# second, conflicting one.
1665+
loop = self.loop
1666+
unhandled = []
1667+
loop.set_exception_handler(lambda loop, context: unhandled.append(context))
1668+
1669+
class Protocol(asyncio.DatagramProtocol):
1670+
def connection_made(self, transport):
1671+
self.transport = transport
1672+
self.sent_extra = False
1673+
self.errors = []
1674+
self.done = loop.create_future()
1675+
1676+
def datagram_received(self, data, addr):
1677+
if not self.done.done():
1678+
self.done.set_result(None)
1679+
1680+
def error_received(self, exc):
1681+
self.errors.append(exc)
1682+
if not self.sent_extra:
1683+
# Reentrantly kicks off another write while the
1684+
# failing one is still unwinding on the stack.
1685+
self.sent_extra = True
1686+
self.transport.sendto(b'extra', self.addr)
1687+
1688+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1689+
sock.setblocking(False)
1690+
sock.bind(('127.0.0.1', 0))
1691+
transport, protocol = loop.run_until_complete(
1692+
loop.create_datagram_endpoint(Protocol, sock=sock))
1693+
protocol.addr = addr = sock.getsockname()
1694+
1695+
oversized = b'\x00' * 70000
1696+
transport.sendto(oversized, addr)
1697+
transport.sendto(b'queued', addr)
1698+
1699+
# The 'extra' datagram sent from error_received() is delivered
1700+
# back to the same socket; waiting for it proves the write loop
1701+
# kept running instead of wedging or crashing.
1702+
loop.run_until_complete(asyncio.wait_for(protocol.done, 10))
1703+
1704+
test_utils.run_until(
1705+
loop, lambda: transport.get_write_buffer_size() == 0)
1706+
1707+
transport.close()
1708+
test_utils.run_briefly(loop)
1709+
1710+
self.assertTrue(protocol.errors)
1711+
self.assertFalse(
1712+
unhandled,
1713+
f'unhandled exception in the write loop: {unhandled}')
1714+
16591715
def test_internal_fds(self):
16601716
loop = self.create_event_loop()
16611717
if not isinstance(loop, selector_events.BaseSelectorEventLoop):

0 commit comments

Comments
 (0)