Files
chongming-core/test/test_parent_death.py
T

148 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
父进程死亡防护(PR_SET_PDEATHSIG)测试
背景与语义见 gateway/src/chongming/app/core/parent_death.py:进程树 onefile 引导 →
granian master → worker(fork)。上游被 SIGKILL 时 worker 会成孤儿(继续监听
端口、挂在服务发现里,其它网关派发到"半死副本"每次白吃 2 秒超时)。守卫让内核
在父进程死亡时给本进程发 SIGTERM(走既有优雅停机摘除注册)。
验证(三层 fork 复刻真实进程树:本测试(引导) → 中间进程(master) → 孙进程(worker)):
1. 关闭开关 CHONGMING_NO_PARENT_GUARD=1 → 不生效(返回 False)
2. 中间进程 SIGKILL 自己(模拟 master 被 -9,内核不可捕获)→ 已设守卫的孙进程
收到 SIGTERM 并退出
3. 对照:同样结构但不设守卫 → 孙进程收不到信号(证明信号确实来自 PDEATHSIG,
而不是别的东西清理孤儿)
4. 幂等:重复调用不重复 prctl
运行: uv run python test/test_parent_death.py
"""
import importlib.util
import os
import signal
import sys
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).parent.parent
_PD_PATH = ROOT / "gateway" / "src" / "chongming" / "app" / "core" / "parent_death.py"
# 独立加载(parent_death 无包内依赖,避免连带导入整个 app 包)
_spec = importlib.util.spec_from_file_location("chongming.app.core.parent_death", _PD_PATH)
pd = importlib.util.module_from_spec(_spec)
sys.modules["chongming.app.core.parent_death"] = pd
_spec.loader.exec_module(pd) # type: ignore[union-attr]
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
# ========== 1. 关闭开关 ==========
os.environ[pd.GUARD_OFF_ENV] = "1"
check("关闭开关打开时不生效", pd.guard_parent_death() is False)
os.environ.pop(pd.GUARD_OFF_ENV, None)
# ========== 2/3. 三层 fork:守卫组 / 对照组 ==========
tmp = Path(tempfile.mkdtemp(prefix="parent_death_test_"))
def scenario(name: str, use_guard: bool) -> dict:
"""中间进程 SIGKILL 自己,观察孙进程是否收到 SIGTERM。"""
d = tmp / name
d.mkdir()
ready, dead, pidf = d / "ready", d / "dead", d / "gc.pid"
middle = os.fork()
if middle == 0: # 中间进程(master 角色)
gc = os.fork()
if gc == 0: # 孙进程(worker 角色)
def _on_term(*_):
dead.write_text("term", encoding="utf-8")
os._exit(0)
signal.signal(signal.SIGTERM, _on_term)
if use_guard:
pd.guard_parent_death()
pidf.write_text(str(os.getpid()), encoding="utf-8")
ready.write_text("1", encoding="utf-8")
time.sleep(15) # 等信号;超时说明没收到
os._exit(1)
for _ in range(250): # 等孙进程就绪,再硬杀自己
if ready.exists():
break
time.sleep(0.02)
os.kill(os.getpid(), signal.SIGKILL)
os._exit(1) # 不会走到
os.waitpid(middle, 0) # 中间进程必死于 SIGKILL
gc_pid = int(pidf.read_text(encoding="utf-8"))
for _ in range(100): # 最多等 5s
if dead.exists():
break
time.sleep(0.05)
got = dead.exists()
try: # 清理(对照组孙进程仍活着)
os.kill(gc_pid, signal.SIGKILL)
except ProcessLookupError:
pass
return {"got_term": got, "gc_pid": gc_pid}
guarded = scenario("guarded", use_guard=True)
check(
"master(SIGKILL) 死后,已设守卫的 worker 收到 SIGTERM 退出",
guarded["got_term"],
f"gc={guarded['gc_pid']}",
)
control = scenario("control", use_guard=False)
check(
"对照组(未设守卫)收不到信号 —— 证明退出确实来自 PDEATHSIG",
not control["got_term"],
f"gc={control['gc_pid']}",
)
# ========== 4. 幂等(在子进程里做:避免给测试进程本身设 PDEATHSIG) ==========
pid = os.fork()
if pid == 0:
a = pd.guard_parent_death()
b = pd.guard_parent_death()
os._exit(0 if (a is True and b is True) else 1)
_, status = os.waitpid(pid, 0)
check("重复调用幂等(不重复 prctl)", os.waitstatus_to_exitcode(status) == 0)
# ========== 5. fork 继承回归(master 先设 → worker 仍必须自己设上) ==========
# 背景:若用模块级"已设置"标志位做幂等,fork 的子进程会继承 True 而短路——
# 但内核在 fork 时恰好清零 PDEATHSIG,于是 worker 其实没设上(本轮实测到过)。
# 这里父进程先真实设置,再 fork,断言子进程调用守卫时**内核状态确实被设上**。
libc5 = pd._load_libc()
pd.guard_parent_death() # 父进程(本测试进程)真实设置一次
pid = os.fork()
if pid == 0:
cleared = pd._current_pdeathsig(libc5) == 0 # fork 后内核已清零(不继承)
ok = pd.guard_parent_death() is True # 回归点:不能因父进程设过而短路
set_now = pd._current_pdeathsig(libc5) == int(signal.SIGTERM)
os._exit(0 if (cleared and ok and set_now) else 1)
_, status = os.waitpid(pid, 0)
libc5.prctl(pd._PR_SET_PDEATHSIG, 0) # 清除测试进程自己的设置,避免副作用
check(
"父进程设过后,fork 的子进程仍会真实设置 PDEATHSIG",
os.waitstatus_to_exitcode(status) == 0,
)
import shutil
shutil.rmtree(tmp, ignore_errors=True)
print("\n" + ("=" * 40))
if failures:
print(f"❌ {len(failures)} 项未通过: {failures}")
sys.exit(1)
print("✅ 全部通过")