@@ -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