| """์ด์์(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 |
|
|
| |
| 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) |
| ) |
|
|
| |
| 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, |
| } |
|
|