safe_terminate() leaves the abandoned termfunc thread running and reports success anyway
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 45/100
- Issue 類型
- 缺陷
- 描述清晰度
- 基本清楚
- 活躍度
- 活躍
- 技術堆疊
- python
研究方向
閱讀 src/execnet/multi.py 中的 safe_terminate 和 Group.terminate,然後檢查 gateway_base.py 中的 WorkerPool.waitall 以及 ThreadExecModel.start 中的執行緒啟動路徑。先重現 timeout 路徑;done 應表示已對被放棄的 termfunc 工作進行處理,waitall 的結果不會被默默丟棄,且終止不會無限期等待。
由索引模型根據 Issue 內容生成。
描述
safe_terminate() abandons the termfunc thread it spawned and then reports success while it is still running.
Versions: execnet 2.1.2, CPython 3.12, Linux. Reached via pytest-xdist 3.8.0 (NodeManager.teardown_nodes → Group.terminate(EXIT_TIMEOUT=10)), but the defect is in safe_terminate itself.
The code
# src/execnet/multi.py:331
def safe_terminate(execmodel, timeout, list_of_paired_functions) -> None:
workerpool = WorkerPool(execmodel)
def termkill(termfunc, killfunc) -> None:
termreply = workerpool.spawn(termfunc)
try:
termreply.get(timeout=timeout)
except OSError:
killfunc()
replylist = []
for termfunc, killfunc in list_of_paired_functions:
reply = workerpool.spawn(termkill, termfunc, killfunc)
replylist.append(reply)
for reply in replylist:
reply.get()
workerpool.waitall(timeout=timeout)
Two problems:
-
The abandoned
termfuncthread is never cancelled. Whentermreply.get(timeout=timeout)raisesOSError,termkillcallskillfunc()and returns — but the thread runningtermfuncis still blocked and keeps running. ForGroup.terminate()thattermfuncisjoin_wait, i.e.gw.join(); gw._io.wait(), so it sits insubprocess.Popen.wait().killfunc(gw._io.kill()) is issued, but nothing waits for it to take effect and nothing joins the thread. -
waitall's timeout result is discarded.WorkerPool.waitall()returnsbool(gateway_base.py:470,return my_waitall_event.wait(timeout=timeout)). Line 349 ignores it, sosafe_terminatereturns normally — and thereforeGroup.terminate()returns normally — even when the abandoned threads from (1) are demonstrably still running.
Relatedly, Group.terminate(timeout) has no overall deadline: reply.get() on line 348 has no timeout at all, so the call can block indefinitely if a killfunc does.
Reproducer
"""safe_terminate() reports success while the termfunc thread it spawned is still running."""
import sys, threading, time
import execnet
from execnet.gateway_base import get_execmodel
from execnet.multi import safe_terminate
print("execnet", execnet.__version__, "| python", sys.version.split()[0])
execmodel = get_execmodel("thread")
entered, release = threading.Event(), threading.Event()
finished = []
def termfunc(): # stands in for join_wait: gw.join(); gw._io.wait()
entered.set()
release.wait(60)
finished.append(True)
def killfunc(): # stands in for kill: gw._io.kill()
pass # a kill that does not immediately reap the child
t0 = time.time()
safe_terminate(execmodel, 1.0, [(termfunc, killfunc)])
elapsed = time.time() - t0
print(f"safe_terminate(timeout=1.0) returned after {elapsed:.1f}s")
print(f" termfunc entered : {entered.is_set()}")
print(f" termfunc finished: {bool(finished)} <-- still running, never cancelled")
release.set()
execnet 2.1.2 | python 3.12.13
safe_terminate(timeout=1.0) returned after 2.0s
termfunc entered : True
termfunc finished: False <-- still running, never cancelled
killfunc is a no-op here to stand in for a kill that does not reap the child promptly; with a real popen gateway the same state is reached whenever Popen.wait() has not returned by the time waitall's timeout expires.
Why it matters
These threads are started by ThreadExecModel.start via _thread.start_new_thread (gateway_base.py:152), so they are not threading.Threads and threading._shutdown() never joins them. Once safe_terminate has returned, the caller is free to finish and the interpreter to finalize while those threads are still executing Python — they are then stopped at an arbitrary GIL checkpoint.
We hit this under pytest-xdist: teardown routinely exceeds xdist's 10s EXIT_TIMEOUT, so Group.terminate() takes the kill path on essentially every run, and faulthandler dumps taken in that window consistently show the leaked threads:
Thread ...: File ".../execnet/multi.py", line 231 in join_wait # x4, in subprocess.wait()
Thread ...: File ".../execnet/multi.py", line 339 in termkill # x3, in Reply.waitfinish
Thread ...: File ".../execnet/multi.py", line 348 in safe_terminate # main, unbounded reply.get()
File ".../execnet/multi.py", line 237 in terminate
File ".../xdist/workermanage.py", line 117 in teardown_nodes
On one such run the process died with SIGSEGV in exactly this window, with the truncated dump ending inside Gateway.__repr__ (reached from kill() at multi.py:234) on a frame whose code object could no longer be resolved. I am not claiming execnet segfaults — a faulthandler.dump_traceback_later watchdog was walking those stacks concurrently and that race is a plausible cause on its own. But the leaked threads are what put live Python into that window in the first place.
Suggested directions
- After
killfunc(), wait ontermreplyagain with a short grace period so the abandoned thread is actually observed to finish. - Propagate the
waitalltimeout instead of discarding it, soGroup.terminate()can tell its caller that gateways did not come down. - Give
Group.terminate(timeout)a single overall deadline rather than the currenttimeoutat line 339 + unbounded wait at 348 +timeoutat 349.
Happy to send a PR if you have a preference on the shape.
- 主要語言
- Python
- 星號
- 103
- 分支
- 50
- 平均合併
- 5 天 12 小時
- 30 天內合併 PR
- 4
環境準備
這個專案沒有提供開發容器、Dockerfile 或貢獻指南,環境需要你自己搭建:先看它的 README,通用步驟見我們的新手貢獻指南。
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
pytest-dev/execnet 的其他 Issue
-
難度 4/5 3-5 天 新手友好度 45/100
pytest-dev/execnet#374 · 3 則留言 ·
-
難度 3/5 1-2 天 新手友好度 45/100
pytest-dev/execnet#306 · 1 則留言 ·
-
難度 4/5 3-5 天 新手友好度 35/100
pytest-dev/execnet#279 · 1 個 reaction ·
-
Exception ignored in atexit callback / RuntimeError: can't create new thread at interpreter shutdown可能重新可做 @RonnyPfannschmidt 於 910 天前認領,目前沒有進行中的 PR。 未關閉bug
pytest-dev/execnet#271 · 4 則留言 · 已指派 1 人 ·
-
難度 5/5 一週以上 新手友好度 10/100
pytest-dev/execnet#205 ·
查看 pytest-dev/execnet 的全部 Issue
相似的 Issue
-
難度 2/5 1-3 小時 新手友好度 68/100
KalicoCrew/kalico#1012 ·
維護者通常 1 天內回覆
-
難度 1/5 1 小時以內 新手友好度 82/100
esphome/esphome.io#7657 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 62/100
VNG-Realisatie/gemma-zaken#2658 ·
-
難度 1/5 1 小時以內 新手友好度 75/100
維護者通常 1 天內回覆
-
comp/gateway comp/plugins duplicate P3 type/bug
難度 2/5 1-3 小時 新手友好度 82/100
NousResearch/hermes-agent#137019 ·
維護者通常 1 天內回覆