Fixes #771.
Calling abort() on a UDPTransport with a datagram queued (the OS refused it with EAGAIN) reported two errors to the loop's exception handler. Vanilla asyncio silently discards the datagram.
When the handle closes, libuv cancels queued sends with UV_ECANCELED. The send callback turned that into a CancelledError "Fatal write error", then called _maybe_resume_protocol() after the protocol had already been cleared. That raised the resume_writing() AttributeError.
A second difference turned up while fixing this. A queued datagram can still be sent between abort() and the scheduled connection_lost(). uvloop then called resume_writing() on the aborted protocol, which asyncio never does, because its _force_close clears the buffer.
Changes:
__uv_udp_on_send drops sends that were cancelled because the handle is closed.
UDPTransport._on_sent reports nothing to the protocol once _conn_lost is set, i.e. after abort() or a fatal error. A graceful close() still flushes the queue and resumes the protocol, as asyncio does.
UDPTransport._close stops receiving, as UVStream._close does. The transport holds a reference to itself while receiving, so a UDP transport still open at loop.close() was never freed. The new loop-close test caught this through the debug build's handle-count check.
Tests run on both uvloop and asyncio. They use a UNIX datagram socket, because UDP over loopback never refuses a datagram. The asyncio variants are skipped on macOS: there the socket refuses datagrams with ENOBUFS, which libuv treats like EAGAIN and queues, but asyncio reports to error_received() and drops.
Tests:
-
test_abort_with_queued_datagram: the case from the issue.
-
test_abort_with_queued_datagram_then_writable: the peer makes room before the loop runs again, and there is no resume_writing() after abort().
-
test_close_with_queued_datagram_then_writable: close() behaviour is unchanged.
-
test_loop_close_with_queued_datagram (uvloop only): closing the loop with the transport still open cancels the queued send. On master this reports a fatal CancelledError and calls resume_writing(). The test covers the UV_ECANCELED check, which the abort tests don't reach because _conn_lost is already set.
All four tests fail on uvloop without this change.
🤖 Generated with Claude Code