Conversation
Aborting a UDPTransport while a datagram was queued reported a fatal CancelledError and a resume_writing() AttributeError to the loop's exception handler, because libuv cancels queued sends when the handle closes and the send callback treated that as an error after the protocol had been detached. Also, a queued datagram that was sent between abort() and the transport closing still resumed the protocol. Like asyncio, stop reporting send results to the protocol once the connection is lost. Fixes MagicStack#771 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UV_ECANCELED check in the send callback was only exercised by closing the loop with a UDP transport still open, as abort() is now handled by the _conn_lost check in _on_sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UDPTransport keeps a reference to itself while receiving, which was only dropped by _stop_reading(). loop.close() closes leftover handles with _close(), so a UDP transport still open at that point was never freed. Stop reading in _close(), as UVStream does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests sent datagrams until the transport queued one, which never happened on macOS CI and spun until the job was killed. Use a small send buffer, as anyio's UNIX datagram tests do, and give up after a bounded number of sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS refuses UNIX datagrams with ENOBUFS rather than EAGAIN. libuv treats ENOBUFS like EAGAIN and queues the datagram, but asyncio reports it to error_received() and drops it, so the send queue never fills. The small send buffer did not change that, so drop it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #771.
Calling
abort()on aUDPTransportwith a datagram queued (the OS refused it withEAGAIN) 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 aCancelledError"Fatal write error", then called_maybe_resume_protocol()after the protocol had already been cleared. That raised theresume_writing()AttributeError.A second difference turned up while fixing this. A queued datagram can still be sent between
abort()and the scheduledconnection_lost(). uvloop then calledresume_writing()on the aborted protocol, which asyncio never does, because its_force_closeclears the buffer.Changes:
__uv_udp_on_senddrops sends that were cancelled because the handle is closed.UDPTransport._on_sentreports nothing to the protocol once_conn_lostis set, i.e. afterabort()or a fatal error. A gracefulclose()still flushes the queue and resumes the protocol, as asyncio does.UDPTransport._closestops receiving, asUVStream._closedoes. The transport holds a reference to itself while receiving, so a UDP transport still open atloop.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 likeEAGAINand queues, but asyncio reports toerror_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 noresume_writing()afterabort().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 fatalCancelledErrorand callsresume_writing(). The test covers theUV_ECANCELEDcheck, which the abort tests don't reach because_conn_lostis already set.All four tests fail on uvloop without this change.
🤖 Generated with Claude Code