CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
FE↔后端联调实测暴露并修复: 1. 问卷键名三处错位(FE height_cm/weight_kg/psych_style vs 文档 height/weight/psyche_style)——vision._bmi 多键取值、diagnosis 维度 装配与规则上下文双键或运算,两侧谁改键名都不再炸 2. celery worker asyncio.run 每投递起新 loop + 模块级引擎缓存 → asyncpg 连接跨 loop(同进程第 3 个报告任务必现 RuntimeError: attached to a different loop;集成测试单循环掩盖)。新增 apps/worker/loop.py 持久 loop,report/ping 两任务接入 E2E 终态: psyche_style/height_weight 恢复、BMI 体型确定值、ok 26/32、 真实 LLM 文案通过校验 degraded=False。
70 lines
2.6 KiB
Python
Executable File
70 lines
2.6 KiB
Python
Executable File
"""Celery task definitions.
|
|
|
|
Only the scaffold's ``ping`` task lives here. It proves the whole async path:
|
|
API creates ``tk_task`` → queue → worker transitions running → succeeded →
|
|
task centre reflects it. Real long-running tasks (report / tryon / matting /
|
|
render) follow exactly this shape and additionally call
|
|
``billing.commit()``/``refund()`` and ``compliance.publish_asset()``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, cast
|
|
|
|
from apps.worker import loop as worker_loop
|
|
from apps.worker.celery_app import celery_app
|
|
from packages.domain.task.state import TaskStatus
|
|
from packages.shared.context import bind_context
|
|
from packages.shared.db import get_session_factory
|
|
from packages.shared.logging import get_logger
|
|
from packages.task.service import TaskService
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
async def _run_ping(task_id: int, echo: str | None = None) -> dict[str, Any]:
|
|
factory = get_session_factory()
|
|
async with factory() as session:
|
|
service = TaskService(session)
|
|
task = await service.transition(
|
|
task_id=task_id, target=TaskStatus.RUNNING, stage_text="执行中"
|
|
)
|
|
payload = task.payload or {}
|
|
message = echo or str(payload.get("echo", "pong"))
|
|
await session.commit()
|
|
|
|
finished = await service.transition(
|
|
task_id=task_id,
|
|
target=TaskStatus.SUCCEEDED,
|
|
stage_text="已完成",
|
|
result={"echo": message},
|
|
)
|
|
await session.commit()
|
|
return {"task_id": str(finished.task_id), "echo": message}
|
|
|
|
|
|
async def _fail_ping(task_id: int, error: str) -> None:
|
|
factory = get_session_factory()
|
|
async with factory() as session:
|
|
service = TaskService(session)
|
|
await service.transition(
|
|
task_id=task_id,
|
|
target=TaskStatus.FAILED,
|
|
stage_text="执行失败",
|
|
error_message=error,
|
|
)
|
|
await session.commit()
|
|
|
|
|
|
# Celery 的 task 装饰器没有类型标注,会把被装饰函数擦成 untyped
|
|
@celery_app.task(name="meida.task.ping", bind=True, max_retries=2) # type: ignore[untyped-decorator]
|
|
def ping_task(self: Any, task_id: int, echo: str | None = None) -> dict[str, Any]:
|
|
bind_context(task_id=task_id)
|
|
try:
|
|
return cast("dict[str, Any]", worker_loop.run(_run_ping(task_id, echo)))
|
|
except Exception as exc:
|
|
logger.exception("ping_task_failed", task_id=task_id)
|
|
# Real tasks additionally refund the quota reservation here (FR-BILL-05).
|
|
worker_loop.run(_fail_ping(task_id, f"{type(exc).__name__}: {exc}"))
|
|
raise
|