- uvloop version: 0.23.0 (also 0.22.1)
- Python version: 3.12.3
- Platform: Linux (x86_64, glibc 2.39)
- Can you reproduce the bug with
PYTHONASYNCIODEBUG in env?: Yes
- Does uvloop behave differently from vanilla asyncio? How?: Yes. With vanilla asyncio,
abort() silently discards the queued datagram and calls connection_lost(None). With uvloop, connection_lost(None) is called too, but two errors are also passed to the loop's exception handler.
Calling abort() on a UDPTransport while a datagram is still queued (because the OS refused it with EAGAIN) reports two errors to the loop's exception handler:
Fatal error on transport UDPTransport (Fatal write error on datagram transport), with exception=CancelledError(). The queued send is cancelled because we aborted, so this is not an error.
protocol.resume_writing() failed, with AttributeError("'NoneType' object has no attribute 'resume_writing'"), raised from UVBaseTransport._maybe_resume_protocol after the protocol has already been detached.
A UNIX datagram socket is used to make the OS refuse datagrams, because UDP on loopback never exerts back-pressure. The peer never reads, so the send buffer fills up.
import asyncio
import socket
import sys
import tempfile
import uvloop
class Protocol(asyncio.DatagramProtocol):
def connection_made(self, transport):
transport.set_write_buffer_limits(0)
def connection_lost(self, exc):
print("connection_lost", exc)
async def main():
loop = asyncio.get_running_loop()
errors = []
loop.set_exception_handler(lambda loop, context: errors.append(context))
path = f"{tempfile.mkdtemp()}/peer.sock"
peer = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
peer.bind(path) # never read from
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.connect(path)
sock.setblocking(False)
transport, _ = await loop.create_datagram_endpoint(Protocol, sock=sock)
# Send until the OS refuses a datagram and the transport has to queue it
while not transport.get_write_buffer_size():
transport.sendto(b"x" * 64)
transport.abort()
await asyncio.sleep(0.1)
for context in errors:
print("exception handler:", context["message"], repr(context.get("exception")))
if sys.argv[1:] == ["uvloop"]:
uvloop.run(main())
else:
asyncio.run(main())
Output with python repro.py:
Output with python repro.py uvloop:
connection_lost None
exception handler: Fatal error on transport UDPTransport (Fatal write error on datagram transport) CancelledError()
exception handler: protocol.resume_writing() failed AttributeError("'NoneType' object has no attribute 'resume_writing'")
Expected: the same as vanilla asyncio, i.e. the queued datagram is silently discarded and nothing is reported to the exception handler.
Found via anyio, whose UDP socket aclose() aborts the transport.
PYTHONASYNCIODEBUGin env?: Yesabort()silently discards the queued datagram and callsconnection_lost(None). With uvloop,connection_lost(None)is called too, but two errors are also passed to the loop's exception handler.Calling
abort()on aUDPTransportwhile a datagram is still queued (because the OS refused it withEAGAIN) reports two errors to the loop's exception handler:Fatal error on transport UDPTransport (Fatal write error on datagram transport), withexception=CancelledError(). The queued send is cancelled because we aborted, so this is not an error.protocol.resume_writing() failed, withAttributeError("'NoneType' object has no attribute 'resume_writing'"), raised fromUVBaseTransport._maybe_resume_protocolafter the protocol has already been detached.A UNIX datagram socket is used to make the OS refuse datagrams, because UDP on loopback never exerts back-pressure. The peer never reads, so the send buffer fills up.
Output with
python repro.py:Output with
python repro.py uvloop:Expected: the same as vanilla asyncio, i.e. the queued datagram is silently discarded and nothing is reported to the exception handler.
Found via anyio, whose UDP socket
aclose()aborts the transport.