From 7a3755ac55d31a3453325960626ac03d8cae9020 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Mon, 20 Jul 2026 10:31:00 +0800 Subject: [PATCH] fix(dev-server): limit reload watcher to source --- .../bugs/uvicorn-reload-log-feedback-loop.md | 14 +++++ server/server_start.sh | 2 +- .../tests/test_server_start_dependencies.py | 56 ++++++++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md diff --git a/document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md b/document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md new file mode 100644 index 0000000..0250eac --- /dev/null +++ b/document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md @@ -0,0 +1,14 @@ +# Uvicorn 热重载监听日志形成反馈循环 + +日期:2026-07-18 +文档路径:document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md + +## 修复记录 + +- 15:54:记录 bug 修复:开发模式的 Uvicorn 热重载监听整个服务目录,应用写入日志会再次触发 reload 检测日志,形成持续反馈循环。 + - Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。 + - 原因:`--reload` 未限定源码目录,`server/logs/app.log` 的每次追加都被 watchfiles 识别为变更,并产生新的日志。 + - 修改:热重载分支增加 `--reload-dir src`,只监听后端源码;非热重载与多 worker 启动参数保持不变;新增两种启动分支的脚本回归测试。 + - 操作:在容器内执行 Shell 语法检查、启动依赖测试、后端联合回归、Ruff 和差异格式检查;为避免自动执行待确认的数据库迁移,本轮未重启主容器。 + - 验证:脚本语法通过,启动脚本定向测试通过,后端联合回归 `165 passed`,Ruff 与 `git diff --check` 通过;代码将在下一次经确认的安全重建后生效。 + - 影响:生效后日志文件写入不再触发热重载扫描,可停止无效日志增长和持续 CPU/IO 消耗。 diff --git a/server/server_start.sh b/server/server_start.sh index aba3610..bf818b0 100755 --- a/server/server_start.sh +++ b/server/server_start.sh @@ -433,7 +433,7 @@ start_server() { echo "" if [ "$SERVER_RELOAD" = "true" ]; then - exec "$PYTHON_BIN" -m uvicorn app.main:app --reload --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" + exec "$PYTHON_BIN" -m uvicorn app.main:app --reload --reload-dir src --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" fi if [ "$SERVER_WORKERS" -gt 1 ] 2>/dev/null; then diff --git a/server/tests/test_server_start_dependencies.py b/server/tests/test_server_start_dependencies.py index 6a201b0..eb6261c 100644 --- a/server/tests/test_server_start_dependencies.py +++ b/server/tests/test_server_start_dependencies.py @@ -1,9 +1,43 @@ from __future__ import annotations -from pathlib import Path import os import stat import subprocess +from pathlib import Path + + +def _run_start_server( + tmp_path: Path, + *, + reload_enabled: bool, +) -> subprocess.CompletedProcess[str]: + fake_python = tmp_path / "fake-python.sh" + fake_python.write_text( + """#!/usr/bin/env sh +printf 'ARG:%s\n' "$@" +""", + encoding="utf-8", + ) + fake_python.chmod(fake_python.stat().st_mode | stat.S_IEXEC) + + script_path = Path(__file__).resolve().parents[1] / "server_start.sh" + script_prefix = script_path.read_text(encoding="utf-8").split('case "$MODE" in', 1)[0] + command = f"""{script_prefix} +PYTHON_BIN="{fake_python}" +SERVER_HOST="127.0.0.1" +SERVER_PORT="8765" +SERVER_RELOAD="{'true' if reload_enabled else 'false'}" +SERVER_WORKERS="1" +start_server +""" + return subprocess.run( + ["bash", "-c", command], + capture_output=True, + text=True, + env={**os.environ, "MODE": "test"}, + cwd=script_path.parent, + check=False, + ) def test_dependencies_ready_fails_when_jwt_is_missing(tmp_path: Path) -> None: @@ -38,3 +72,23 @@ dependencies_ready ) assert result.returncode != 0 + + +def test_reload_watches_only_server_source_directory(tmp_path: Path) -> None: + result = _run_start_server(tmp_path, reload_enabled=True) + + assert result.returncode == 0, result.stderr + assert "ARG:--reload" in result.stdout + assert "ARG:--reload-dir\nARG:src" in result.stdout + assert "ARG:--app-dir\nARG:src" in result.stdout + + +def test_non_reload_server_command_remains_unchanged(tmp_path: Path) -> None: + result = _run_start_server(tmp_path, reload_enabled=False) + + assert result.returncode == 0, result.stderr + assert "ARG:app.main:app" in result.stdout + assert "ARG:--host\nARG:127.0.0.1" in result.stdout + assert "ARG:--port\nARG:8765" in result.stdout + assert "ARG:--reload" not in result.stdout + assert "ARG:--reload-dir" not in result.stdout