mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
refactor: use factory for supervised uvicorn modes
This commit is contained in:
+53
-6
@@ -30,6 +30,7 @@ import setproctitle
|
||||
import signal
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import uvicorn as uvicorn
|
||||
from PIL import Image
|
||||
@@ -55,7 +56,10 @@ elif SystemUtils.is_frozen():
|
||||
|
||||
from app.factory import app
|
||||
from app.runtime.config import global_vars, settings
|
||||
from app.runtime.topology import validate_process_topology
|
||||
from app.runtime.topology import (
|
||||
UnsupportedProcessTopologyError,
|
||||
validate_process_topology,
|
||||
)
|
||||
from app.startup.database_initializer import prepare_database
|
||||
|
||||
setproctitle.setproctitle(settings.PROJECT_NAME)
|
||||
@@ -69,15 +73,58 @@ class MoviePilotServer(uvicorn.Server):
|
||||
super().handle_exit(sig, frame)
|
||||
|
||||
|
||||
Server = MoviePilotServer(Config(app, host=settings.HOST, port=settings.PORT,
|
||||
reload=settings.DEV, workers=settings.API_WORKERS,
|
||||
timeout_graceful_shutdown=60))
|
||||
APP_FACTORY = "app.factory:create_app"
|
||||
Server: Optional[MoviePilotServer] = None
|
||||
|
||||
|
||||
def create_server() -> MoviePilotServer:
|
||||
"""创建不带 reload/multiprocess supervisor 的单进程生产服务器。"""
|
||||
server = MoviePilotServer(
|
||||
Config(
|
||||
app,
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=False,
|
||||
workers=1,
|
||||
timeout_graceful_shutdown=60,
|
||||
)
|
||||
)
|
||||
# 数据库准备阶段收到的信号早于 Server 物化,创建后必须继承既有停止意图。
|
||||
if global_vars.is_system_stopped:
|
||||
server.should_exit = True
|
||||
return server
|
||||
|
||||
|
||||
def run_api_server() -> None:
|
||||
"""按开发 reload、安全模式多进程或生产单进程选择 Uvicorn 入口。"""
|
||||
global Server
|
||||
supervised = settings.DEV or settings.API_WORKERS > 1
|
||||
if supervised:
|
||||
if settings.DEV and settings.API_WORKERS > 1:
|
||||
raise UnsupportedProcessTopologyError(
|
||||
"Uvicorn reload 与多 worker 不能同时启用;"
|
||||
"开发模式请设置 API_WORKERS=1。"
|
||||
)
|
||||
Server = None
|
||||
uvicorn.run(
|
||||
APP_FACTORY,
|
||||
factory=True,
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.DEV,
|
||||
workers=settings.API_WORKERS,
|
||||
timeout_graceful_shutdown=60,
|
||||
)
|
||||
return
|
||||
Server = create_server()
|
||||
Server.run()
|
||||
|
||||
|
||||
def request_shutdown() -> None:
|
||||
"""发布协作停止标志并请求 Uvicorn 退出"""
|
||||
global_vars.stop_system()
|
||||
Server.should_exit = True
|
||||
if Server is not None:
|
||||
Server.should_exit = True
|
||||
|
||||
|
||||
def start_tray():
|
||||
@@ -143,7 +190,7 @@ def run_application() -> None:
|
||||
|
||||
start_tray()
|
||||
prepare_database()
|
||||
Server.run()
|
||||
run_api_server()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -235,6 +235,9 @@ sequenceDiagram
|
||||
|
||||
- **缓存装配先于业务导入**:缓存装饰器会在业务模块 import 时创建后端,
|
||||
因此 `configure_cache_dependencies()` 在 `lifecycle.py` 顶部即执行。
|
||||
- **Uvicorn 入口分流**:生产单 worker 使用带协作停止语义的 `MoviePilotServer`;开发 reload
|
||||
和安全模式多 worker 使用 `app.factory:create_app` import string/factory,由 supervisor
|
||||
创建应用实例。`app.factory:app` 继续保留给既有 ASGI supervisor 和测试使用。
|
||||
- **引擎预热 fail-fast**:同步/异步数据库引擎在单线程期完成首次创建,
|
||||
避免调度器放出大量线程后再创建引擎导致连接锁竞争。
|
||||
- **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
|
||||
|
||||
@@ -61,7 +61,10 @@ uv sync --locked --no-dev --no-install-project
|
||||
./scripts/start-local.sh logs --follow
|
||||
```
|
||||
|
||||
默认会使用 `DEBUG=true` 和 `DEV=true`,与 IDE 开发启动保持一致;如果不需要热重载,可以这样启动以降低资源占用:
|
||||
默认会使用 `DEBUG=true` 和 `DEV=true`,与 IDE 开发启动保持一致。开发热重载通过
|
||||
`app.factory:create_app` 的 import string/factory 入口运行,文件变化后由 Uvicorn 重新创建
|
||||
应用结构;不会尝试在 reload 进程间传递已经实例化的 FastAPI 对象。如果不需要热重载,
|
||||
可以这样启动以降低资源占用:
|
||||
|
||||
```bash
|
||||
DEV=false ./scripts/start-local.sh
|
||||
@@ -192,8 +195,9 @@ Safety 直接识别项目清单和锁文件,不需要生成或维护 requireme
|
||||
uv run --locked --no-sync pylint app/
|
||||
```
|
||||
|
||||
GitHub Actions 会在 `v3` 的 PR/push 中独立执行宿主架构与 Pylint 门禁;最新官方插件仓
|
||||
通过每周或手工观察工作流检查,只上传语义差异报告,不会自动更新已提交基线。
|
||||
GitHub Actions 会在 `v3` 的 PR/push 中独立执行宿主架构门禁,并对本次改动的 Python
|
||||
文件执行 Pylint 硬门禁;`app/` 全量结果作为建议性报告上传。最新官方插件仓通过每周
|
||||
或手工观察工作流检查,只上传语义差异报告,不会自动更新已提交基线。
|
||||
|
||||
### 7. 参考资源
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
|
||||
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
|
||||
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
|
||||
> 实施进度:阶段 0(ARCH-201~203)与 ARCH-210 已完成,后续任务按 ID 独立提交和回滚
|
||||
> 实施进度:阶段 0(ARCH-201~203)与 ARCH-210~211 已完成,后续任务按 ID 独立提交和回滚
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
|
||||
+130
-124
@@ -1,41 +1,41 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-08-17T15:05:56.902649+00:00",
|
||||
"generated_at": "2026-08-21T11:45:07.622632+00:00",
|
||||
"platform": "macOS-26.5.2-arm64-arm-64bit",
|
||||
"python": "3.12.6",
|
||||
"repeat": 3,
|
||||
"targets": {
|
||||
"app.startup.lifecycle": {
|
||||
"loaded_module_count": 1779,
|
||||
"max_ms": 968.467,
|
||||
"median_ms": 968.335,
|
||||
"min_ms": 960.971,
|
||||
"loaded_module_count": 1809,
|
||||
"max_ms": 1023.29,
|
||||
"median_ms": 1018.356,
|
||||
"min_ms": 992.011,
|
||||
"samples_ms": [
|
||||
968.335,
|
||||
968.467,
|
||||
960.971
|
||||
1023.29,
|
||||
1018.356,
|
||||
992.011
|
||||
]
|
||||
},
|
||||
"app.factory": {
|
||||
"loaded_module_count": 1791,
|
||||
"max_ms": 1026.357,
|
||||
"median_ms": 1019.56,
|
||||
"min_ms": 997.442,
|
||||
"loaded_module_count": 1818,
|
||||
"max_ms": 1058.352,
|
||||
"median_ms": 1048.27,
|
||||
"min_ms": 1045.441,
|
||||
"samples_ms": [
|
||||
1019.56,
|
||||
1026.357,
|
||||
997.442
|
||||
1058.352,
|
||||
1048.27,
|
||||
1045.441
|
||||
]
|
||||
},
|
||||
"app.main": {
|
||||
"loaded_module_count": 1933,
|
||||
"max_ms": 1091.202,
|
||||
"median_ms": 1083.385,
|
||||
"min_ms": 1076.762,
|
||||
"loaded_module_count": 1961,
|
||||
"max_ms": 1316.051,
|
||||
"median_ms": 1144.056,
|
||||
"min_ms": 1138.523,
|
||||
"samples_ms": [
|
||||
1091.202,
|
||||
1076.762,
|
||||
1083.385
|
||||
1138.523,
|
||||
1316.051,
|
||||
1144.056
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -47,78 +47,81 @@
|
||||
{
|
||||
"mode": "normal",
|
||||
"enabled_component_count": 14,
|
||||
"startup_ms": 0.55,
|
||||
"full_lifespan_ms": 0.621,
|
||||
"startup_ms": 0.566,
|
||||
"full_lifespan_ms": 0.67,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.071,
|
||||
"领域依赖装配": 0.035,
|
||||
"HTTP 基础能力": 0.077,
|
||||
"领域依赖装配": 0.038,
|
||||
"数据库引擎预热": 0.028,
|
||||
"数据库连接预算": 0.025,
|
||||
"路由": 0.022,
|
||||
"模块服务": 0.022,
|
||||
"插件备份恢复": 0.022,
|
||||
"插件": 0.021,
|
||||
"定时器": 0.022,
|
||||
"监控器": 0.022,
|
||||
"待处理整理回放": 0.023,
|
||||
"命令服务": 0.022,
|
||||
"工作流": 0.022,
|
||||
"插件同步与启动收尾": 0.035
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "normal",
|
||||
"enabled_component_count": 14,
|
||||
"startup_ms": 0.574,
|
||||
"full_lifespan_ms": 0.669,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.08,
|
||||
"领域依赖装配": 0.036,
|
||||
"数据库引擎预热": 0.029,
|
||||
"数据库连接预算": 0.026,
|
||||
"路由": 0.023,
|
||||
"模块服务": 0.021,
|
||||
"插件备份恢复": 0.021,
|
||||
"数据库连接预算": 0.027,
|
||||
"路由": 0.027,
|
||||
"模块服务": 0.023,
|
||||
"插件备份恢复": 0.022,
|
||||
"插件": 0.02,
|
||||
"定时器": 0.02,
|
||||
"监控器": 0.023,
|
||||
"待处理整理回放": 0.025,
|
||||
"命令服务": 0.023,
|
||||
"工作流": 0.023,
|
||||
"插件同步与启动收尾": 0.03
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "normal",
|
||||
"enabled_component_count": 14,
|
||||
"startup_ms": 0.571,
|
||||
"full_lifespan_ms": 0.678,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.077,
|
||||
"领域依赖装配": 0.036,
|
||||
"数据库引擎预热": 0.028,
|
||||
"数据库连接预算": 0.029,
|
||||
"路由": 0.025,
|
||||
"模块服务": 0.022,
|
||||
"插件备份恢复": 0.022,
|
||||
"插件": 0.023,
|
||||
"定时器": 0.023,
|
||||
"监控器": 0.022,
|
||||
"待处理整理回放": 0.024,
|
||||
"待处理整理回放": 0.021,
|
||||
"命令服务": 0.023,
|
||||
"工作流": 0.022
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "normal",
|
||||
"enabled_component_count": 14,
|
||||
"startup_ms": 0.548,
|
||||
"full_lifespan_ms": 0.613,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.075,
|
||||
"领域依赖装配": 0.035,
|
||||
"数据库引擎预热": 0.028,
|
||||
"数据库连接预算": 0.028,
|
||||
"路由": 0.026,
|
||||
"模块服务": 0.022,
|
||||
"插件备份恢复": 0.023,
|
||||
"插件": 0.023,
|
||||
"定时器": 0.021,
|
||||
"监控器": 0.019,
|
||||
"待处理整理回放": 0.024,
|
||||
"命令服务": 0.023,
|
||||
"工作流": 0.022
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "normal",
|
||||
"enabled_component_count": 14,
|
||||
"startup_ms": 0.549,
|
||||
"full_lifespan_ms": 0.616,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.07,
|
||||
"领域依赖装配": 0.034,
|
||||
"数据库引擎预热": 0.027,
|
||||
"数据库连接预算": 0.028,
|
||||
"路由": 0.026,
|
||||
"模块服务": 0.023,
|
||||
"插件备份恢复": 0.024,
|
||||
"插件": 0.023,
|
||||
"定时器": 0.021,
|
||||
"监控器": 0.019,
|
||||
"待处理整理回放": 0.024,
|
||||
"命令服务": 0.023,
|
||||
"工作流": 0.022
|
||||
"工作流": 0.022,
|
||||
"插件同步与启动收尾": 0.036
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
@@ -129,8 +132,8 @@
|
||||
"database_connections_started": 0
|
||||
}
|
||||
],
|
||||
"median_startup_ms": 0.549,
|
||||
"median_full_lifespan_ms": 0.616,
|
||||
"median_startup_ms": 0.571,
|
||||
"median_full_lifespan_ms": 0.67,
|
||||
"enabled_component_count": 14
|
||||
},
|
||||
"safe": {
|
||||
@@ -138,15 +141,38 @@
|
||||
{
|
||||
"mode": "safe",
|
||||
"enabled_component_count": 6,
|
||||
"startup_ms": 0.399,
|
||||
"full_lifespan_ms": 0.46,
|
||||
"startup_ms": 0.419,
|
||||
"full_lifespan_ms": 0.523,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.069,
|
||||
"领域依赖装配": 0.035,
|
||||
"数据库引擎预热": 0.027,
|
||||
"HTTP 基础能力": 0.082,
|
||||
"领域依赖装配": 0.04,
|
||||
"数据库引擎预热": 0.031,
|
||||
"数据库连接预算": 0.029,
|
||||
"路由": 0.025,
|
||||
"模块服务": 0.022,
|
||||
"插件同步与启动收尾": 0.037
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "safe",
|
||||
"enabled_component_count": 6,
|
||||
"startup_ms": 0.488,
|
||||
"full_lifespan_ms": 0.582,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.078,
|
||||
"领域依赖装配": 0.036,
|
||||
"数据库引擎预热": 0.03,
|
||||
"数据库连接预算": 0.027,
|
||||
"路由": 0.024,
|
||||
"模块服务": 0.026
|
||||
"模块服务": 0.022,
|
||||
"插件同步与启动收尾": 0.032
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
@@ -159,36 +185,16 @@
|
||||
{
|
||||
"mode": "safe",
|
||||
"enabled_component_count": 6,
|
||||
"startup_ms": 0.382,
|
||||
"full_lifespan_ms": 0.443,
|
||||
"startup_ms": 0.41,
|
||||
"full_lifespan_ms": 0.538,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.065,
|
||||
"领域依赖装配": 0.031,
|
||||
"数据库引擎预热": 0.025,
|
||||
"数据库连接预算": 0.025,
|
||||
"路由": 0.022,
|
||||
"模块服务": 0.023
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
"threads_after": 2,
|
||||
"tasks_before": 1,
|
||||
"tasks_started": 2,
|
||||
"tasks_after": 1,
|
||||
"database_connections_started": 0
|
||||
},
|
||||
{
|
||||
"mode": "safe",
|
||||
"enabled_component_count": 6,
|
||||
"startup_ms": 0.416,
|
||||
"full_lifespan_ms": 0.482,
|
||||
"stage_ms": {
|
||||
"HTTP 基础能力": 0.074,
|
||||
"HTTP 基础能力": 0.076,
|
||||
"领域依赖装配": 0.034,
|
||||
"数据库引擎预热": 0.028,
|
||||
"数据库连接预算": 0.027,
|
||||
"数据库连接预算": 0.026,
|
||||
"路由": 0.025,
|
||||
"模块服务": 0.025
|
||||
"模块服务": 0.024,
|
||||
"插件同步与启动收尾": 0.06
|
||||
},
|
||||
"threads_before": 2,
|
||||
"threads_started": 2,
|
||||
@@ -199,8 +205,8 @@
|
||||
"database_connections_started": 0
|
||||
}
|
||||
],
|
||||
"median_startup_ms": 0.399,
|
||||
"median_full_lifespan_ms": 0.46,
|
||||
"median_startup_ms": 0.419,
|
||||
"median_full_lifespan_ms": 0.538,
|
||||
"enabled_component_count": 6
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ def test_application_preserves_stop_requested_before_startup(monkeypatch):
|
||||
"prepare_database",
|
||||
lambda: calls.append("prepare_database"),
|
||||
)
|
||||
monkeypatch.setattr(main.Server, "run", lambda: calls.append("server"))
|
||||
monkeypatch.setattr(main, "run_api_server", lambda: calls.append("server"))
|
||||
|
||||
main.run_application()
|
||||
|
||||
@@ -469,7 +469,7 @@ def test_application_does_not_start_server_after_migration_failure(monkeypatch):
|
||||
"prepare_database",
|
||||
MagicMock(side_effect=migration_error),
|
||||
)
|
||||
monkeypatch.setattr(main.Server, "run", server_run)
|
||||
monkeypatch.setattr(main, "run_api_server", server_run)
|
||||
|
||||
with pytest.raises(RuntimeError) as raised:
|
||||
main.run_application()
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_main_rejects_topology_before_startup_side_effects(monkeypatch):
|
||||
monkeypatch.setattr(main.signal, "signal", signal_handler)
|
||||
monkeypatch.setattr(main, "start_tray", start_tray)
|
||||
monkeypatch.setattr(main, "prepare_database", prepare_database)
|
||||
monkeypatch.setattr(main.Server, "run", server_run)
|
||||
monkeypatch.setattr(main, "run_api_server", server_run)
|
||||
|
||||
with pytest.raises(UnsupportedProcessTopologyError):
|
||||
main.run_application()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""MoviePilot Uvicorn factory、reload 与生产服务器入口测试。"""
|
||||
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app import factory, main
|
||||
from app.runtime.topology import UnsupportedProcessTopologyError
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[1]
|
||||
|
||||
|
||||
def test_create_app_does_not_start_plugin_manager_or_threads(monkeypatch):
|
||||
"""ASGI factory 只构建应用结构,不得在创建阶段物化插件运行时。"""
|
||||
plugin_manager = MagicMock(side_effect=AssertionError("plugin runtime started"))
|
||||
monkeypatch.setattr(factory, "PluginManager", plugin_manager)
|
||||
threads_before = threading.active_count()
|
||||
|
||||
created = factory.create_app()
|
||||
|
||||
assert created is not factory.app
|
||||
assert threading.active_count() == threads_before
|
||||
plugin_manager.assert_not_called()
|
||||
|
||||
|
||||
def test_production_entry_uses_custom_single_process_server(monkeypatch):
|
||||
"""生产单 worker 保留发布协作停止标志的自定义 Server。"""
|
||||
server = MagicMock()
|
||||
monkeypatch.setattr(main.settings, "DEV", False)
|
||||
monkeypatch.setattr(main.settings, "API_WORKERS", 1)
|
||||
monkeypatch.setattr(main, "create_server", MagicMock(return_value=server))
|
||||
uvicorn_run = MagicMock()
|
||||
monkeypatch.setattr(main.uvicorn, "run", uvicorn_run)
|
||||
|
||||
main.run_api_server()
|
||||
|
||||
assert main.Server is server
|
||||
server.run.assert_called_once_with()
|
||||
uvicorn_run.assert_not_called()
|
||||
|
||||
|
||||
def test_development_reload_uses_import_string_factory(monkeypatch):
|
||||
"""开发 reload 必须让 Uvicorn 重新导入 factory,而不是序列化 app 实例。"""
|
||||
monkeypatch.setattr(main.settings, "DEV", True)
|
||||
monkeypatch.setattr(main.settings, "API_WORKERS", 1)
|
||||
uvicorn_run = MagicMock()
|
||||
monkeypatch.setattr(main.uvicorn, "run", uvicorn_run)
|
||||
monkeypatch.setattr(main, "Server", MagicMock())
|
||||
|
||||
main.run_api_server()
|
||||
|
||||
assert main.Server is None
|
||||
uvicorn_run.assert_called_once_with(
|
||||
main.APP_FACTORY,
|
||||
factory=True,
|
||||
host=main.settings.HOST,
|
||||
port=main.settings.PORT,
|
||||
reload=True,
|
||||
workers=1,
|
||||
timeout_graceful_shutdown=60,
|
||||
)
|
||||
|
||||
|
||||
def test_safe_mode_multi_worker_uses_import_string_factory(monkeypatch):
|
||||
"""安全模式多 worker 由 Uvicorn supervisor 创建独立 ASGI factory 实例。"""
|
||||
monkeypatch.setattr(main.settings, "DEV", False)
|
||||
monkeypatch.setattr(main.settings, "MOVIEPILOT_SAFE_MODE", True)
|
||||
monkeypatch.setattr(main.settings, "API_WORKERS", 2)
|
||||
uvicorn_run = MagicMock()
|
||||
monkeypatch.setattr(main.uvicorn, "run", uvicorn_run)
|
||||
|
||||
main.run_api_server()
|
||||
|
||||
assert uvicorn_run.call_args.kwargs["factory"] is True
|
||||
assert uvicorn_run.call_args.kwargs["reload"] is False
|
||||
assert uvicorn_run.call_args.kwargs["workers"] == 2
|
||||
|
||||
|
||||
def test_reload_and_multiple_workers_are_rejected_together(monkeypatch):
|
||||
"""Uvicorn 不支持的 reload + workers 组合必须给出明确错误。"""
|
||||
monkeypatch.setattr(main.settings, "DEV", True)
|
||||
monkeypatch.setattr(main.settings, "MOVIEPILOT_SAFE_MODE", True)
|
||||
monkeypatch.setattr(main.settings, "API_WORKERS", 2)
|
||||
uvicorn_run = MagicMock()
|
||||
monkeypatch.setattr(main.uvicorn, "run", uvicorn_run)
|
||||
|
||||
with pytest.raises(UnsupportedProcessTopologyError, match="不能同时启用"):
|
||||
main.run_api_server()
|
||||
|
||||
uvicorn_run.assert_not_called()
|
||||
|
||||
|
||||
def test_request_shutdown_is_safe_before_server_creation(monkeypatch):
|
||||
"""数据库准备或 reload supervisor 阶段收到退出请求时不依赖 Server 已创建。"""
|
||||
stop_system = MagicMock()
|
||||
monkeypatch.setattr(main.global_vars, "stop_system", stop_system)
|
||||
monkeypatch.setattr(main, "Server", None)
|
||||
|
||||
main.request_shutdown()
|
||||
|
||||
stop_system.assert_called_once_with()
|
||||
|
||||
|
||||
def test_production_server_preserves_shutdown_requested_before_creation(
|
||||
monkeypatch,
|
||||
):
|
||||
"""数据库准备期间收到的停止请求必须传递给随后创建的生产 Server。"""
|
||||
stop_event = threading.Event()
|
||||
stop_event.set()
|
||||
monkeypatch.setattr(main.global_vars, "STOP_EVENT", stop_event)
|
||||
|
||||
server = main.create_server()
|
||||
|
||||
assert server.should_exit is True
|
||||
|
||||
|
||||
def test_local_launcher_keeps_module_entrypoint():
|
||||
"""本地开发脚本继续通过 app.main 进入统一启动准备流程。"""
|
||||
script = (PROJECT_ROOT / "scripts" / "start-local.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert 'exec "$VENV_PYTHON" -m app.main' in script
|
||||
Reference in New Issue
Block a user