mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 04:27:40 +08:00
592 lines
21 KiB
Python
592 lines
21 KiB
Python
"""应用生命周期组件的组装、启动和关闭编排。"""
|
|
|
|
import asyncio
|
|
import inspect
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
from typing import Callable
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from app.startup.initializers.cache import configure_cache_dependencies
|
|
# 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。
|
|
configure_cache_dependencies()
|
|
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
|
|
try:
|
|
import urllib3.fields as _urllib3_fields
|
|
|
|
if not hasattr(_urllib3_fields, "format_header_param") and hasattr(
|
|
_urllib3_fields, "format_header_param_rfc2231"
|
|
):
|
|
_urllib3_fields.format_header_param = (
|
|
_urllib3_fields.format_header_param_rfc2231
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
from app.chain.system import SystemChain
|
|
from app.application.plugin.lifecycle import plugin_lifecycle
|
|
from app.application.plugin.runtime import get_plugin_manager
|
|
from app.runtime.config import global_vars
|
|
from app.runtime.settings import RuntimeSettingsCompat
|
|
|
|
settings = RuntimeSettingsCompat()
|
|
from app.runtime.health import get_application_health
|
|
from app.runtime.topology import validate_process_topology
|
|
from app.runtime.tasks import TaskRegistry, configure_task_registry
|
|
from app.adapters.external.server import MoviePilotServerHelper
|
|
from app.runtime.state import SystemHelper
|
|
from app.runtime.log import logger, LoggerManager
|
|
from app.startup.initializers.command import init_command, stop_command, restart_command
|
|
from app.startup.initializers.agent import stop_agent
|
|
from app.startup.initializers.domain import configure_domain_dependencies
|
|
from app.startup.initializers.modules import (
|
|
drain_events,
|
|
init_modules,
|
|
settle_events,
|
|
stop_modules,
|
|
)
|
|
from app.startup.initializers.monitor import stop_monitor, init_monitor
|
|
from app.startup.initializers.plugins import (
|
|
configure_plugin_services,
|
|
execute_task,
|
|
finalize_plugins,
|
|
init_plugins,
|
|
quiesce_plugin_services,
|
|
quiesce_plugins,
|
|
stop_plugin_monitor,
|
|
sync_plugins,
|
|
)
|
|
from app.startup.initializers.routers import init_routers
|
|
from app.startup.initializers.scheduler import (
|
|
stop_scheduler,
|
|
init_scheduler,
|
|
init_plugin_scheduler,
|
|
)
|
|
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
|
|
from app.startup.initializers.transfer import (
|
|
replay_pending_transfers,
|
|
stop_transfer_runtime,
|
|
)
|
|
from app.startup.initializers.workflow import init_workflow, stop_workflow
|
|
from app.startup.lifecycle.components import (
|
|
LifecycleComponent,
|
|
LifecycleFailurePolicy,
|
|
LifecycleMode,
|
|
lifecycle_manifest,
|
|
)
|
|
from app.adapters.network.http import (
|
|
aclose_shared_async_transports,
|
|
configure_default_user_agent,
|
|
)
|
|
|
|
|
|
async def init_extra():
|
|
"""
|
|
同步插件及重启相关依赖服务
|
|
"""
|
|
if settings.MOVIEPILOT_SAFE_MODE:
|
|
SystemHelper().set_system_modified()
|
|
SystemChain().restart_finish()
|
|
return
|
|
plugin_manager = get_plugin_manager()
|
|
try:
|
|
async with plugin_lifecycle.hold_startup():
|
|
if await sync_plugins():
|
|
await execute_task(
|
|
global_vars.loop,
|
|
init_plugin_scheduler,
|
|
"插件定时服务刷新",
|
|
)
|
|
await asyncio.wrap_future(restart_command())
|
|
finally:
|
|
plugin_manager.set_plugin_settling(False)
|
|
plugin_manager.start_monitor()
|
|
# 设置系统已修改标志
|
|
SystemHelper().set_system_modified()
|
|
# 重启完成
|
|
SystemChain().restart_finish()
|
|
# 上报当前安装版本
|
|
await MoviePilotServerHelper.async_report_usage()
|
|
|
|
|
|
async def run_shutdown_step(
|
|
name: str,
|
|
callback: Callable[[], object],
|
|
timeout_seconds: float | None = None,
|
|
) -> bool:
|
|
"""在有限预算内执行关闭阶段,并返回资源 owner 是否已经收敛。"""
|
|
try:
|
|
result = callback()
|
|
if inspect.isawaitable(result):
|
|
task = asyncio.ensure_future(result)
|
|
|
|
def _consume_shutdown_result(done: asyncio.Future) -> None:
|
|
"""消费延迟收敛任务的最终异常,避免事件循环产生未取回异常。"""
|
|
try:
|
|
done.result()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception as err:
|
|
logger.error(f"关闭{name}最终收尾失败:{err}")
|
|
|
|
task.add_done_callback(_consume_shutdown_result)
|
|
if timeout_seconds:
|
|
try:
|
|
result = await asyncio.wait_for(
|
|
asyncio.shield(task), timeout=timeout_seconds
|
|
)
|
|
except asyncio.TimeoutError:
|
|
logger.error("关闭%s超时,已请求取消并保留未收敛任务", name)
|
|
task.cancel()
|
|
return False
|
|
else:
|
|
result = await task
|
|
if result is False:
|
|
logger.error("关闭%s未收敛,资源所有权保持不变", name)
|
|
return False
|
|
return True
|
|
except Exception as err:
|
|
logger.error(f"关闭{name}失败:{err}")
|
|
return False
|
|
|
|
|
|
async def run_startup_step(
|
|
name: str,
|
|
callback: Callable[[], object],
|
|
timeout_seconds: float | None = None,
|
|
) -> object:
|
|
"""执行单个启动阶段并记录耗时,失败时保留原异常和 fail-fast 语义。"""
|
|
started_at = time.perf_counter()
|
|
try:
|
|
result = callback()
|
|
if inspect.isawaitable(result):
|
|
if timeout_seconds:
|
|
result = await asyncio.wait_for(result, timeout=timeout_seconds)
|
|
else:
|
|
result = await result
|
|
return result
|
|
finally:
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
logger.info("启动%s完成,耗时=%.2fms", name, elapsed_ms)
|
|
|
|
|
|
async def stop_lifecycle_components(
|
|
components: tuple[LifecycleComponent, ...],
|
|
) -> bool:
|
|
"""按声明顺序关闭组件,并在关键 owner 未收敛时停止释放依赖。"""
|
|
all_converged = True
|
|
for component in sorted(
|
|
(item for item in components if item.stop is not None),
|
|
key=lambda item: item.stop_order or 0,
|
|
):
|
|
completed = await run_shutdown_step(
|
|
component.name,
|
|
component.stop,
|
|
component.stop_timeout_seconds,
|
|
)
|
|
if completed:
|
|
continue
|
|
all_converged = False
|
|
if component.stop_failure is LifecycleFailurePolicy.FAIL_FAST:
|
|
logger.error(
|
|
"关闭%s未收敛,停止释放其后续依赖",
|
|
component.name,
|
|
)
|
|
break
|
|
return all_converged
|
|
|
|
|
|
def select_startup_cleanup_components(
|
|
components: tuple[LifecycleComponent, ...],
|
|
*,
|
|
started_names: set[str],
|
|
active_component: LifecycleComponent | None,
|
|
) -> tuple[LifecycleComponent, ...]:
|
|
"""选择启动失败时已启动、部分启动及其 stop-only owner 的清理集合。"""
|
|
cleanup_names = set(started_names)
|
|
if active_component is not None:
|
|
cleanup_names.add(active_component.name)
|
|
|
|
# stop-only owner 没有启动回调,按已激活依赖递归纳入,避免清理时反向
|
|
# 实例化尚未触达的插件、模块或外部资源。
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for component in components:
|
|
if (
|
|
component.stop is None
|
|
or component.start is not None
|
|
or component.name in cleanup_names
|
|
or not set(component.dependencies).issubset(cleanup_names)
|
|
):
|
|
continue
|
|
cleanup_names.add(component.name)
|
|
changed = True
|
|
|
|
return tuple(
|
|
component
|
|
for component in components
|
|
if component.stop is not None and component.name in cleanup_names
|
|
)
|
|
|
|
|
|
async def initialize_modules_component(app: FastAPI) -> None:
|
|
"""启动模块并把其类型化运行时发布到当前 FastAPI AppState。"""
|
|
try:
|
|
runtime = await init_modules()
|
|
except BaseException:
|
|
from app.startup.initializers.modules import stop_database_worker
|
|
|
|
try:
|
|
await stop_database_worker()
|
|
except Exception as cleanup_error: # noqa: BLE001 保留原始启动异常
|
|
logger.error(f"启动失败后的数据库任务清理失败:{cleanup_error}")
|
|
raise
|
|
if runtime is not None:
|
|
app.state.host_runtime = runtime
|
|
|
|
|
|
def initialize_task_registry(app: FastAPI) -> None:
|
|
"""创建当前 lifespan 独占的后台任务登记器。"""
|
|
task_registry = TaskRegistry()
|
|
app.state.task_registry = task_registry
|
|
configure_task_registry(task_registry)
|
|
|
|
|
|
async def stop_task_registry(app: FastAPI) -> bool:
|
|
"""停止接收新后台任务,并把已封口登记器保留到下一次显式启动。"""
|
|
task_registry = getattr(app.state, "task_registry", None)
|
|
if not isinstance(task_registry, TaskRegistry):
|
|
configure_task_registry(None)
|
|
app.state.task_registry = None
|
|
return True
|
|
# 即使 owner 已收敛,也不能在后续插件/模块 stop hook 仍会运行时退回永久
|
|
# accepting 的兼容默认登记器。下一次 initialize_task_registry 会显式替换它。
|
|
return await task_registry.shutdown(timeout_seconds=30.0)
|
|
|
|
|
|
def prepare_plugin_restore() -> None:
|
|
"""先装配插件外部系统服务,再恢复插件及其依赖。"""
|
|
configure_plugin_services()
|
|
SystemChain().restore_plugins()
|
|
|
|
|
|
def prepare_database_component(app: FastAPI) -> None:
|
|
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
|
|
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
|
|
# 继续保持不建库、不加载迁移运行时的纯 ASGI 结构语义。
|
|
from app.startup.initializers.database import (
|
|
prepare_database,
|
|
verify_database_revision,
|
|
)
|
|
|
|
prepare_database()
|
|
verify_database_revision()
|
|
get_application_health(app).mark_database_ready()
|
|
|
|
|
|
def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
|
"""按现有顺序构建应用组件清单,回调在每次 lifespan 启动时重新绑定。"""
|
|
return (
|
|
LifecycleComponent(
|
|
name="后台任务登记器",
|
|
start=lambda: initialize_task_registry(app),
|
|
stop=lambda: stop_task_registry(app),
|
|
start_order=5,
|
|
stop_order=5,
|
|
start_timeout_seconds=30,
|
|
stop_timeout_seconds=60,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="数据库准备",
|
|
start=lambda: prepare_database_component(app),
|
|
start_order=10,
|
|
start_timeout_seconds=300,
|
|
),
|
|
LifecycleComponent(
|
|
name="HTTP 基础能力",
|
|
start=lambda: configure_default_user_agent(settings.USER_AGENT),
|
|
stop=aclose_shared_async_transports,
|
|
start_order=20,
|
|
stop_order=80,
|
|
start_timeout_seconds=30,
|
|
stop_timeout_seconds=120,
|
|
),
|
|
LifecycleComponent(
|
|
name="领域依赖装配",
|
|
dependencies=("HTTP 基础能力",),
|
|
start=configure_domain_dependencies,
|
|
start_order=30,
|
|
start_timeout_seconds=30,
|
|
),
|
|
LifecycleComponent(
|
|
name="数据库引擎预热",
|
|
dependencies=("数据库准备", "领域依赖装配"),
|
|
start=lambda: (get_engine(), get_global_async_engine()),
|
|
start_order=40,
|
|
start_timeout_seconds=120,
|
|
),
|
|
LifecycleComponent(
|
|
name="数据库连接预算",
|
|
dependencies=("数据库引擎预热",),
|
|
start=check_connection_budget,
|
|
start_order=50,
|
|
start_timeout_seconds=30,
|
|
),
|
|
LifecycleComponent(
|
|
name="路由",
|
|
dependencies=("数据库连接预算",),
|
|
start=lambda: init_routers(app, settings.API_V1_STR),
|
|
start_order=60,
|
|
start_timeout_seconds=30,
|
|
),
|
|
LifecycleComponent(
|
|
name="模块服务",
|
|
dependencies=("路由",),
|
|
start=lambda: initialize_modules_component(app),
|
|
stop=stop_modules,
|
|
start_order=70,
|
|
stop_order=70,
|
|
start_timeout_seconds=300,
|
|
stop_timeout_seconds=300,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件备份恢复",
|
|
dependencies=("模块服务",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=prepare_plugin_restore,
|
|
start_order=80,
|
|
start_timeout_seconds=300,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件",
|
|
dependencies=("插件备份恢复",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=init_plugins,
|
|
stop=finalize_plugins,
|
|
start_order=90,
|
|
stop_order=60,
|
|
start_timeout_seconds=300,
|
|
stop_timeout_seconds=300,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件变更监控",
|
|
dependencies=("插件",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
stop=stop_plugin_monitor,
|
|
stop_order=8,
|
|
stop_timeout_seconds=10,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="定时器",
|
|
dependencies=("插件",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=init_scheduler,
|
|
stop=stop_scheduler,
|
|
start_order=100,
|
|
stop_order=50,
|
|
start_timeout_seconds=120,
|
|
stop_timeout_seconds=120,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="监控器",
|
|
dependencies=("定时器",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=init_monitor,
|
|
stop=stop_monitor,
|
|
start_order=110,
|
|
stop_order=40,
|
|
start_timeout_seconds=120,
|
|
stop_timeout_seconds=120,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="整理后台服务",
|
|
dependencies=("模块服务",),
|
|
stop=stop_transfer_runtime,
|
|
stop_order=52,
|
|
stop_timeout_seconds=45,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="AI智能体会话",
|
|
dependencies=("模块服务",),
|
|
stop=stop_agent,
|
|
stop_order=51,
|
|
stop_timeout_seconds=300,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件事件入口",
|
|
dependencies=("插件",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
stop=quiesce_plugins,
|
|
stop_order=53,
|
|
stop_timeout_seconds=300,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="事件尾任务结算",
|
|
dependencies=("模块服务", "插件"),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
stop=settle_events,
|
|
stop_order=54,
|
|
stop_timeout_seconds=120,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件后台服务",
|
|
dependencies=("插件",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
stop=quiesce_plugin_services,
|
|
stop_order=55,
|
|
stop_timeout_seconds=300,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="事件投递屏障",
|
|
dependencies=("模块服务", "整理后台服务"),
|
|
stop=drain_events,
|
|
stop_order=58,
|
|
stop_timeout_seconds=120,
|
|
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
|
),
|
|
LifecycleComponent(
|
|
name="待处理整理回放",
|
|
dependencies=("监控器", "整理后台服务"),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=replay_pending_transfers,
|
|
start_order=120,
|
|
start_timeout_seconds=30,
|
|
),
|
|
LifecycleComponent(
|
|
name="命令服务",
|
|
dependencies=("待处理整理回放",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=init_command,
|
|
stop=stop_command,
|
|
start_order=130,
|
|
stop_order=30,
|
|
start_timeout_seconds=120,
|
|
stop_timeout_seconds=120,
|
|
),
|
|
LifecycleComponent(
|
|
name="工作流",
|
|
dependencies=("命令服务",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
start=init_workflow,
|
|
stop=stop_workflow,
|
|
start_order=140,
|
|
stop_order=20,
|
|
start_timeout_seconds=120,
|
|
stop_timeout_seconds=120,
|
|
),
|
|
LifecycleComponent(
|
|
name="插件备份",
|
|
dependencies=("插件",),
|
|
mode=LifecycleMode.NORMAL_ONLY,
|
|
stop=lambda: SystemChain().backup_plugins(),
|
|
stop_order=10,
|
|
stop_timeout_seconds=300,
|
|
),
|
|
)
|
|
|
|
|
|
def get_lifecycle_manifest(app: FastAPI, *, safe_mode: bool) -> tuple[dict[str, object], ...]:
|
|
"""导出指定模式下的生命周期组件、依赖、顺序和超时清单。"""
|
|
return lifecycle_manifest(
|
|
build_lifecycle_components(app),
|
|
safe_mode=safe_mode,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""
|
|
定义应用的生命周期事件
|
|
"""
|
|
health = get_application_health(app)
|
|
health.begin_startup()
|
|
main_loop = asyncio.get_running_loop()
|
|
main_loop_owner: object | None = None
|
|
enabled_components: tuple[LifecycleComponent, ...] = ()
|
|
started_component_names: set[str] = set()
|
|
active_start_component: LifecycleComponent | None = None
|
|
try:
|
|
validate_process_topology(
|
|
workers=settings.API_WORKERS,
|
|
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
|
|
)
|
|
print("Starting up...")
|
|
main_loop_owner = global_vars.set_loop(main_loop)
|
|
components = build_lifecycle_components(app)
|
|
enabled_components = tuple(
|
|
component
|
|
for component in components
|
|
if component.enabled(settings.MOVIEPILOT_SAFE_MODE)
|
|
)
|
|
logger.info(
|
|
"启用生命周期组件:%s",
|
|
", ".join(component.name for component in enabled_components),
|
|
)
|
|
for component in sorted(
|
|
(item for item in enabled_components if item.start is not None),
|
|
key=lambda item: item.start_order or 0,
|
|
):
|
|
active_start_component = component
|
|
await run_startup_step(
|
|
component.name,
|
|
component.start,
|
|
component.start_timeout_seconds,
|
|
)
|
|
started_component_names.add(component.name)
|
|
active_start_component = None
|
|
if settings.MOVIEPILOT_SAFE_MODE:
|
|
print("MoviePilot safe mode enabled: skip plugins, scheduler, monitor, commands and workflow.")
|
|
# 插件同步到本地
|
|
sync_plugins_task = asyncio.create_task(
|
|
run_startup_step("插件同步与启动收尾", init_extra)
|
|
)
|
|
task_registry = app.state.task_registry
|
|
task_registry.register(sync_plugins_task, owner="startup.plugin_settlement")
|
|
health.mark_ready()
|
|
except BaseException:
|
|
# Uvicorn 在 lifespan 抛错时不会开始接流量;状态仍需供嵌入式入口和测试诊断。
|
|
health.mark_failed()
|
|
cleanup_components = select_startup_cleanup_components(
|
|
enabled_components,
|
|
started_names=started_component_names,
|
|
active_component=active_start_component,
|
|
)
|
|
try:
|
|
await stop_lifecycle_components(cleanup_components)
|
|
except Exception as cleanup_error:
|
|
logger.error(f"启动失败后的生命周期清理失败:{cleanup_error}")
|
|
finally:
|
|
if main_loop_owner is not None:
|
|
global_vars.clear_loop(main_loop_owner)
|
|
raise
|
|
try:
|
|
# 在此处 yield,表示应用已经启动,控制权交回 FastAPI 主事件循环
|
|
yield
|
|
finally:
|
|
health.mark_stopping()
|
|
print("Shutting down...")
|
|
global_vars.stop_system()
|
|
try:
|
|
# 插件 settlement 已登记到最前置 TaskRegistry。由该 FAIL_FAST owner
|
|
# 在统一预算内取消/等待,不能在屏障之前无界 await 绕过停机预算。
|
|
await stop_lifecycle_components(enabled_components)
|
|
finally:
|
|
try:
|
|
# 日志最后关闭,确保其他组件的收尾信息已写入文件
|
|
LoggerManager.shutdown()
|
|
finally:
|
|
if main_loop_owner is not None:
|
|
global_vars.clear_loop(main_loop_owner)
|