fix(worker): #1970 注册 AI 标签 Celery 任务(worker.tag_atom_clip unregistered) #1987

Merged
auto-approve-bot merged 1 commits from fix/1970-register-ai-tag-tasks into develop 2026-09-19 08:42:09 +08:00
2 changed files with 86 additions and 0 deletions
+4
View File
@@ -28,6 +28,10 @@ celery_app.conf.imports = (
"worker_app.tasks.health",
"worker_app.tasks.ingest",
"worker_app.tasks.atom_clips",
# #1970 片段级 AI 标签:必须显式 import 注册,否则 worker 报
# "Received unregistered task of type 'worker.tag_atom_clip'"
"worker_app.tasks.atom_clip_tagging",
"worker_app.tasks.backfill_atom_clip_tags",
"worker_app.tasks.classification",
"worker_app.tasks.generation",
"worker_app.tasks.voice_extraction",
@@ -0,0 +1,82 @@
"""#1970 AI 标签 Celery 任务注册回归测试。
背景:staging 上 worker.generate_atom_clips 正常派发 tag_atom_clip,
但消费端报 "Received unregistered task of type 'worker.tag_atom_clip'",
根因是 celery_app.conf.imports 漏列任务模块,worker 进程从未 import 之。
注意:tests/unit 下大量旧测试在 import 期向 sys.modules 注入
worker_app.celery_app 的 MagicMock 且不还原,全量收集时会污染本测试,
因此这里用 AST 静态解析 + 隔离子进程验证,不依赖 sys.modules 状态。
"""
from __future__ import annotations
import ast
import os
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
CELERY_APP_PY = REPO_ROOT / "apps" / "worker" / "worker_app" / "celery_app.py"
REQUIRED_MODULES = (
"worker_app.tasks.atom_clip_tagging",
"worker_app.tasks.backfill_atom_clip_tags",
)
def _conf_imports_values() -> set[str]:
"""从 celery_app.py AST 中提取 celery_app.conf.imports 元组的字符串项。"""
tree = ast.parse(CELERY_APP_PY.read_text(encoding="utf-8"))
values: set[str] = set()
for node in ast.walk(tree):
if not (isinstance(node, ast.Assign) and len(node.targets) == 1):
continue
target = node.targets[0]
# celery_app.conf.imports = (...) 或 conf.imports = (...)
if not (isinstance(target, ast.Attribute) and target.attr == "imports"):
continue
if isinstance(node.value, (ast.Tuple, ast.List)):
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
values.add(elt.value)
return values
def test_ai_tag_modules_in_celery_imports():
imports = _conf_imports_values()
for module in REQUIRED_MODULES:
assert module in imports, f"{module} 未加入 celery_app.conf.imports"
def test_ai_tag_tasks_registered_in_isolated_process():
"""隔离子进程(无 conftest / 无 sys.modules mock)真实加载 Celery app。"""
# 模拟 worker 启动时按 conf.imports import 任务模块的行为;
# 只导入 AI 标签两个模块(其他模块依赖 cv2 等本地未安装的重依赖)。
code = (
"import importlib, sys; "
"from worker_app.celery_app import celery_app; "
"mods = [m for m in celery_app.conf.imports or () "
"if 'atom_clip_tagging' in m or 'backfill_atom_clip_tags' in m]; "
"[importlib.import_module(m) for m in mods]; "
"missing = [n for n in "
"['worker.tag_atom_clip', 'worker.backfill_atom_clip_tags'] "
"if n not in celery_app.tasks]; "
"sys.exit(1 if missing or len(mods) < 2 else 0)"
)
env = os.environ.copy()
paths = [
str(REPO_ROOT),
str(REPO_ROOT / "apps" / "worker"),
str(REPO_ROOT / "packages"),
]
env["PYTHONPATH"] = os.pathsep.join(paths) + os.pathsep + env.get("PYTHONPATH", "")
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
env=env,
timeout=60,
)
assert result.returncode == 0, "隔离子进程中任务未注册成功:\n" f"stdout={result.stdout}\nstderr={result.stderr}"