mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix(serve): import app object before uvicorn startup probe (#1760)
main.py --serve-only passes the app to uvicorn as the import string "api.app:app", so uvicorn imports the app lazily inside the server thread. That import (litellm + the full app tree, ~10s+ on constrained hosts) runs inside the 3.0s startup self-check window, so the probe times out and --serve-only returns 1, causing a container restart loop on slower machines (e.g. a 2 vCPU VPS). Faster hosts finish the import under 3s and pass, which is why the issue was spec-dependent and long unreported. Import the ASGI app object in the calling thread before the probe and hand the object to uvicorn, so the heavy import stays out of the probe window. Genuine import failures still surface immediately. Update the start_api_server unit tests to stub api.app the same way they already stub uvicorn. Fixes #1759 Co-authored-by: gang.wu@ximalaya.com <gang.wu@ximalaya.com> Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [修复] 避免 runtime scheduler 重建定时任务时重复立即运行事件监控,减少重复告警和后台任务状态丢失。
|
||||
- [修复] Web/API runtime scheduler 接管 `--serve --schedule` 后保留 `--dry-run`、`--no-notify` 等启动参数语义。
|
||||
- [改进] Web 历史报告详情不再内嵌展示 AI 建议卡片,结构化决策信号集中在 AI 建议页查询,并保留按来源报告 ID 筛选或 URL 参数精确定位入口。
|
||||
- [修复] `main.py --serve-only` 在低配主机上因 uvicorn 在 3.0s 启动自检窗口内才惰性 import 应用(litellm + 整个 app 树)导致超时退出、容器反复重启;改为在计时前于调用线程预先 import app 对象再交给 uvicorn,启动自检不再误杀慢启动。
|
||||
- [修复] Docker 镜像预置 efinance 缓存目录(efinance/data)属主给非 root 运行用户 dsa,修复 A 股 efinance 数据源因写 search-cache.json 触发 PermissionError 而每次抓取失败降级的问题。
|
||||
- [修复] Docker 部署中 Web 设置页保存自定义 Webhook 模板时自动转义 `$content_json` 等应用占位符,并在运行时还原,避免 Compose 重新部署将其展开为空。
|
||||
|
||||
|
||||
13
main.py
13
main.py
@@ -1033,9 +1033,18 @@ def start_api_server(host: str, port: int, config: Config) -> None:
|
||||
"log_level": level_name,
|
||||
"log_config": None,
|
||||
}
|
||||
# Import the ASGI app object in the calling thread instead of handing uvicorn
|
||||
# the "api.app:app" import string. With the string, uvicorn imports the app
|
||||
# lazily inside the server thread, and that import (litellm + the full app
|
||||
# tree, ~10s+ on constrained hosts) runs inside the startup probe window
|
||||
# below, tripping the 3.0s timeout and causing a restart loop on slower
|
||||
# machines. Importing first keeps the heavy work out of the probe window;
|
||||
# genuine import failures still surface immediately to the caller.
|
||||
from api.app import app as fastapi_app
|
||||
|
||||
try:
|
||||
uvicorn_config = uvicorn.Config(
|
||||
"api.app:app",
|
||||
fastapi_app,
|
||||
install_signal_handlers=False,
|
||||
**uvicorn_kwargs,
|
||||
)
|
||||
@@ -1045,7 +1054,7 @@ def start_api_server(host: str, port: int, config: Config) -> None:
|
||||
# when it's a boolean flag.
|
||||
use_config_signal_handlers = False
|
||||
uvicorn_config = uvicorn.Config(
|
||||
"api.app:app",
|
||||
fastapi_app,
|
||||
**uvicorn_kwargs,
|
||||
)
|
||||
uvicorn_server = uvicorn.Server(config=uvicorn_config)
|
||||
|
||||
@@ -28,6 +28,23 @@ _MAIN_IMPORT_ENV_OVERRIDES = {
|
||||
}
|
||||
|
||||
|
||||
def _api_app_stub_modules():
|
||||
"""sys.modules entries so ``start_api_server`` can ``from api.app import app``
|
||||
without importing the real (heavy) app tree in these isolated unit tests.
|
||||
|
||||
``start_api_server`` imports the ASGI app object in the calling thread so the
|
||||
import stays out of the uvicorn startup probe window; these control-flow tests
|
||||
stub it the same way they already stub uvicorn.
|
||||
"""
|
||||
import types
|
||||
|
||||
api_pkg = types.ModuleType("api")
|
||||
api_app_mod = types.ModuleType("api.app")
|
||||
api_app_mod.app = SimpleNamespace()
|
||||
api_pkg.app = api_app_mod
|
||||
return {"api": api_pkg, "api.app": api_app_mod}
|
||||
|
||||
|
||||
class _DummyConfig(SimpleNamespace):
|
||||
def validate(self):
|
||||
return []
|
||||
@@ -174,7 +191,10 @@ class MainScheduleModeTestCase(unittest.TestCase):
|
||||
pass
|
||||
|
||||
with patch("socket.socket", return_value=_UnusedSocket()), \
|
||||
patch.dict("sys.modules", {"uvicorn": _FakeUvicornModule()}):
|
||||
patch.dict(
|
||||
"sys.modules",
|
||||
{"uvicorn": _FakeUvicornModule(), **_api_app_stub_modules()},
|
||||
):
|
||||
|
||||
with self.assertRaises(RuntimeError) as caught:
|
||||
main.start_api_server("127.0.0.1", 8000, config)
|
||||
@@ -212,7 +232,13 @@ class MainScheduleModeTestCase(unittest.TestCase):
|
||||
pass
|
||||
|
||||
with patch("socket.socket", return_value=_UnusedSocket()), \
|
||||
patch.dict("sys.modules", {"uvicorn": SimpleNamespace(Config=_CompatConfig, Server=_CompatServer)}):
|
||||
patch.dict(
|
||||
"sys.modules",
|
||||
{
|
||||
"uvicorn": SimpleNamespace(Config=_CompatConfig, Server=_CompatServer),
|
||||
**_api_app_stub_modules(),
|
||||
},
|
||||
):
|
||||
main.start_api_server("127.0.0.1", 8000, config)
|
||||
|
||||
self.assertIsNotNone(_CompatServer.instance)
|
||||
|
||||
Reference in New Issue
Block a user