leechard / app /services /admin.py
nenae18's picture
Deploy LeeChard
5d3c2a9 verified
Raw
History Blame Contribute Delete
3.7 kB
"""์šด์˜์ž(admin) ์„œ๋น„์Šค โ€” dead-letter ์ˆ˜๋™ ์žฌ์ฒ˜๋ฆฌ.
์›๋ณธ์ด ์ด๋ฏธ ์‚ญ์ œ๋œ task ๋Š” ์žฌ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค(์žฌ์ดฌ์˜ ํ•„์š”). ๊ทธ ์™ธ์—๋Š” task ๋ฅผ
์ƒˆ dispatch ์„ธ๋Œ€๋กœ ์ดˆ๊ธฐํ™”ํ•ด QUEUED ๋กœ ๋˜๋Œ๋ฆฌ๊ณ  outbox ์ด๋ฒคํŠธ + ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค.
"""
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import update
from sqlalchemy.orm import Session
from app.domain.states import TaskStatus
from app.models.photo_task import PhotoTask
from app.models.task_event import TaskEvent
from app.services.storage import StorageService, get_storage
from app.services.task_repo import ensure_dispatch_event
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def _original_available(task: PhotoTask, storage: StorageService) -> bool:
if task.original_deleted_at is not None:
return False
if not task.original_key:
return False
return storage.exists(task.original_key)
def manual_retry_dead_letter(
db: Session,
task_id: str,
*,
actor: str,
reason: str | None = None,
storage: StorageService | None = None,
) -> dict:
"""dead-letter task ๋ฅผ ์ˆ˜๋™ ์žฌ์ฒ˜๋ฆฌ. ๋ฐ˜ํ™˜ dict ์˜ status ๋กœ ๊ฒฐ๊ณผ ๊ตฌ๋ถ„.
- {"status": "retake_required", "error_code": "original_not_available_for_retry"}
- {"status": "requeued", "task_id", "retry_generation"}
"""
storage = storage or get_storage()
task = db.get(PhotoTask, task_id)
if task is None:
return {"status": "not_found", "task_id": task_id}
if task.dead_lettered_at is None:
return {"status": "not_dead_lettered", "task_id": task_id}
if not _original_available(task, storage):
return {
"status": "retake_required",
"task_id": task_id,
"error_code": "original_not_available_for_retry",
}
old_status = task.status
now = _utcnow()
new_generation = task.retry_generation + 1
new_dispatch_version = task.dispatch_version + 1 # lock_version ์€ reset ํ•˜์ง€ ์•Š์Œ
# ์ƒˆ dispatch ์„ธ๋Œ€๋กœ ์ „๋ฉด ์ดˆ๊ธฐํ™” (core UPDATE)
db.execute(
update(PhotoTask)
.where(PhotoTask.task_id == task_id)
.values(
status=TaskStatus.QUEUED,
attempt_count=0,
dead_lettered_at=None,
failure_code=None,
failure_message=None,
failure_stage=None,
error_code=None,
error_detail=None,
locked_by=None,
locked_until=None,
lock_token=None,
current_attempt_id=None,
celery_job_id=None,
next_retry_at=now,
retry_generation=new_generation,
manual_retry_count=PhotoTask.manual_retry_count + 1,
dispatch_version=PhotoTask.dispatch_version + 1,
updated_at=now,
)
.execution_options(synchronize_session=False)
)
# ์žฌdispatch ์šฉ outbox(์ƒˆ dispatch_version dedupe_key, ๋ฉฑ๋“ฑ) + ๊ฐ์‚ฌ ๋กœ๊ทธ
ensure_dispatch_event(
db, task_id, new_dispatch_version, next_retry_at=now, rearm_published=True
)
db.add(
TaskEvent(
task_id=task_id,
actor=actor,
reason=reason,
old_status=old_status.value if hasattr(old_status, "value") else str(old_status),
new_status=TaskStatus.QUEUED.value,
retry_generation=new_generation,
dispatch_version=new_dispatch_version,
)
)
db.commit()
return {
"status": "requeued",
"task_id": task_id,
"retry_generation": new_generation,
"dispatch_version": new_dispatch_version,
}