修复生辰任务恢复机制BUG|修复生成视频/图片token快照不回落交易流水BUG

This commit is contained in:
2026-06-26 11:51:02 +08:00
parent f3675924dc
commit 06cdab9cbc
12 changed files with 773 additions and 125 deletions
@@ -12,6 +12,7 @@ from app.services.error_codes import extract_error_message
from app.services.generation_log_service import log_task_event
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
from app.services.generation_provider_service import create_provider_task
from app.services.media_token_usage_snapshot_service import sync_chat_generation_task_media_token_snapshot
from app.services.redis_registry_service import ensure_aware_utc
from app.tasks.celery_app import celery_app
@@ -223,6 +224,7 @@ async def _run(task_id: str):
ensure_ascii=False,
default=str,
)
await sync_chat_generation_task_media_token_snapshot(db, task, provider_response=task.provider_response_json)
if task.remote_result_url and not task.seedance_task_id:
# 同步图片路径:原 SDK 已经返回最终 URL。
@@ -1,11 +1,24 @@
from __future__ import annotations
from app.tasks.async_runner import run_async
from datetime import datetime, timezone, timedelta
import errno
import math
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.enums.generation_task import (
ALLOWED_GENERATION_MODES,
ChatGenerationPipelineStage,
ChatGenerationTaskEventType,
ChatGenerationTaskStatus,
GenerationType,
)
from app.models.base import async_session
from app.models.chat_generation_task import ChatGenerationTask
from app.services.celery_download_recovery_service import (
@@ -18,17 +31,17 @@ from app.services.error_codes import extract_error_message
from app.services.generation_download_service import download_generation_result
from app.services.generation_log_service import log_task_event
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
from app.services.media_token_usage_snapshot_service import sync_chat_generation_task_media_token_snapshot
from app.services.resource_accounting_service import record_chat_task_generated_resource
from app.tasks.celery_app import celery_app
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"}
DOWNLOAD_QUEUE = "gen_result_download"
DOWNLOAD_STAGE_QUEUED = "download_queued"
DOWNLOAD_STAGE_DOWNLOADING = "downloading"
DOWNLOAD_STAGE_RETRY_WAITING = "retry_waiting"
DOWNLOAD_STAGE_DONE = "done"
DOWNLOAD_STAGE_FAILED = "download_failed"
DOWNLOAD_STAGE_QUEUED = ChatGenerationPipelineStage.DOWNLOAD_QUEUED.value
DOWNLOAD_STAGE_DOWNLOADING = ChatGenerationPipelineStage.DOWNLOADING.value
DOWNLOAD_STAGE_RETRY_WAITING = ChatGenerationPipelineStage.RETRY_WAITING.value
DOWNLOAD_STAGE_DONE = ChatGenerationPipelineStage.DONE.value
DOWNLOAD_STAGE_FAILED = ChatGenerationPipelineStage.DOWNLOAD_FAILED.value
RESULT_READY_STAGE = ChatGenerationPipelineStage.RESULT_READY.value
def _now() -> datetime:
@@ -56,6 +69,15 @@ def _retry_at(attempt: int, now: datetime | None = None) -> datetime:
return now + timedelta(seconds=max(1, base * max(1, attempt)))
def _countdown_until(value: datetime | None, *, minimum: int = 1) -> int:
target = ensure_aware_utc(value)
if target is None:
return minimum
extra = int(getattr(settings, "DOWNLOAD_RETRY_COUNTDOWN_EXTRA_SECONDS", 1) or 0)
seconds = (target - _now()).total_seconds()
return max(minimum, math.ceil(seconds) + extra)
def _is_expired(value: datetime | None, now: datetime | None = None) -> bool:
value = ensure_aware_utc(value)
if value is None:
@@ -64,10 +86,10 @@ def _is_expired(value: datetime | None, now: datetime | None = None) -> bool:
def _is_already_completed(task: ChatGenerationTask) -> bool:
if task.status == "completed" or task.pipeline_stage == DOWNLOAD_STAGE_DONE:
if task.gen_type == "image" and task.image_url:
if task.status == ChatGenerationTaskStatus.COMPLETED.value or task.pipeline_stage == DOWNLOAD_STAGE_DONE:
if task.gen_type == GenerationType.IMAGE.value and task.image_url:
return True
if task.gen_type == "video" and task.video_url:
if task.gen_type == GenerationType.VIDEO.value and task.video_url:
return True
return False
@@ -77,6 +99,61 @@ def _build_celery_task_id(task_id: str, attempt: int | None = None, reason: str
return f"download:{task_id}:{int(attempt or 0)}:{safe_reason}:{uuid.uuid4().hex[:12]}"
def _is_non_retryable_download_error(exc: Exception) -> bool:
if not bool(getattr(settings, "DOWNLOAD_NON_RETRYABLE_LOCAL_ERRORS", True)):
return False
if isinstance(exc, PermissionError):
return True
if isinstance(exc, OSError) and getattr(exc, "errno", None) in {errno.EACCES, errno.EPERM, errno.ENOSPC, errno.EROFS, errno.ENAMETOOLONG}:
return True
message = str(exc).lower()
non_retryable_fragments = (
"permission denied",
"no space left on device",
"read-only file system",
"file name too long",
"invalid argument",
)
return any(fragment in message for fragment in non_retryable_fragments)
async def _log_download_event(
task: ChatGenerationTask | None = None,
*,
task_id: str | None = None,
event_type: ChatGenerationTaskEventType | str,
from_status: str | None = None,
to_status: str | None = None,
from_stage: str | None = None,
to_stage: str | None = None,
message: str | None = None,
detail: Any = None,
) -> None:
if not bool(getattr(settings, "DOWNLOAD_EVENT_VERBOSE_ENABLED", True)):
# 成功/失败关键事件仍保留;只关闭 verbose skip 事件。
critical = {
ChatGenerationTaskEventType.DOWNLOAD_START.value,
ChatGenerationTaskEventType.DOWNLOAD_SUCCESS.value,
ChatGenerationTaskEventType.DOWNLOAD_FAILED.value,
ChatGenerationTaskEventType.DOWNLOAD_FAILED_NON_RETRYABLE.value,
ChatGenerationTaskEventType.DOWNLOAD_RETRY_WAITING.value,
}
event_value = event_type.value if hasattr(event_type, "value") else str(event_type)
if event_value not in critical:
return
await log_task_event(
task,
task_id=task_id,
event_type=event_type.value if hasattr(event_type, "value") else str(event_type),
from_status=from_status,
to_status=to_status,
from_stage=from_stage,
to_stage=to_stage,
message=message,
detail=detail,
)
async def _register_active_from_task(
task: ChatGenerationTask,
*,
@@ -101,6 +178,62 @@ async def _register_active_from_task(
await upsert_download_active(record_id=task.id, payload=payload, check_at=check_at)
async def _apply_download_async(
task: ChatGenerationTask,
*,
priority: int,
countdown: int | None,
reason: str,
event_type: ChatGenerationTaskEventType,
failed_event_type: ChatGenerationTaskEventType,
) -> bool:
if not celery_app:
await _log_download_event(
task,
event_type=failed_event_type,
message="Celery 未启用,下载任务无法投递",
detail={"queue": DOWNLOAD_QUEUE, "priority": priority, "countdown": countdown, "reason": reason},
)
return False
try:
download_generation_result_task.apply_async(
args=[task.id],
queue=DOWNLOAD_QUEUE,
priority=priority,
countdown=countdown,
task_id=task.download_celery_task_id,
)
await _log_download_event(
task,
event_type=event_type,
to_stage=task.pipeline_stage,
detail={
"queue": DOWNLOAD_QUEUE,
"priority": priority,
"countdown": countdown,
"download_celery_task_id": task.download_celery_task_id,
"reason": reason,
"attempt": task.download_attempt_count,
},
)
return True
except Exception as exc:
await _log_download_event(
task,
event_type=failed_event_type,
message=str(exc),
detail={
"queue": DOWNLOAD_QUEUE,
"priority": priority,
"countdown": countdown,
"download_celery_task_id": task.download_celery_task_id,
"reason": reason,
"attempt": task.download_attempt_count,
},
)
return False
async def enqueue_download_task(
db: AsyncSession,
task: ChatGenerationTask,
@@ -110,11 +243,17 @@ async def enqueue_download_task(
countdown: int | None = None,
) -> str | None:
"""统一投递图片/视频下载任务,并同步 DB + Redis active 注册表。"""
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
reason = reason or ("recover" if recover else "normal")
if not task:
return None
if task.status != "generating":
if task.generation_mode not in ALLOWED_GENERATION_MODES:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_INVALID_MODE, message="不支持的 generation_mode", detail={"reason": reason})
return None
if task.status != ChatGenerationTaskStatus.GENERATING.value:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_NOT_GENERATING, message="任务不是 generating 状态", detail={"reason": reason, "status": task.status})
return None
if not task.remote_result_url:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_NO_REMOTE_RESULT_URL, message="缺少 remote_result_url", detail={"reason": reason})
return None
now = _now()
@@ -122,9 +261,10 @@ async def enqueue_download_task(
celery_task_id = _build_celery_task_id(
task.id,
attempt=task.download_attempt_count or task.retry_count or 0,
reason=reason or ("recover" if recover else "normal"),
reason=reason,
)
old_stage = task.pipeline_stage
task.pipeline_stage = DOWNLOAD_STAGE_QUEUED
task.download_celery_task_id = celery_task_id
task.download_enqueued_at = now
@@ -137,13 +277,22 @@ async def enqueue_download_task(
check_at = _queue_timeout_at(now)
await _register_active_from_task(task, check_at=check_at, priority=priority, reason=reason)
if celery_app:
download_generation_result_task.apply_async(
args=[task.id],
queue=DOWNLOAD_QUEUE,
priority=priority,
countdown=countdown,
task_id=celery_task_id,
await _apply_download_async(
task,
priority=priority,
countdown=countdown,
reason=reason,
event_type=ChatGenerationTaskEventType.DOWNLOAD_RECOVERY_ENQUEUE if recover else ChatGenerationTaskEventType.DOWNLOAD_ENQUEUE,
failed_event_type=ChatGenerationTaskEventType.DOWNLOAD_RECOVERY_ENQUEUE_FAILED if recover else ChatGenerationTaskEventType.DOWNLOAD_ENQUEUE_FAILED,
)
if old_stage != DOWNLOAD_STAGE_QUEUED:
# 独立记录阶段变化的上下文,便于和真正投递事件对照。
await _log_download_event(
task,
event_type=ChatGenerationTaskEventType.DOWNLOAD_ENQUEUE,
from_stage=old_stage,
to_stage=DOWNLOAD_STAGE_QUEUED,
detail={"reason": reason, "recover": recover, "check_at": check_at},
)
return celery_task_id
@@ -158,34 +307,81 @@ async def _reload_task(db: AsyncSession, task_id: str) -> ChatGenerationTask | N
return result.scalar_one_or_none()
async def _reschedule_not_due_retry(task: ChatGenerationTask, *, now: datetime) -> None:
countdown = _countdown_until(task.download_next_retry_at)
await _register_active_from_task(
task,
check_at=ensure_aware_utc(task.download_next_retry_at) or (now + timedelta(seconds=countdown)),
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
reason="retry_waiting_not_due",
)
await _apply_download_async(
task,
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
countdown=countdown,
reason="retry_waiting_not_due",
event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_ENQUEUE,
failed_event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_ENQUEUE_FAILED,
)
async def _claim_download_lease(db: AsyncSession, task: ChatGenerationTask) -> bool:
now = _now()
if not task or task.generation_mode not in ALLOWED_GENERATION_MODES:
if not task:
return False
if task.status != "generating":
if task.generation_mode not in ALLOWED_GENERATION_MODES:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_INVALID_MODE, message="下载任务跳过:不支持的 generation_mode")
return False
if task.status != ChatGenerationTaskStatus.GENERATING.value:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_NOT_GENERATING, message="下载任务跳过:任务不是 generating 状态", detail={"status": task.status, "stage": task.pipeline_stage})
return False
if _is_already_completed(task):
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_ALREADY_COMPLETED, message="下载任务跳过:任务已完成", detail={"status": task.status, "stage": task.pipeline_stage})
return False
if not task.remote_result_url:
await _log_download_event(task, event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_NO_REMOTE_RESULT_URL, message="下载任务跳过:缺少 remote_result_url")
return False
stage = task.pipeline_stage
if stage == DOWNLOAD_STAGE_DOWNLOADING:
if not _is_expired(task.download_lease_until, now):
await _log_download_event(
task,
event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_DOWNLOADING_LEASE_ALIVE,
message="下载任务跳过:已有 downloading lease 且未过期",
detail={"lease_until": task.download_lease_until, "download_celery_task_id": task.download_celery_task_id},
)
return False
await log_task_event(
await _log_download_event(
task,
event_type="DOWNLOAD_STUCK_RECOVER",
event_type=ChatGenerationTaskEventType.DOWNLOAD_STUCK_RECOVER,
message=f"downloading lease 已过期,重新抢占下载。lease_until={task.download_lease_until}",
)
elif stage == DOWNLOAD_STAGE_RETRY_WAITING:
if not _is_expired(task.download_next_retry_at, now):
await _log_download_event(
task,
event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_NOT_DUE,
message="下载重试提前触发,尚未到 next_retry_at,已重新投递延后重试",
detail={
"now": now,
"download_next_retry_at": task.download_next_retry_at,
"download_celery_task_id": task.download_celery_task_id,
},
)
await _reschedule_not_due_retry(task, now=now)
return False
elif stage in (DOWNLOAD_STAGE_QUEUED, "result_ready"):
elif stage in (DOWNLOAD_STAGE_QUEUED, RESULT_READY_STAGE):
pass
else:
await _log_download_event(
task,
event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_STAGE_NOT_ALLOWED,
message="下载任务跳过:当前阶段不允许下载",
detail={"stage": stage, "status": task.status, "download_celery_task_id": task.download_celery_task_id},
)
return False
old_stage = stage
@@ -207,9 +403,9 @@ async def _claim_download_lease(db: AsyncSession, task: ChatGenerationTask) -> b
reason="claim_download_lease",
)
await log_task_event(
await _log_download_event(
task,
event_type="DOWNLOAD_START",
event_type=ChatGenerationTaskEventType.DOWNLOAD_START,
from_stage=old_stage,
to_stage=DOWNLOAD_STAGE_DOWNLOADING,
detail={
@@ -244,9 +440,9 @@ async def _mark_retry_waiting(db: AsyncSession, task: ChatGenerationTask, exc: E
reason="download_retry_waiting",
)
await log_task_event(
await _log_download_event(
task,
event_type="DOWNLOAD_RETRY_WAITING",
event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_WAITING,
message=error_message,
to_stage=DOWNLOAD_STAGE_RETRY_WAITING,
detail={
@@ -262,16 +458,53 @@ def _should_final_fail(task: ChatGenerationTask) -> bool:
return int(task.download_attempt_count or task.retry_count or 0) >= int(settings.DOWNLOAD_TASK_MAX_ATTEMPTS or 3)
async def _mark_download_failed(
db: AsyncSession,
task: ChatGenerationTask,
*,
exc: Exception,
non_retryable: bool = False,
) -> None:
error_message = extract_error_message(exc, "下载") if callable(extract_error_message) else str(exc)
await mark_chat_generation_task_failed_and_refund_once(
db,
task=task,
error_message=error_message,
pipeline_stage=DOWNLOAD_STAGE_FAILED,
)
task.download_last_error = error_message
task.download_lease_until = None
task.download_next_retry_at = None
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
await notify_chat_generation_task_finished(db, task)
await db.commit()
await remove_download_active(task.id)
await _log_download_event(
task,
event_type=ChatGenerationTaskEventType.DOWNLOAD_FAILED_NON_RETRYABLE if non_retryable else ChatGenerationTaskEventType.DOWNLOAD_FAILED,
message=task.error_message,
detail={
"download_attempt_count": task.download_attempt_count,
"max_attempts": settings.DOWNLOAD_TASK_MAX_ATTEMPTS,
"non_retryable": non_retryable,
"download_last_error": task.download_last_error,
},
)
async def _run(task_id: str):
async with async_session() as db:
result = await db.execute(
select(ChatGenerationTask).where(
ChatGenerationTask.id == task_id,
ChatGenerationTask.deleted_at.is_(None),
).with_for_update().limit(1)
)
task = result.scalar_one_or_none()
task = await _reload_task(db, task_id)
if not task:
await _log_download_event(
task_id=task_id,
event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_TASK_MISSING,
message="下载任务跳过:ChatGenerationTask 不存在或已软删",
)
await remove_download_active(task_id)
return
claimed = await _claim_download_lease(db, task)
@@ -283,15 +516,21 @@ async def _run(task_id: str):
task = await _reload_task(db, task_id)
if not task:
await _log_download_event(
task_id=task_id,
event_type=ChatGenerationTaskEventType.DOWNLOAD_SKIP_TASK_MISSING,
message="下载完成后任务不存在或已软删",
)
await remove_download_active(task_id)
return
if task.gen_type == "image":
if task.gen_type == GenerationType.IMAGE.value:
task.image_url = downloaded.url
else:
task.video_url = downloaded.url
task.video_cover_url = downloaded.cover_url
task.status = "completed"
task.status = ChatGenerationTaskStatus.COMPLETED.value
task.pipeline_stage = DOWNLOAD_STAGE_DONE
task.generated_at = _now()
task.retry_count = 0
@@ -308,18 +547,18 @@ async def _run(task_id: str):
remote_url=task.remote_result_url,
generated_at=task.generated_at,
)
await sync_chat_generation_task_media_token_snapshot(db, task)
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
await notify_chat_generation_task_finished(db, task)
await db.commit()
await remove_download_active(task.id)
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
await notify_chat_generation_task_finished(db, task)
await db.commit()
await log_task_event(
await _log_download_event(
task,
event_type="DOWNLOAD_SUCCESS",
to_status="completed",
event_type=ChatGenerationTaskEventType.DOWNLOAD_SUCCESS,
to_status=ChatGenerationTaskStatus.COMPLETED.value,
to_stage=DOWNLOAD_STAGE_DONE,
detail={
"resource_url": downloaded.url,
@@ -337,48 +576,23 @@ async def _run(task_id: str):
task = await _reload_task(db, task_id)
if not task:
await remove_download_active(task_id)
return
if _should_final_fail(task):
error_message = extract_error_message(exc, "下载") if callable(extract_error_message) else str(exc)
await mark_chat_generation_task_failed_and_refund_once(
db,
task=task,
error_message=error_message,
pipeline_stage=DOWNLOAD_STAGE_FAILED,
)
task.download_last_error = error_message
task.download_lease_until = None
task.download_next_retry_at = None
await db.commit()
await remove_download_active(task.id)
from app.services.generation_module_hook_service import notify_chat_generation_task_finished
await notify_chat_generation_task_finished(db, task)
await db.commit()
await log_task_event(
task,
event_type="DOWNLOAD_FAILED",
message=task.error_message,
detail={
"download_attempt_count": task.download_attempt_count,
"max_attempts": settings.DOWNLOAD_TASK_MAX_ATTEMPTS,
},
)
non_retryable = _is_non_retryable_download_error(exc)
if non_retryable or _should_final_fail(task):
await _mark_download_failed(db, task, exc=exc, non_retryable=non_retryable)
else:
next_retry_at = await _mark_retry_waiting(db, task, exc)
if celery_app:
delay_seconds = max(1, int((next_retry_at - _now()).total_seconds()))
download_generation_result_task.apply_async(
args=[task.id],
queue=DOWNLOAD_QUEUE,
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
countdown=delay_seconds,
task_id=task.download_celery_task_id,
)
delay_seconds = _countdown_until(next_retry_at)
await _apply_download_async(
task,
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
countdown=delay_seconds,
reason="download_exception_retry",
event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_ENQUEUE,
failed_event_type=ChatGenerationTaskEventType.DOWNLOAD_RETRY_ENQUEUE_FAILED,
)
if celery_app:
@@ -12,6 +12,7 @@ from app.services.error_codes import extract_error_message
from app.services.generation_log_service import log_task_event, log_provider_call
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
from app.services.generation_provider_service import poll_provider_task
from app.services.media_token_usage_snapshot_service import sync_chat_generation_task_media_token_snapshot
from app.services.redis_registry_service import (
datetime_to_epoch,
ensure_aware_utc,
@@ -245,6 +246,7 @@ async def _run(task_id: str):
task.video_tokens_used = poll_result.get("video_tokens", 0) or 0
task.provider_response_json = response_data
await sync_chat_generation_task_media_token_snapshot(db, task, provider_response=response_data)
if not task.remote_result_url:
await _mark_failed(db, task, message="供应商任务成功但未返回结果URL", detail=poll_result)
@@ -1,12 +1,17 @@
# app/tasks/generation_recovery_tasks.py
from __future__ import annotations
import logging
from typing import Any, Dict
from app.config import settings
from app.models.base import async_session
from app.services.redis_registry_service import get_registry_redis, redis_acquire_lock
from app.tasks.async_runner import run_async
from app.tasks.celery_app import celery_app
logger = logging.getLogger("video_gen")
async def _run_download_once() -> Dict[str, Any]:
from app.services.generation_recovery_service import recover_download_tasks_once
@@ -22,11 +27,49 @@ async def _run_generation_once() -> Dict[str, Any]:
return await recover_generation_tasks_once(db)
async def _acquire_download_recovery_loop_lock() -> tuple[bool, str]:
"""下载恢复循环锁。
Redis 不可用时降级为直接执行 DB fallback,避免恢复能力彻底失效;
Redis 可用但锁被其他 worker 持有时,本轮跳过,不再重复投递下一轮。
"""
redis = await get_registry_redis()
if redis is None:
return True, "redis_unavailable_run_db_fallback"
token = await redis_acquire_lock(
lock_key=settings.DOWNLOAD_RECOVERY_LOOP_LOCK_KEY,
ttl_seconds=int(settings.DOWNLOAD_RECOVERY_LOOP_LOCK_TTL_SECONDS or 55),
log_context="download_recovery_loop",
)
return (bool(token), "lock_acquired" if token else "lock_held")
def _schedule_next_download_recovery_loop() -> None:
if not celery_app or not bool(getattr(settings, "DOWNLOAD_RECOVERY_LOOP_ENABLED", True)):
return
try:
recover_download_tasks_once.apply_async(
countdown=max(1, int(settings.DOWNLOAD_RECOVERY_INTERVAL_SECONDS or 60)),
queue="gen_result_download",
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
)
except Exception:
logger.exception("下载恢复循环下一轮投递失败")
if celery_app:
@celery_app.task(name="generation.recover_download_tasks_once")
def recover_download_tasks_once() -> Dict[str, Any]:
return run_async(_run_download_once())
@celery_app.task(name="generation.recover_download_tasks_once", bind=True)
def recover_download_tasks_once(self) -> Dict[str, Any]:
acquired, reason = run_async(_acquire_download_recovery_loop_lock())
if not acquired:
return {"skipped": reason}
try:
result = run_async(_run_download_once())
result["loop_lock"] = reason
return result
finally:
_schedule_next_download_recovery_loop()
@celery_app.task(name="generation.recover_generation_tasks_once")