Skip to content

Keep watching an fd after a poll error on Linux - #791

Open
Ayush701-code-zm wants to merge 1 commit into
MagicStack:masterfrom
Ayush701-code-zm:fix-poll-error-keep-watching-787
Open

Ayush701-code-zm wants to merge 1 commit into
MagicStack:masterfrom
Ayush701-code-zm:fix-poll-error-keep-watching-787

Conversation

@Ayush701-code-zm

Copy link
Copy Markdown

Fixes #787

On Linux, libuv reports POLLERR on a polled fd as UV_EBADF after it has stopped the watcher. __on_uvpoll_event passed that to UVPoll._fatal_error, which ran the callbacks once, closed the poll and left the closed UVPoll in Loop._polls. After that the fd was no longer watched, a later add_writer() raised RuntimeError: ... the handler is closed, and re-arming from inside the callback did nothing.

This change makes a poll error behave like asyncio's level-triggered epoll:

  • _on_poll_error runs the reader and writer callbacks as for a ready fd, then restarts the watcher with the current mask if a reader or writer is still set. The callbacks keep being called until they are removed, as in asyncio.
  • If the fd is gone (checked with os.fstat, since restarting a watcher on a closed fd makes libuv abort in epoll_ctl) or uv_poll_start fails, _close_on_error drops the poll from Loop._polls, closes it and reports the error to the exception handler. A later add_reader()/add_writer() then creates a new poll.
  • _fatal_error is unchanged and still used for the start/stop failure paths.

Tests: four Linux-only tests in tests/test_base.py, run for both uvloop and asyncio, based on the reproducers in the issue (writer keeps being called after the read end is closed, add_writer after the error with and without remove_writer, and a callback re-arming itself). They fail on master for uvloop and pass for asyncio; with this change all pass. The full test suite passes locally (Linux x86_64, Python 3.12, bundled libuv 1.53.0).

One thing to note: like asyncio, an fd stuck in an error state now keeps calling its callback every iteration until it is removed. If you'd prefer only the smaller fix (dropping the closed poll from _polls and reporting the error), I can trim it to that.

On Linux, libuv reports POLLERR to the poll callback as UV_EBADF after
it has stopped the watcher. UVPoll handled this as a fatal error: it
ran the reader and the writer once, dropped them and closed the handle,
which stayed in Loop._polls. The fd was then no longer watched, a later
add_writer() raised RuntimeError and re-arming from the callback did
nothing.

Match asyncio (level-triggered epoll) instead: run the callbacks as for
a ready fd and start the poll again, so they keep being called until
they are removed. If the fd is gone or the restart fails, close the
handle, remove it from Loop._polls so that a later add_reader() or
add_writer() creates a new one, and report the error to the exception
handler.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

uvloop stops watching a handle after an error of it on Linux

1 participant