Skip to content

UDPTransport.abort() with a queued datagram reports a fatal write error and a resume_writing() AttributeError #771

Description

@graingert
  • 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:

  1. 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.
  2. 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:

connection_lost None

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions