From ba193cce0885b77a9dd4fda7fdc82154067e9ae5 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 23 Aug 2026 15:29:42 +0800 Subject: [PATCH] fix(imdb): register async cache cleanup --- app/modules/imdb/api.py | 13 ++++++-- .../adr/0007-background-action-reliability.md | 3 ++ .../backend-architecture-next-stage.md | 2 ++ .../architecture/dependency-baseline.json | 5 +-- tests/test_imdb_module.py | 33 +++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/app/modules/imdb/api.py b/app/modules/imdb/api.py index 912a31fc7..428dbd313 100644 --- a/app/modules/imdb/api.py +++ b/app/modules/imdb/api.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError from app.adapters.network.http import AsyncRequestUtils, RequestUtils from app.runtime.cache import cached from app.runtime.settings import RuntimeSettingsCompat +from app.runtime.tasks import get_task_registry settings = RuntimeSettingsCompat() from app.runtime.log import logger @@ -339,11 +340,19 @@ class ImdbApi: self._cached_get_json.cache_clear() self._cached_graphql.cache_clear() try: - loop = asyncio.get_running_loop() + asyncio.get_running_loop() except RuntimeError: asyncio.run(self.async_clear_cache()) else: - loop.create_task(self.async_clear_cache()) + # 同步 ABI 不能改成 async;运行中的循环交给宿主登记器收口,避免清理任务悬挂。 + try: + get_task_registry().create( + self.async_clear_cache(), + owner="module.imdb.cache_clear", + ) + except RuntimeError: + # 兼容宿主已经进入关停阶段的同步调用:同步缓存已清理,异步缓存无需再启动新任务。 + return async def async_clear_cache(self) -> None: """清理 IMDb 异步 GET 与 GraphQL 请求缓存区。""" diff --git a/docs/adr/0007-background-action-reliability.md b/docs/adr/0007-background-action-reliability.md index b7ee7080e..f17713380 100644 --- a/docs/adr/0007-background-action-reliability.md +++ b/docs/adr/0007-background-action-reliability.md @@ -83,6 +83,9 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同 `api.anthropic.stream` 登记并在 lifespan shutdown 时取消;它们仍是 E0 请求交付,不提供跨重启恢复。 - stdio MCP 的 stderr reader 属于会话资源内部任务;会话退出时先取消并等待 reader 收口,再终止子进程,避免 资源已释放而 reader 仍悬挂。 +- IMDb 同步 `clear_cache()` ABI 在事件循环内触发的异步缓存清理登记为 + `module.imdb.cache_clear`;同步调用方式和无运行事件循环时的立即清理行为保持不变,宿主关停后不再 + 接受新的清理任务。 ### Transfer pending / 文件整理 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index fef49ebcc..3d3b44926 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -836,6 +836,8 @@ ADR 必须逐个映射当前 Event、BackgroundTasks、Scheduler job、Agent tas - OpenAI Chat Completions 与 Anthropic Messages 的流式 Agent 执行分别登记为 `api.openai.stream` 和 `api.anthropic.stream`。SSE payload、断线取消、临时会话清理和非流式入口保持 原语义;未启动完整 lifespan 的协议校验和旧直接调用通过兼容依赖回退到默认登记器。 +- IMDb 同步清缓存兼容入口在运行事件循环时改由 TaskRegistry 登记异步缓存清理任务,owner 为 + `module.imdb.cache_clear`;同步签名、模块调用方式和无事件循环时的立即清理语义保持不变。 #### ARCH-251:用现有数据库做首个 durable side-effect pilot diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 2eef8683f..766042d30 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -13,8 +13,8 @@ "runtime_to_db": [], "workflow_to_db": [] }, - "edge_count": 6477, - "edge_sha256": "41e51256026afbd3abc9684dd1beb653026be927cab14384df6ff6d248806972", + "edge_count": 6478, + "edge_sha256": "d1c26c72787d07b411f6e047863bfb135005be8d3cf2212f2f03e0d8e0326283", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -4322,6 +4322,7 @@ "app.modules.imdb.api -> app.runtime.cache", "app.modules.imdb.api -> app.runtime.log", "app.modules.imdb.api -> app.runtime.settings", + "app.modules.imdb.api -> app.runtime.tasks", "app.modules.indexer -> app.application", "app.modules.indexer -> app.application.site", "app.modules.indexer -> app.application.site.health", diff --git a/tests/test_imdb_module.py b/tests/test_imdb_module.py index 0a6d9c264..0466a29f9 100644 --- a/tests/test_imdb_module.py +++ b/tests/test_imdb_module.py @@ -20,6 +20,7 @@ from app.modules.imdb.api import ( ImdbTitle, ) from app.runtime.config import settings +from app.runtime.tasks import TaskRegistry from app.schemas.types import MediaRecognizeType, MediaSource, MediaType @@ -168,6 +169,38 @@ def test_imdb_graphql_error_is_not_cached() -> None: assert api._request.post_json.call_count == 2 +def test_imdb_clear_cache_registers_async_cleanup_in_running_loop() -> None: + """同步清缓存入口在异步宿主中应登记任务,并保留原同步调用约定。""" + + async def scenario() -> None: + """验证异步缓存清理直到完成前都由宿主登记器持有。""" + api = ImdbApi() + registry = TaskRegistry() + release = asyncio.Event() + + async def clear_async_cache() -> None: + """等待测试释放,确保可以观察登记中的任务。""" + await release.wait() + + with ( + patch("app.modules.imdb.api.get_task_registry", return_value=registry), + patch.object(api, "async_clear_cache", side_effect=clear_async_cache), + ): + assert api.clear_cache() is None + assert [record.owner for record in registry.records] == [ + "module.imdb.cache_clear" + ] + + release.set() + await registry.records[0].task + await asyncio.sleep(0) + + assert registry.records == () + api.close() + + asyncio.run(scenario()) + + def test_async_imdb_api_merges_paginated_episodes() -> None: """IMDb 异步剧集查询应使用 GraphQL 游标合并并缓存所有分页。""" api = ImdbApi()