"""应用生命周期组件的组装、启动和关闭编排。""" 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)