Loop.close() can hang indefinitely on an in-flight libuv getaddrinfo request
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- python
- 領域
- backend, networking
調査の方向性
Start by reading uvloop/loop.pyx around Loop._close() and trace the final UV_RUN_DEFAULT drain against outstanding uv_getaddrinfo requests. Run the provided deterministic reproducer on Linux with uvloop 0.22.1, then check behavior on 0.23.0. Done means Loop.close() does not wait indefinitely for blocked native DNS work while preserving request and callback lifetime safety; the issue does not name a regression test file.
索引モデルが issue の本文から書いたものです。
説明
Summary
uvloop.Loop.close() can block indefinitely when an in-flight libuv uv_getaddrinfo request is still running in a worker thread. The final synchronous uv_run(UV_RUN_DEFAULT) in Loop._close() waits for outstanding requests, even after all open handles have been closed. If native DNS resolution stalls, close() stalls with it.
I reproduced this deterministically on uvloop 0.22.1 / CPython 3.13.5 / Linux x86_64 using an LD_PRELOAD shim to delay getaddrinfo() (no broken DNS or network access required). Under the same conditions, standard-library asyncio's loop.close() returns immediately.
Version caveat: I have not executed the reproducer against the 0.23.0 binary. However, the relevant shutdown code remains in the v0.23.0 tag, which upgrades vendored libuv to 1.52.1. The findings below are runtime results for 0.22.1, not a claimed runtime confirmation for 0.23.0.
Environment verified
- OS: Linux x86_64 (kernel 6.18.44 in the reproduced run)
- Python: CPython 3.13.5
- uvloop: 0.22.1
- Comparison: stdlib asyncio on the same Python installation
- Build prerequisite for deterministic test: C compiler (
cc)
Minimal deterministic reproducer
Reproducer files (gist):
reproduce_uvloop_close_hang.py: self-contained runner; compiles its own C interception shim, runs uvloop and asyncio in isolated child processes, and enforces a watchdog timeout.uvloop_close_minimal.pyanduvloop_close_block_dns.c: smaller two-file version, Linux only.observed_results.txt: full captured output of the verified runs.
On Linux:
python -m pip install 'uvloop==0.22.1'
python reproduce_uvloop_close_hang.py --block-seconds 10 --timeout 3
To check the latest reported release (not yet validated by me):
python -m pip install 'uvloop==0.23.0'
python reproduce_uvloop_close_hang.py --block-seconds 10 --timeout 3
The runner prints the version actually installed. It injects a 10-second delay in native getaddrinfo() for block-uvloop-test.invalid, schedules loop.getaddrinfo() without awaiting completion, and calls loop.close() after the worker has started. The watchdog applies to close() as observed from the parent process. It uses os._exit(0) only after close() returns to prevent Python interpreter shutdown from independently waiting for a still-running executor thread in the asyncio control case.
Observed result (uvloop 0.22.1)
10-second native DNS delay; 3-second watchdog:
--- uvloop ---
IMPL: uvloop 0.22.1
SHIM_STARTED: native getaddrinfo; sleeping 10s
PRE_CLOSE: task_done=False
CLOSE_ENTER
RESULT: WATCHDOG_TIMEOUT
CONFIRMED: close() did not return while DNS was blocked
--- asyncio ---
IMPL: stdlib asyncio
SHIM_STARTED: native getaddrinfo; sleeping 10s
PRE_CLOSE: task_done=False
CLOSE_ENTER
CLOSE_RETURN after 0.000s
RESULT: EXIT_0
CONTROL: stdlib asyncio close() returned
2-second native DNS delay; 6-second watchdog:
- uvloop's
close()returned after 1.700 seconds insideclose()(the DNS work had already been running for about 0.3 seconds). - During DNS completion, uvloop logged an unhandled
RuntimeError: Event loop is closedfrom__on_addrinfo_resolved/_getaddrinfo.callbackattempting to schedule a callback. - The stdlib asyncio control still returned immediately.
No transport or socket leak is required to reproduce the stall.
Likely cause
In uvloop/loop.pyx on the v0.23.0 tag, Loop._close() ends its handle cleanup with:
__close_all_handles(self)
self._shutdown_signals()
# During this run there should be no open handles,
# so it should finish right away
self.__run(uv.UV_RUN_DEFAULT)
The assumption does not account for active requests. uv_getaddrinfo uses a uv_getaddrinfo_t request, which is not a handle visited by uv_walk(). libuv documents that UV_RUN_DEFAULT runs until no active referenced handles or requests remain. Its request API also documents that uv_cancel() cannot cancel an already executing request and that callback ownership/lifetime constraints persist after cancellation.
As a result, if the underlying resolver never returns, a synchronous Loop.close() can remain stuck in that final drain indefinitely.
Expected behavior
Calling loop.close() should not block indefinitely on invisible, uninterruptible native DNS/threadpool work. This differs from the stdlib asyncio close contract, which explicitly does not wait for executor work to finish. A fix should preserve libuv request and callback lifetime safety; simply stopping the drain without cleaning up native requests may not be safe.
Related background
- CPython #78218: discussion of not waiting for background executor work during
close(). - uvloop #335: shutdown hang after
KeyboardInterrupt, but no establisheduv_getaddrinfomechanism. - uvloop #414: intermittent hangs/FD errors, addressed by an unrelated double-close fix.
I did not find an existing issue or PR documenting this specific Loop._close() → UV_RUN_DEFAULT → active uv_getaddrinfo stall. The attached deterministic reproducer could also be adapted into a regression test.
- 主要言語
- Cython
- スター
- 11.9k
- フォーク
- 615
- 平均マージ
- 4時間 43分
- マージ済み PR(30日)
- 3
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MagicStack/uvloop のほかの issue
-
uv 1.53.0 fix対応中かも @fantix が 6 日前に担当しました。 オープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
MagicStack/uvloop#774 · コメント 1 件 · リアクション 1 件 ·
-
License not clearオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#759 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#741 · リアクション 2 件 ·
-
`asyncio.get_event_loop` raises error instead of spawning new loop when no loop is running.対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#702 · コメント 8 件 · リアクション 9 件 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
MagicStack/uvloop#773 ·
MagicStack/uvloop の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
[BUG] Re-subscribing the Mono returned by listTools/listResources/listResourceTemplates/listPrompts/listRoots duplicates the results対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
modelcontextprotocol/java-sdk#1174 · リアクション 1 件 ·
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
NanmiCoder/cc-haha#1533 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
maxrave-dev/SimpMusic#2649 ·
メンテナーはふだん 2 日以内に返信
-
product/backlog
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
TencentBlueKing/bk-nodemgr#3645 ·
メンテナーはふだん 1 日以内に返信