asyncio: SelectorEventLoop busy-loops at 100% CPU forever when the self-pipe socketpair reaches EOF
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 77.2k
- Fork
- 35.9k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Bug description
The selector-side twin of #156333. BaseSelectorEventLoop also wakes itself through a self-socket created by socket.socketpair(). On Windows this is a loopback TCP pair, and if the connection reaches a clean EOF while the loop runs, _read_from_self returns but the reader is still registered on the dead fd — select() reports it readable immediately, forever, pinning one core at 100% with nothing logged.
Lib/asyncio/selector_events.py:
def _read_from_self(self):
while True:
try:
data = self._ssock.recv(4096)
if not data:
break
self._process_self_data(data)
except InterruptedError:
continue
except BlockingIOError:
break
At EOF, recv() returns b'', the loop breaks, and the callback returns. But the fd is still registered via _add_reader(self._ssock.fileno(), self._read_from_self) from _make_self_pipe(). A closed-for-read socket is permanently "readable", so every select() iteration re-fires the callback — a tight spin. The loop is otherwise idle and never recovers.
The proactor half of this bug is fixed by #156343 (rebuild the pair on the empty result). The same OS teardown of the loopback pair that triggers it triggers this on any WindowsSelectorEventLoopPolicy process.
Reproducer (deterministic, Windows)
import asyncio, socket, time
calls = 0
orig = asyncio.selector_events.BaseSelectorEventLoop._read_from_self
def counting(self):
global calls
calls += 1
return orig(self)
asyncio.selector_events.BaseSelectorEventLoop._read_from_self = counting
async def main():
loop = asyncio.get_running_loop()
await asyncio.sleep(0.1)
loop._csock.shutdown(socket.SHUT_WR) # graceful half-close = clean EOF
global calls
calls = 0
start = time.process_time()
await asyncio.sleep(3)
print(f"_read_from_self calls during 3s idle: {calls}")
print(f"CPU consumed while sleeping 3s: {time.process_time() - start:.2f}s")
loop = asyncio.SelectorEventLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
Measured on Windows 11, CPython main (3.16.0a0, self-built): 582,692 callback invocations, 1.23s CPU, during a 3-second idle sleep. Expected ~0.
Note the instrumentation must patch the class before the loop is created — _make_self_pipe() captures the bound method at registration time, so patching an instance afterwards does not intercept the already-armed reader (which is also why the spin is invisible to profilers that hook late).
Environment
- Windows 11, self-built main (3.16.0a0)
- The proactor variant (#156333) reproduces on installed 3.12.10 / 3.13.12; the
_read_from_selfcode path shown above is unchanged on main
Notes on scope
- Unix loops use an AF_UNIX pair, which the OS does not tear down this way, so the organic trigger is Windows-specific; a deterministic test using
shutdown(SHUT_WR)works on any platform, though. - Related, on the proactor side: the same teardown can also surface as
ConnectionResetErrorfrom the pending recv instead of a clean EOF; that path is currently unhandled too (noted in #156343).
Suggested direction
On recv() == b'', rebuild the self-pipe the way #156343 does for the proactor loop: allocate the new pair first, move any signal wakeup fd registration (set_wakeup_fd() returns the previous fd, so it can be probed and moved only when it names the old socket — Unix loops with signal handlers), remove the old reader, close the old sockets, and register the reader on the new fd.
Linked PRs
- gh-156345
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu trong Lib/asyncio/selector_events.py với _read_from_self và _make_self_pipe, sau đó so sánh bản sửa lỗi proactor trong #156343 và công việc liên quan trong #156345. Tái hiện trường hợp EOF của self-pipe trên Windows và xác minh rằng một selector loop đang rảnh không còn gọi reader lặp đi lặp lại, trong khi việc đăng ký signal wakeup vẫn chính xác.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- backend, operating-systems
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 25/100