@@ -1712,6 +1712,103 @@ def error_received(self, exc):
17121712 unhandled ,
17131713 f'unhandled exception in the write loop: { unhandled } ' )
17141714
1715+ def test_datagram_close_flushes_queued_data (self ):
1716+ # See https://github.com/python/cpython/issues/156920: _conn_lost
1717+ # used to mean "close() was requested" rather than "no more data
1718+ # will be sent". Since add_done_callback() always defers an
1719+ # already-completed write's callback with call_soon(), a sendto()
1720+ # immediately followed by close() -- with no await in between --
1721+ # leaves a write genuinely outstanding at close() time on every
1722+ # platform, not just a slow one. Closing must let that write (and
1723+ # anything queued behind it) drain and still call connection_lost(),
1724+ # instead of tripping the "no more data will be sent" guard before
1725+ # the drain has actually happened and hanging forever.
1726+ loop = self .loop
1727+
1728+ class Receiver (asyncio .DatagramProtocol ):
1729+ def connection_made (self , transport ):
1730+ self .received = []
1731+
1732+ def datagram_received (self , data , addr ):
1733+ self .received .append (data )
1734+
1735+ recv_sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
1736+ recv_sock .setblocking (False )
1737+ recv_sock .bind (('127.0.0.1' , 0 ))
1738+ recv_transport , receiver = loop .run_until_complete (
1739+ loop .create_datagram_endpoint (Receiver , sock = recv_sock ))
1740+ addr = recv_sock .getsockname ()
1741+
1742+ class Protocol (asyncio .DatagramProtocol ):
1743+ def connection_made (self , transport ):
1744+ self .lost = loop .create_future ()
1745+
1746+ def connection_lost (self , exc ):
1747+ if not self .lost .done ():
1748+ self .lost .set_result (exc )
1749+
1750+ sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
1751+ sock .setblocking (False )
1752+ sock .bind (('127.0.0.1' , 0 ))
1753+ transport , protocol = loop .run_until_complete (
1754+ loop .create_datagram_endpoint (Protocol , sock = sock ))
1755+
1756+ # 'first' is still in flight (its completion callback hasn't run
1757+ # yet) and 'second' is queued behind it when close() is called.
1758+ transport .sendto (b'first' , addr )
1759+ transport .sendto (b'second' , addr )
1760+ transport .close ()
1761+
1762+ loop .run_until_complete (asyncio .wait_for (protocol .lost , 10 ))
1763+
1764+ test_utils .run_until (
1765+ loop , lambda : len (receiver .received ) >= 2 )
1766+ self .assertEqual (sorted (receiver .received ), [b'first' , b'second' ])
1767+
1768+ recv_transport .close ()
1769+ test_utils .run_briefly (loop )
1770+
1771+ def test_datagram_close_during_write_error_calls_connection_lost (self ):
1772+ # See https://github.com/python/cpython/issues/156920: if the
1773+ # write that's outstanding when close() is called goes on to fail
1774+ # (rather than succeed), the failure handler used to only re-arm
1775+ # the write loop when data was still queued behind it. If that
1776+ # failing write was the last thing in the buffer, nothing re-armed
1777+ # the loop, so the close() in progress never got to call
1778+ # connection_lost() -- it hung forever instead of finishing once
1779+ # the buffer was actually empty.
1780+ loop = self .loop
1781+
1782+ class Protocol (asyncio .DatagramProtocol ):
1783+ def connection_made (self , transport ):
1784+ self .lost = loop .create_future ()
1785+ self .errors = []
1786+
1787+ def error_received (self , exc ):
1788+ self .errors .append (exc )
1789+
1790+ def connection_lost (self , exc ):
1791+ if not self .lost .done ():
1792+ self .lost .set_result (exc )
1793+
1794+ sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
1795+ sock .setblocking (False )
1796+ sock .bind (('127.0.0.1' , 0 ))
1797+ transport , protocol = loop .run_until_complete (
1798+ loop .create_datagram_endpoint (Protocol , sock = sock ))
1799+ addr = sock .getsockname ()
1800+
1801+ # 'ok' is still in flight when close() is called; 'oversized' is
1802+ # queued behind it and fails once it reaches the front of the
1803+ # buffer, leaving the buffer empty right as the error is handled.
1804+ oversized = b'\x00 ' * 70000
1805+ transport .sendto (b'ok' , addr )
1806+ transport .sendto (oversized , addr )
1807+ transport .close ()
1808+
1809+ loop .run_until_complete (asyncio .wait_for (protocol .lost , 10 ))
1810+ self .assertTrue (protocol .errors )
1811+
17151812 def test_internal_fds (self ):
17161813 loop = self .create_event_loop ()
17171814 if not isinstance (loop , selector_events .BaseSelectorEventLoop ):
0 commit comments