Files
chongming-core/test/test_logging_fork.py
T

101 lines
4.0 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.
"""
fork 安全:文件日志流在子进程重开(reentrant call 修复)测试
背景:gateway 的日志是"队列 handler + 监听线程"结构,文件 handler 的流在父进程
打开。granian/gunicorn 的 worker 由 master **fork** 而来(实测 worker 与 master 的
fd 表完全一致)——子进程会继承父进程的 BufferedWriter,其内部锁/owner 状态在
fork 瞬间可能正被父进程监听线程占用(跨 fork 复制不可靠)。症状:冻结版(Nuitka)
里每个 worker 的**每条文件日志**都报
RuntimeError: reentrant call inside <_io.BufferedWriter ...>
(控制台 handler 正常,文件日志整体失效)。
修复:_after_fork_child 里把文件 handler 的流在子进程重开(新文件描述符)。
本测试验证:
1. fork 后子进程的 handler.stream 不再是继承的那个对象(结构保证)
2. 子进程能真正把记录写进日志文件(行为保证——旧实现下这里写不进去)
3. 控制台等非文件 handler 不被动(无 _open,跳过)
运行: uv run python test_logging_fork.py
"""
import importlib.util
import logging
import os
import sys
import tempfile
from pathlib import Path
tmp = tempfile.TemporaryDirectory()
log_dir = Path(tmp.name)
# 与 test_logging_async.py 相同的加载方式:独立加载 logger 模块并把日志目录重定向
_logger_path = (
Path(__file__).resolve().parent.parent / "gateway" / "src" / "chongming" / "app" / "core" / "logger.py"
)
_spec = importlib.util.spec_from_file_location("chongming.app.core.logger", _logger_path)
logger_mod = importlib.util.module_from_spec(_spec)
sys.modules["chongming.app.core.logger"] = logger_mod
_spec.loader.exec_module(logger_mod) # type: ignore[union-attr]
logger_mod.log_config["file"] = str(log_dir)
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)
check("os.register_at_fork 已注册 after_in_child", hasattr(os, "register_at_fork"))
logger = logging.getLogger("forktest")
logger_mod.setup_logging(logger, "forktest")
file_handler = next(
h for h in logger_mod._listener_handlers if isinstance(h, logger_mod.DailyRotatingFileHandler)
)
# 同步直写一条(不经队列),保持监听线程与流都活着,便于接下来 fork
parent_record = logging.LogRecord("forktest", logging.INFO, __file__, 0, "parent-line", (), None)
file_handler.handle(parent_record)
file_handler.flush()
check("父进程日志已落盘", "parent-line" in next(iter(log_dir.glob("forktest-*.log"))).read_text(encoding="utf-8"))
inherited_stream = file_handler.stream
check("父进程流已打开", inherited_stream is not None)
# ---- fork:子进程应换掉继承的流,并能独立写日志 ----
pid = os.fork()
if pid == 0: # 子进程
try:
reopened = file_handler.stream is not inherited_stream
record = logging.LogRecord(
"forktest", logging.INFO, __file__, 0, "child-line", (), None
)
# 直接同步走 handler(不经队列),写入结果立即可断言
file_handler.handle(record)
os._exit(0 if reopened else 3)
except BaseException: # noqa: BLE001 - 子进程异常用退出码传出
os._exit(4)
_, status = os.waitpid(pid, 0)
exit_code = os.waitstatus_to_exitcode(status)
check("fork 后子进程的流已换新(不再是继承对象)", exit_code == 0, f"exit={exit_code}")
check(
"子进程可直接写入日志文件(旧实现下 reentrant 失败)",
"child-line" in next(iter(log_dir.glob("forktest-*.log"))).read_text(encoding="utf-8"),
)
check(
"控制台 handler 不受影响(仍指向 stderr)",
any(isinstance(h, logging.StreamHandler) and not isinstance(h, logger_mod.DailyRotatingFileHandler) for h in logger_mod._listener_handlers),
)
tmp.cleanup()
print("\n" + ("=" * 40))
if failures:
print(f"❌ {len(failures)} 项未通过: {failures}")
sys.exit(1)
print("✅ 全部通过")