mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor: use factory for supervised uvicorn modes
This commit is contained in:
+52
-5
@@ -30,6 +30,7 @@ import setproctitle
|
|||||||
import signal
|
import signal
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import uvicorn as uvicorn
|
import uvicorn as uvicorn
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -55,7 +56,10 @@ elif SystemUtils.is_frozen():
|
|||||||
|
|
||||||
from app.factory import app
|
from app.factory import app
|
||||||
from app.runtime.config import global_vars, settings
|
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
|
from app.startup.database_initializer import prepare_database
|
||||||
|
|
||||||
setproctitle.setproctitle(settings.PROJECT_NAME)
|
setproctitle.setproctitle(settings.PROJECT_NAME)
|
||||||
@@ -69,14 +73,57 @@ class MoviePilotServer(uvicorn.Server):
|
|||||||
super().handle_exit(sig, frame)
|
super().handle_exit(sig, frame)
|
||||||
|
|
||||||
|
|
||||||
Server = MoviePilotServer(Config(app, host=settings.HOST, port=settings.PORT,
|
APP_FACTORY = "app.factory:create_app"
|
||||||
reload=settings.DEV, workers=settings.API_WORKERS,
|
Server: Optional[MoviePilotServer] = None
|
||||||
timeout_graceful_shutdown=60))
|
|
||||||
|
|
||||||
|
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:
|
def request_shutdown() -> None:
|
||||||
"""发布协作停止标志并请求 Uvicorn 退出"""
|
"""发布协作停止标志并请求 Uvicorn 退出"""
|
||||||
global_vars.stop_system()
|
global_vars.stop_system()
|
||||||
|
if Server is not None:
|
||||||
Server.should_exit = True
|
Server.should_exit = True
|
||||||
|
|
||||||
|
|
||||||
@@ -143,7 +190,7 @@ def run_application() -> None:
|
|||||||
|
|
||||||
start_tray()
|
start_tray()
|
||||||
prepare_database()
|
prepare_database()
|
||||||
Server.run()
|
run_api_server()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ sequenceDiagram
|
|||||||
|
|
||||||
- **缓存装配先于业务导入**:缓存装饰器会在业务模块 import 时创建后端,
|
- **缓存装配先于业务导入**:缓存装饰器会在业务模块 import 时创建后端,
|
||||||
因此 `configure_cache_dependencies()` 在 `lifecycle.py` 顶部即执行。
|
因此 `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**:同步/异步数据库引擎在单线程期完成首次创建,
|
- **引擎预热 fail-fast**:同步/异步数据库引擎在单线程期完成首次创建,
|
||||||
避免调度器放出大量线程后再创建引擎导致连接锁竞争。
|
避免调度器放出大量线程后再创建引擎导致连接锁竞争。
|
||||||
- **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
|
- **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ uv sync --locked --no-dev --no-install-project
|
|||||||
./scripts/start-local.sh logs --follow
|
./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
|
```bash
|
||||||
DEV=false ./scripts/start-local.sh
|
DEV=false ./scripts/start-local.sh
|
||||||
@@ -192,8 +195,9 @@ Safety 直接识别项目清单和锁文件,不需要生成或维护 requireme
|
|||||||
uv run --locked --no-sync pylint app/
|
uv run --locked --no-sync pylint app/
|
||||||
```
|
```
|
||||||
|
|
||||||
GitHub Actions 会在 `v3` 的 PR/push 中独立执行宿主架构与 Pylint 门禁;最新官方插件仓
|
GitHub Actions 会在 `v3` 的 PR/push 中独立执行宿主架构门禁,并对本次改动的 Python
|
||||||
通过每周或手工观察工作流检查,只上传语义差异报告,不会自动更新已提交基线。
|
文件执行 Pylint 硬门禁;`app/` 全量结果作为建议性报告上传。最新官方插件仓通过每周
|
||||||
|
或手工观察工作流检查,只上传语义差异报告,不会自动更新已提交基线。
|
||||||
|
|
||||||
### 7. 参考资源
|
### 7. 参考资源
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
|
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
|
||||||
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
|
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
|
||||||
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
|
> 相关文档:`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. 结论先行
|
## 1. 结论先行
|
||||||
|
|
||||||
|
|||||||
+130
-124
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"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",
|
"platform": "macOS-26.5.2-arm64-arm-64bit",
|
||||||
"python": "3.12.6",
|
"python": "3.12.6",
|
||||||
"repeat": 3,
|
"repeat": 3,
|
||||||
"targets": {
|
"targets": {
|
||||||
"app.startup.lifecycle": {
|
"app.startup.lifecycle": {
|
||||||
"loaded_module_count": 1779,
|
"loaded_module_count": 1809,
|
||||||
"max_ms": 968.467,
|
"max_ms": 1023.29,
|
||||||
"median_ms": 968.335,
|
"median_ms": 1018.356,
|
||||||
"min_ms": 960.971,
|
"min_ms": 992.011,
|
||||||
"samples_ms": [
|
"samples_ms": [
|
||||||
968.335,
|
1023.29,
|
||||||
968.467,
|
1018.356,
|
||||||
960.971
|
992.011
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"app.factory": {
|
"app.factory": {
|
||||||
"loaded_module_count": 1791,
|
"loaded_module_count": 1818,
|
||||||
"max_ms": 1026.357,
|
"max_ms": 1058.352,
|
||||||
"median_ms": 1019.56,
|
"median_ms": 1048.27,
|
||||||
"min_ms": 997.442,
|
"min_ms": 1045.441,
|
||||||
"samples_ms": [
|
"samples_ms": [
|
||||||
1019.56,
|
1058.352,
|
||||||
1026.357,
|
1048.27,
|
||||||
997.442
|
1045.441
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"app.main": {
|
"app.main": {
|
||||||
"loaded_module_count": 1933,
|
"loaded_module_count": 1961,
|
||||||
"max_ms": 1091.202,
|
"max_ms": 1316.051,
|
||||||
"median_ms": 1083.385,
|
"median_ms": 1144.056,
|
||||||
"min_ms": 1076.762,
|
"min_ms": 1138.523,
|
||||||
"samples_ms": [
|
"samples_ms": [
|
||||||
1091.202,
|
1138.523,
|
||||||
1076.762,
|
1316.051,
|
||||||
1083.385
|
1144.056
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,78 +47,81 @@
|
|||||||
{
|
{
|
||||||
"mode": "normal",
|
"mode": "normal",
|
||||||
"enabled_component_count": 14,
|
"enabled_component_count": 14,
|
||||||
"startup_ms": 0.55,
|
"startup_ms": 0.566,
|
||||||
"full_lifespan_ms": 0.621,
|
"full_lifespan_ms": 0.67,
|
||||||
"stage_ms": {
|
"stage_ms": {
|
||||||
"HTTP 基础能力": 0.071,
|
"HTTP 基础能力": 0.077,
|
||||||
"领域依赖装配": 0.035,
|
"领域依赖装配": 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.029,
|
||||||
"数据库连接预算": 0.026,
|
"数据库连接预算": 0.027,
|
||||||
"路由": 0.023,
|
"路由": 0.027,
|
||||||
"模块服务": 0.021,
|
"模块服务": 0.023,
|
||||||
"插件备份恢复": 0.021,
|
"插件备份恢复": 0.022,
|
||||||
"插件": 0.02,
|
"插件": 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.023,
|
||||||
"监控器": 0.022,
|
"监控器": 0.022,
|
||||||
"待处理整理回放": 0.024,
|
"待处理整理回放": 0.021,
|
||||||
"命令服务": 0.023,
|
"命令服务": 0.023,
|
||||||
"工作流": 0.022
|
"工作流": 0.022,
|
||||||
},
|
"插件同步与启动收尾": 0.036
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
"threads_before": 2,
|
"threads_before": 2,
|
||||||
"threads_started": 2,
|
"threads_started": 2,
|
||||||
@@ -129,8 +132,8 @@
|
|||||||
"database_connections_started": 0
|
"database_connections_started": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"median_startup_ms": 0.549,
|
"median_startup_ms": 0.571,
|
||||||
"median_full_lifespan_ms": 0.616,
|
"median_full_lifespan_ms": 0.67,
|
||||||
"enabled_component_count": 14
|
"enabled_component_count": 14
|
||||||
},
|
},
|
||||||
"safe": {
|
"safe": {
|
||||||
@@ -138,15 +141,38 @@
|
|||||||
{
|
{
|
||||||
"mode": "safe",
|
"mode": "safe",
|
||||||
"enabled_component_count": 6,
|
"enabled_component_count": 6,
|
||||||
"startup_ms": 0.399,
|
"startup_ms": 0.419,
|
||||||
"full_lifespan_ms": 0.46,
|
"full_lifespan_ms": 0.523,
|
||||||
"stage_ms": {
|
"stage_ms": {
|
||||||
"HTTP 基础能力": 0.069,
|
"HTTP 基础能力": 0.082,
|
||||||
"领域依赖装配": 0.035,
|
"领域依赖装配": 0.04,
|
||||||
"数据库引擎预热": 0.027,
|
"数据库引擎预热": 0.031,
|
||||||
"数据库连接预算": 0.029,
|
"数据库连接预算": 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.024,
|
||||||
"模块服务": 0.026
|
"模块服务": 0.022,
|
||||||
|
"插件同步与启动收尾": 0.032
|
||||||
},
|
},
|
||||||
"threads_before": 2,
|
"threads_before": 2,
|
||||||
"threads_started": 2,
|
"threads_started": 2,
|
||||||
@@ -159,36 +185,16 @@
|
|||||||
{
|
{
|
||||||
"mode": "safe",
|
"mode": "safe",
|
||||||
"enabled_component_count": 6,
|
"enabled_component_count": 6,
|
||||||
"startup_ms": 0.382,
|
"startup_ms": 0.41,
|
||||||
"full_lifespan_ms": 0.443,
|
"full_lifespan_ms": 0.538,
|
||||||
"stage_ms": {
|
"stage_ms": {
|
||||||
"HTTP 基础能力": 0.065,
|
"HTTP 基础能力": 0.076,
|
||||||
"领域依赖装配": 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,
|
|
||||||
"领域依赖装配": 0.034,
|
"领域依赖装配": 0.034,
|
||||||
"数据库引擎预热": 0.028,
|
"数据库引擎预热": 0.028,
|
||||||
"数据库连接预算": 0.027,
|
"数据库连接预算": 0.026,
|
||||||
"路由": 0.025,
|
"路由": 0.025,
|
||||||
"模块服务": 0.025
|
"模块服务": 0.024,
|
||||||
|
"插件同步与启动收尾": 0.06
|
||||||
},
|
},
|
||||||
"threads_before": 2,
|
"threads_before": 2,
|
||||||
"threads_started": 2,
|
"threads_started": 2,
|
||||||
@@ -199,8 +205,8 @@
|
|||||||
"database_connections_started": 0
|
"database_connections_started": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"median_startup_ms": 0.399,
|
"median_startup_ms": 0.419,
|
||||||
"median_full_lifespan_ms": 0.46,
|
"median_full_lifespan_ms": 0.538,
|
||||||
"enabled_component_count": 6
|
"enabled_component_count": 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ def test_application_preserves_stop_requested_before_startup(monkeypatch):
|
|||||||
"prepare_database",
|
"prepare_database",
|
||||||
lambda: calls.append("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()
|
main.run_application()
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ def test_application_does_not_start_server_after_migration_failure(monkeypatch):
|
|||||||
"prepare_database",
|
"prepare_database",
|
||||||
MagicMock(side_effect=migration_error),
|
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:
|
with pytest.raises(RuntimeError) as raised:
|
||||||
main.run_application()
|
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.signal, "signal", signal_handler)
|
||||||
monkeypatch.setattr(main, "start_tray", start_tray)
|
monkeypatch.setattr(main, "start_tray", start_tray)
|
||||||
monkeypatch.setattr(main, "prepare_database", prepare_database)
|
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):
|
with pytest.raises(UnsupportedProcessTopologyError):
|
||||||
main.run_application()
|
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