| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…y-looping When the self-pipe socketpair of a BaseSelectorEventLoop reaches a clean EOF (e.g. the OS tears the connection down across a power or session state change on Windows), _read_from_self broke out of its read loop but left the reader registered on the dead socket. A closed-for-read socket is permanently readable, so every select() iteration re-fired the callback: one core pinned at 100% CPU with nothing logged, measured at 582k callback invocations during a 3-second idle sleep. Rebuild the pair instead: allocate the replacement before touching the old sockets so an allocation failure leaves the previous state intact, move the process-wide signal wakeup fd to the new socket when (and only when) it is registered on our _csock -- restoring foreign registrations untouched, and keeping the old write end open when it cannot be moved from a worker thread -- then remove the old reader, close the old sockets, and register the reader on the new socket.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Root cause
BaseSelectorEventLoop wakes itself through a socketpair created in
_make_self_pipe(). When that pair reaches a clean EOF — on Windows the
pair is a loopback TCP connection, and the OS can tear it down across
power or session state changes (sleep/hibernate, RDP disconnect, fast
user switching) — _read_from_self() breaks out of its drain loop:
but the reader stays registered on the dead _ssock. A peer-closed
socket is permanently readable, so every select() iteration re-fires
the callback: one core pinned at 100% CPU with nothing logged,
measured at 582,692 _read_from_self invocations during a 3-second
idle sleep (1.23s CPU). The loop never recovers.
This is the selector-side twin of gh-156333 (proactor loop, fixed in
#156343); the reporter of that issue hit the same teardown on a
WindowsSelectorEventLoop at 3.13.15.
The fix
_rebuild_self_pipe(), called from _read_from_self() on clean EOF,
mirrors the proactor-side approach with selector-specific care:
non-blocking. An allocation failure leaves the previous state intact.
signal.set_wakeup_fd(-1) returns the previously registered fd; move
the registration to the new _csock only when it was pointed at our
old _csock. A foreign registration (another loop's self-pipe, a
test fixture) is restored untouched via set_wakeup_fd(old_fd).
main thread; a rebuild triggered from a worker thread keeps the old
_csock open so signal delivery keeps working (one socket leaked
beats silently breaking Python's signal handling), and skips the
wakeup-fd move for the same reason.
sockets (old _csock closed only when it was safe to move), then
register the reader on the new _ssock — restoring the
cross-thread wakeup invariant _make_self_pipe() established.
ConnectionResetError still propagates to the caller unchanged — that
is sock_recv's documented contract and the loop's error-handling path
(call_exception_handler) remains the right place for it.
Tests
Six new tests in test_selector_events.py:
All six fail on the pre-fix code (verified by checking out the original
selector_events.py); after the fix the full test_asyncio suite
passes on Windows (34/34 files, incl. test_selector_events and
test_windows_events) and on macOS.
Measured on the Windows repro from the issue: 582,692 callback
invocations / 1.23s CPU during 3s idle → 1 invocation (the rebuild
itself) / 0.00s CPU, with cross-thread wakeup intact.