mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor: declare stop signal and plugin settlement in lifecycle manifest
This commit is contained in:
@@ -309,6 +309,13 @@ def prepare_plugin_restore() -> None:
|
|||||||
SystemChain().restore_plugins()
|
SystemChain().restore_plugins()
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_plugin_settlement(app: FastAPI) -> None:
|
||||||
|
"""调度插件同步与启动收尾任务,并把取消权交给最前置的 TaskRegistry owner。"""
|
||||||
|
task = asyncio.create_task(run_startup_step("插件同步与启动收尾", init_extra))
|
||||||
|
task_registry = app.state.task_registry
|
||||||
|
task_registry.register(task, owner="startup.plugin_settlement")
|
||||||
|
|
||||||
|
|
||||||
def prepare_database_component(app: FastAPI) -> None:
|
def prepare_database_component(app: FastAPI) -> None:
|
||||||
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
|
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
|
||||||
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
|
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
|
||||||
@@ -531,6 +538,22 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
|||||||
stop_order=10,
|
stop_order=10,
|
||||||
stop_timeout_seconds=300,
|
stop_timeout_seconds=300,
|
||||||
),
|
),
|
||||||
|
# 停止信号必须先于一切资源释放发出,让工作流、整理等长任务尽早感知停机。
|
||||||
|
LifecycleComponent(
|
||||||
|
name="停止信号",
|
||||||
|
stop=global_vars.stop_system,
|
||||||
|
stop_order=4,
|
||||||
|
stop_timeout_seconds=10,
|
||||||
|
),
|
||||||
|
# 插件同步与启动收尾是最后一个启动阶段;任务本身由 TaskRegistry 在关停预算内取消等待。
|
||||||
|
LifecycleComponent(
|
||||||
|
name="插件同步与启动收尾",
|
||||||
|
dependencies=("工作流",),
|
||||||
|
start=lambda: schedule_plugin_settlement(app),
|
||||||
|
start_order=150,
|
||||||
|
start_timeout_seconds=30,
|
||||||
|
start_failure=LifecycleFailurePolicy.FAIL_FAST,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -585,12 +608,6 @@ async def lifespan(app: FastAPI):
|
|||||||
active_start_component = None
|
active_start_component = None
|
||||||
if settings.MOVIEPILOT_SAFE_MODE:
|
if settings.MOVIEPILOT_SAFE_MODE:
|
||||||
print("MoviePilot safe mode enabled: skip plugins, scheduler, monitor, commands and workflow.")
|
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()
|
health.mark_ready()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
# Uvicorn 在 lifespan 抛错时不会开始接流量;状态仍需供嵌入式入口和测试诊断。
|
# Uvicorn 在 lifespan 抛错时不会开始接流量;状态仍需供嵌入式入口和测试诊断。
|
||||||
@@ -614,10 +631,10 @@ async def lifespan(app: FastAPI):
|
|||||||
finally:
|
finally:
|
||||||
health.mark_stopping()
|
health.mark_stopping()
|
||||||
print("Shutting down...")
|
print("Shutting down...")
|
||||||
global_vars.stop_system()
|
|
||||||
try:
|
try:
|
||||||
# 插件 settlement 已登记到最前置 TaskRegistry。由该 FAIL_FAST owner
|
# 插件 settlement 已登记到最前置 TaskRegistry。由该 FAIL_FAST owner
|
||||||
# 在统一预算内取消/等待,不能在屏障之前无界 await 绕过停机预算。
|
# 在统一预算内取消/等待,不能在屏障之前无界 await 绕过停机预算。
|
||||||
|
# 停止信号与各资源 owner 的释放顺序由组件清单声明。
|
||||||
await stop_lifecycle_components(enabled_components)
|
await stop_lifecycle_components(enabled_components)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ chain 层零 `app.db` / `app.modules` 内部直连,domain 与 chain 层配置
|
|||||||
3. 停止标志设置;
|
3. 停止标志设置;
|
||||||
4. 日志关闭靠注释约定顺序。
|
4. 日志关闭靠注释约定顺序。
|
||||||
|
|
||||||
|
> 处理进展(2026-08-24):第 2、3 项已组件化——"停止信号"(stop_order=4,先于一切
|
||||||
|
> 资源释放发出停机通知,启动失败清理同样生效)与"插件同步与启动收尾"(start_order=150,
|
||||||
|
> 依赖工作流,取消权仍归最前置 TaskRegistry owner)均已进入声明式清单并通过顺序快照测试。
|
||||||
|
> 第 1、4 项经复核**不能直接进清单**:当前引擎的 FAIL_FAST break 会跳过更高 stop_order
|
||||||
|
> 的组件,而主循环清除与日志关闭在现有嵌套 finally 中是无条件执行的"最外层保底",
|
||||||
|
> 直接搬移会让无关 owner 未收敛时跳过这两步(回归)。如需收口,须先做显式引擎决策:
|
||||||
|
> 引入"最终保底 finalizer"概念或调整 FAIL_FAST 传播语义。
|
||||||
|
|
||||||
另有两个组合根脆弱点:
|
另有两个组合根脆弱点:
|
||||||
|
|
||||||
* `initializers/command.py`、`initializers/scheduler.py`、`initializers/agent.py`
|
* `initializers/command.py`、`initializers/scheduler.py`、`initializers/agent.py`
|
||||||
|
|||||||
@@ -553,8 +553,10 @@ def test_lifecycle_manifest_declares_normal_and_safe_mode_order() -> None:
|
|||||||
"待处理整理回放",
|
"待处理整理回放",
|
||||||
"命令服务",
|
"命令服务",
|
||||||
"工作流",
|
"工作流",
|
||||||
|
"插件同步与启动收尾",
|
||||||
]
|
]
|
||||||
assert normal_stop == [
|
assert normal_stop == [
|
||||||
|
"停止信号",
|
||||||
"后台任务登记器",
|
"后台任务登记器",
|
||||||
"插件变更监控",
|
"插件变更监控",
|
||||||
"插件备份",
|
"插件备份",
|
||||||
@@ -583,6 +585,8 @@ def test_lifecycle_manifest_declares_normal_and_safe_mode_order() -> None:
|
|||||||
"AI智能体会话",
|
"AI智能体会话",
|
||||||
"整理后台服务",
|
"整理后台服务",
|
||||||
"事件投递屏障",
|
"事件投递屏障",
|
||||||
|
"停止信号",
|
||||||
|
"插件同步与启动收尾",
|
||||||
}
|
}
|
||||||
assert all(item["start_failure"] == "fail_fast" for item in normal)
|
assert all(item["start_failure"] == "fail_fast" for item in normal)
|
||||||
assert {
|
assert {
|
||||||
@@ -840,7 +844,9 @@ def test_lifespan_cleans_started_owners_after_late_startup_failure(monkeypatch):
|
|||||||
asyncio.run(run_lifespan())
|
asyncio.run(run_lifespan())
|
||||||
|
|
||||||
assert raised.value is startup_error
|
assert raised.value is startup_error
|
||||||
lifecycle.global_vars.stop_system.assert_not_called()
|
# 停止信号是无依赖的 stop-only owner:启动失败清理同样要先发出停机通知,
|
||||||
|
# 让仍在运行的后台任务尽早感知进程即将退出。
|
||||||
|
lifecycle.global_vars.stop_system.assert_called_once_with()
|
||||||
for name in (
|
for name in (
|
||||||
"stop_plugin_monitor",
|
"stop_plugin_monitor",
|
||||||
"backup_plugins",
|
"backup_plugins",
|
||||||
|
|||||||
Reference in New Issue
Block a user