"""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 import asyncio from typing import Any 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 asyncio.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). asyncio.run(_fail_ping(task_id, f"{type(exc).__name__}: {exc}")) raise