148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""
|
||||
|
|
父进程死亡防护(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("✅ 全部通过")
|