refactor: own search recommendation tasks

This commit is contained in:
jxxghp
2026-08-24 06:20:48 +08:00
parent b3211ae4e4
commit 9a0d23857d
4 changed files with 70 additions and 5 deletions
+5 -1
View File
@@ -25,6 +25,7 @@ from app.application.configuration import (
get_configured_system_config, get_configured_system_config,
) )
from app.runtime.progress import AsyncProgressHelper, ProgressHelper from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.runtime.tasks import get_task_registry
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.search.state import ( from app.application.search.state import (
SearchStateService, SearchStateService,
@@ -605,7 +606,10 @@ class SearchChain(ChainBase):
state._ai_recommend_running = False state._ai_recommend_running = False
state._ai_recommend_task = None state._ai_recommend_task = None
state._ai_recommend_task = asyncio.create_task(run_recommend()) state._ai_recommend_task = get_task_registry().create(
run_recommend(),
owner="chain.search.ai_recommend",
)
def search_by_id( def search_by_id(
self, media_source: MediaSource, media_id: str, self, media_source: MediaSource, media_id: str,
@@ -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~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务。 > 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -320,13 +320,24 @@
- 回归测试覆盖父任务取消和进度消费者失败,均证明阻塞 loader 收到取消后才允许父调用结束;插件管理器 - 回归测试覆盖父任务取消和进度消费者失败,均证明阻塞 loader 收到取消后才允许父调用结束;插件管理器
方法、目录 DTO、市场参数、缓存、V1/V2/V3 索引与插件 SDK/Compat 均未修改。依赖图仍为 `806/6545` 方法、目录 DTO、市场参数、缓存、V1/V2/V3 索引与插件 SDK/Compat 均未修改。依赖图仍为 `806/6545`
### 长期整改阶段 31:搜索 AI 推荐生命周期托管(2026-08-24
- 搜索 AI 推荐原先用类级 `_ai_recommend_task` 持有裸 `asyncio.create_task()`:新搜索和手工取消能停止
旧任务,但 lifespan shutdown 不知道该任务存在,可能在 Agent/LLM 调用尚未结束时越过关停预算。
- 同步 `start_recommend_task()` 入口现在通过 TaskRegistry 创建 `chain.search.ai_recommend` owner,仍把同一个
Task 保存到原类状态供轮询、强制重启和取消入口使用;任务结果、错误、缓存索引和旧请求丢弃逻辑不变。
- 回归测试使用真实 Registry 阻塞推荐调用,证明 shutdown 会取消并等待任务终态,随后 running/task 状态
正常复位。API 参数与响应、SearchChain 方法签名、Agent 调用协议和插件 SDK/Compat 均未修改。
- 依赖基线仅新增 `app.chain.search -> app.runtime.tasks`,模块保持 `806`、内部边为 `6546`12 组禁止边
与唯一隔离 TMDB SCC 均未变化。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
- 依赖图当前为 `806` 个 Python 模块、`6545` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 依赖图当前为 `806` 个 Python 模块、`6546` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
@@ -346,6 +357,8 @@
`finally` 取消并等待清理,断线和 ASGI 取消均不会留下请求级 task。 `finally` 取消并等待清理,断线和 ASGI 取消均不会留下请求级 task。
插件市场目录并发也采用同一责任边界:API/Agent 父请求取消或进度回调失败时,聚合服务会取消并等待 插件市场目录并发也采用同一责任边界:API/Agent 父请求取消或进度回调失败时,聚合服务会取消并等待
全部市场/代际 loader;正常单源失败仍隔离,不把请求级读取错误登记成 lifespan 后台任务。 全部市场/代际 loader;正常单源失败仍隔离,不把请求级读取错误登记成 lifespan 后台任务。
搜索结果 AI 推荐则属于跨请求轮询任务,继续由 SearchChain 状态暴露进度,但任务创建已登记
`chain.search.ai_recommend` owner;新搜索、手工取消和 lifespan shutdown 现在共享同一个 Task 终态。
订阅删除的宿主生产者也已完成 durable 分级:消息交互和远程删除不再调用裸线程统计入口,而是 订阅删除的宿主生产者也已完成 durable 分级:消息交互和远程删除不再调用裸线程统计入口,而是
与业务删除原子暂存事件和统计 intent;旧类方法只作为插件 ABI 保留,不纳入宿主可靠性证明。 与业务删除原子暂存事件和统计 intent;旧类方法只作为插件 ABI 保留,不纳入宿主可靠性证明。
消息渠道回环也不再各自拥有 URL、HTTP 判定和逐消息线程:七种宿主渠道共享 ingress,三种立即返回 消息渠道回环也不再各自拥有 URL、HTTP 判定和逐消息线程:七种宿主渠道共享 ingress,三种立即返回
+3 -2
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [], "runtime_to_db": [],
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6545, "edge_count": 6546,
"edge_sha256": "5a57ed0ff52fda67abf49f5ca956fea0adeb0ac595054e0f8f81374ad843a2bd", "edge_sha256": "c840c6290e55ff267e35481176484590c5635d2d22904f57dd32d5adf396d0d1",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -3283,6 +3283,7 @@
"app.chain.search -> app.runtime.execution", "app.chain.search -> app.runtime.execution",
"app.chain.search -> app.runtime.log", "app.chain.search -> app.runtime.log",
"app.chain.search -> app.runtime.progress", "app.chain.search -> app.runtime.progress",
"app.chain.search -> app.runtime.tasks",
"app.chain.search -> app.schemas", "app.chain.search -> app.schemas",
"app.chain.search -> app.schemas.media", "app.chain.search -> app.schemas.media",
"app.chain.search -> app.schemas.mediaserver", "app.chain.search -> app.schemas.mediaserver",
+47
View File
@@ -16,6 +16,7 @@ from app.agent.contracts import ReplyMode
from app.agent.orchestrator import agent_manager from app.agent.orchestrator import agent_manager
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.runtime.config import settings from app.runtime.config import settings
from app.runtime.tasks import TaskRegistry
from app.modules.indexer import IndexerModule from app.modules.indexer import IndexerModule
from app.schemas.types import MediaType from app.schemas.types import MediaType
@@ -102,6 +103,52 @@ class SearchChainAIRecommendTest(unittest.IsolatedAsyncioTestCase):
self.assertFalse(SearchChain._ai_recommend_running) self.assertFalse(SearchChain._ai_recommend_running)
self.assertIsNone(SearchChain._ai_recommend_task) self.assertIsNone(SearchChain._ai_recommend_task)
async def test_recommend_task_is_owned_by_runtime_registry(self):
"""搜索推荐必须登记稳定 owner,并在宿主关停时取消阻塞中的 LLM 调用。"""
chain = self._make_chain()
registry = TaskRegistry()
started = asyncio.Event()
blocker = asyncio.Event()
async def blocked_recommend(_search_results_text: str) -> str:
"""模拟已进入外部 Agent、等待宿主关停取消的推荐调用。"""
started.set()
await blocker.wait()
return "[0]"
with (
patch.object(settings, "AI_AGENT_ENABLE", True, create=True),
patch.object(settings, "AI_RECOMMEND_ENABLED", True, create=True),
patch.object(settings, "AI_RECOMMEND_MAX_ITEMS", 50, create=True),
patch.object(
SearchChain,
"_invoke_recommend_llm",
new=AsyncMock(side_effect=blocked_recommend),
),
patch(
"app.chain.search.get_task_registry",
return_value=registry,
),
):
chain.start_recommend_task(
filtered_indices=None,
search_results_count=1,
results=[_make_result("item-0", 1024, 1)],
)
await asyncio.wait_for(started.wait(), timeout=1)
task = SearchChain._ai_recommend_task
self.assertEqual(
[record.owner for record in registry.records],
["chain.search.ai_recommend"],
)
self.assertTrue(await registry.shutdown(timeout_seconds=1))
self.assertIsNotNone(task)
self.assertTrue(task.done())
self.assertFalse(SearchChain._ai_recommend_running)
self.assertIsNone(SearchChain._ai_recommend_task)
async def test_invoke_recommend_llm_disables_output_message_persistence(self): async def test_invoke_recommend_llm_disables_output_message_persistence(self):
chain = self._make_chain() chain = self._make_chain()
from app.agent.prompt import prompt_manager from app.agent.prompt import prompt_manager