mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: enforce single-worker control plane
This commit is contained in:
@@ -19,6 +19,7 @@ from urllib.request import Request, urlopen
|
||||
import psutil
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.topology import process_topology_issue
|
||||
from app.doctor.models import DoctorFinding, DoctorFindingStatus, DoctorReport, DoctorSeverity
|
||||
from app.adapters.system.host import SystemUtils
|
||||
|
||||
@@ -134,6 +135,7 @@ def default_checks() -> list[CheckFunc]:
|
||||
return [
|
||||
_check_runtime_paths,
|
||||
_check_config,
|
||||
_check_process_topology,
|
||||
_check_processes_and_ports,
|
||||
_check_dependencies,
|
||||
_check_database,
|
||||
@@ -154,6 +156,49 @@ def _mask_text(text: str) -> str:
|
||||
return masked
|
||||
|
||||
|
||||
def _check_process_topology(runner: DoctorRunnerProtocol) -> None:
|
||||
"""诊断 API worker 配置是否会复制全功能控制面。"""
|
||||
issue = process_topology_issue(
|
||||
workers=settings.API_WORKERS,
|
||||
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
|
||||
)
|
||||
context = {
|
||||
"api_workers": settings.API_WORKERS,
|
||||
"safe_mode": settings.MOVIEPILOT_SAFE_MODE,
|
||||
}
|
||||
if issue:
|
||||
runner.add(
|
||||
finding_id="startup.process_topology",
|
||||
severity=DoctorSeverity.Error,
|
||||
status=DoctorFindingStatus.Failed,
|
||||
title="进程拓扑不受支持",
|
||||
detail=issue,
|
||||
recommendation="将 API_WORKERS 设为 1 后重启 MoviePilot。",
|
||||
context=context,
|
||||
)
|
||||
return
|
||||
if settings.API_WORKERS != 1:
|
||||
runner.add(
|
||||
finding_id="startup.process_topology",
|
||||
severity=DoctorSeverity.Warn,
|
||||
status=DoctorFindingStatus.Degraded,
|
||||
title="安全模式临时使用多 worker",
|
||||
detail="安全模式不会启动插件、调度器、监控器和工作流,因此当前不会复制完整控制面。",
|
||||
recommendation="故障排除后恢复 API_WORKERS=1,再退出安全模式。",
|
||||
context=context,
|
||||
)
|
||||
return
|
||||
runner.add(
|
||||
finding_id="startup.process_topology",
|
||||
severity=DoctorSeverity.Info,
|
||||
status=DoctorFindingStatus.Ok,
|
||||
title="进程拓扑受支持",
|
||||
detail="API_WORKERS=1,插件和后台任务只会启动一份。",
|
||||
recommendation="保持单 worker;扩容前需要先拆分 API 数据面和控制面。",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> Optional[dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
@@ -55,6 +55,7 @@ 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.startup.database_initializer import prepare_database
|
||||
|
||||
setproctitle.setproctitle(settings.PROJECT_NAME)
|
||||
@@ -133,6 +134,10 @@ def signal_handler(signum, frame):
|
||||
|
||||
def run_application() -> None:
|
||||
"""初始化进程并启动 API 服务"""
|
||||
validate_process_topology(
|
||||
workers=settings.API_WORKERS,
|
||||
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
|
||||
)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class ConfigModel(BaseModel):
|
||||
# 数据库连接额度校验按它换算总用量。注意:当前主程序以单进程方式启动
|
||||
# (uvicorn.Config 的 workers 仅在多进程 supervisor 路径下生效),
|
||||
# 调大此项前需先解决调度器会在每个 worker 内重复执行的问题
|
||||
API_WORKERS: int = 1
|
||||
API_WORKERS: int = Field(default=1, ge=1)
|
||||
DB_TYPE: str = "sqlite"
|
||||
# 是否在控制台输出 SQL 语句,默认关闭
|
||||
DB_ECHO: bool = False
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""MoviePilot 进程拓扑约束。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UnsupportedProcessTopologyError(RuntimeError):
|
||||
"""启动配置会复制当前只能单实例运行的控制面。"""
|
||||
|
||||
|
||||
def process_topology_issue(*, workers: int, safe_mode: bool) -> Optional[str]:
|
||||
"""
|
||||
返回当前进程拓扑不可运行的原因。
|
||||
|
||||
:param workers: API worker 进程数
|
||||
:param safe_mode: 是否只启动安全模式数据面
|
||||
:return: 支持时返回 None,否则返回可直接展示的错误说明
|
||||
"""
|
||||
if workers < 1:
|
||||
return "API_WORKERS 必须大于等于 1"
|
||||
if workers == 1 or safe_mode:
|
||||
return None
|
||||
return (
|
||||
"MoviePilot V3 全功能模式仅支持 API_WORKERS=1;"
|
||||
f"当前配置为 {workers},每个 worker 都会重复启动插件、调度器、监控器和工作流。"
|
||||
"请将 API_WORKERS 改为 1 后重启。故障排查可以临时启用 "
|
||||
"MOVIEPILOT_SAFE_MODE=true,但安全模式不是全功能扩容方案。"
|
||||
)
|
||||
|
||||
|
||||
def validate_process_topology(*, workers: int, safe_mode: bool) -> None:
|
||||
"""
|
||||
在启动任何持久化或后台副作用前校验进程拓扑。
|
||||
|
||||
:param workers: API worker 进程数
|
||||
:param safe_mode: 是否只启动安全模式数据面
|
||||
:raises UnsupportedProcessTopologyError: 当前拓扑会复制全功能控制面
|
||||
"""
|
||||
issue = process_topology_issue(workers=workers, safe_mode=safe_mode)
|
||||
if issue:
|
||||
raise UnsupportedProcessTopologyError(issue)
|
||||
@@ -27,6 +27,7 @@ except Exception:
|
||||
from app.chain.system import SystemChain
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.runtime.config import global_vars, settings
|
||||
from app.runtime.topology import validate_process_topology
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.log import logger, LoggerManager
|
||||
@@ -47,7 +48,7 @@ from app.startup.scheduler_initializer import (
|
||||
init_scheduler,
|
||||
init_plugin_scheduler,
|
||||
)
|
||||
from app.db import check_connection_budget, get_engine, get_global_async_engine
|
||||
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
|
||||
from app.startup.transfer_initializer import replay_pending_transfers
|
||||
from app.startup.workflow_initializer import init_workflow, stop_workflow
|
||||
from app.startup.lifecycle.components import (
|
||||
@@ -277,6 +278,10 @@ async def lifespan(app: FastAPI):
|
||||
"""
|
||||
定义应用的生命周期事件
|
||||
"""
|
||||
validate_process_topology(
|
||||
workers=settings.API_WORKERS,
|
||||
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
|
||||
)
|
||||
print("Starting up...")
|
||||
# 存储当前循环
|
||||
global_vars.set_loop(asyncio.get_event_loop())
|
||||
|
||||
Reference in New Issue
Block a user