Download app/services/dispatch.py from nenae18/leechard: direct link, hf CLI and curl.
- Browser
- Download file 6.73 kB
-
https://huggingface.co/spaces/nenae18/leechard/resolve/main/app/services/dispatch.py
- Command line
-
hf download hf://spaces/nenae18/leechard/app/services/dispatch.py
-
curl -L -o dispatch.py https://huggingface.co/spaces/nenae18/leechard/resolve/main/app/services/dispatch.py
6.73 kB
| """Enqueue ๋์คํจ์น + ์คํจ ๋ณต๊ตฌ (next_retry_at ๊ฒ์ดํธ ์กด์ค). | |
| - relay_outbox: ๋ฐํ ๋์ outbox ์ด๋ฒคํธ๋ฅผ enqueue. next_retry_at ๊ณผ task ์ ๊ฒฉ์ฑ์ ํ์ธ. | |
| - repair_stuck_queued: ์ง์ enqueue ํ์ง ์๋๋ค. QUEUED ์ธ๋ฐ ๋ฏธ๋ฐํ outbox ๊ฐ ์๋ task ์ | |
| outbox ์ด๋ฒคํธ๋ง ๋ณด์ถฉ(next_retry_at ์น๊ณ). ์ค์ dispatch ๋ relay_outbox ๊ฐ ๋ด๋น. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from collections.abc import Callable | |
| from datetime import datetime, timezone | |
| from typing import Any | |
| from sqlalchemy import update | |
| from sqlalchemy.orm import Session | |
| from app.domain.states import TaskStatus | |
| from app.models.outbox import OutboxEvent | |
| from app.models.photo_task import PhotoTask | |
| from app.services import task_repo | |
| logger = logging.getLogger(__name__) | |
| EnqueueFn = Callable[[str, int], Any] | |
| def _utcnow() -> datetime: | |
| return datetime.now(timezone.utc) | |
| def _aware(dt: datetime | None) -> datetime | None: | |
| """sqlite ๋ tz ๋ฅผ ์์ naive ๋ฅผ ๋๋ ค์ค๋ค โ UTC ๋ก ๊ฐ์ฃผํด ๋น๊ต ์์ ์ฑ ํ๋ณด.""" | |
| if dt is not None and dt.tzinfo is None: | |
| return dt.replace(tzinfo=timezone.utc) | |
| return dt | |
| def default_enqueue(task_id: str, dispatch_version: int = 0) -> Any: | |
| """์ค์ Celery enqueue. payload ์ dispatch_version ์ ์ค์ด stale dispatch ๋ฅผ ์ฐจ๋จ.""" | |
| from app.workers.tasks import process_photo | |
| return process_photo.delay(task_id, dispatch_version) | |
| def _job_id(async_result: Any) -> str | None: | |
| return getattr(async_result, "id", None) | |
| def mark_published(db: Session, task_id: str) -> None: | |
| """ํด๋น task ์ ๋ฏธ๋ฐํ outbox ์ด๋ฒคํธ๋ฅผ ๋ฐํ ์๋ฃ๋ก ๋งํน(upload ์ฑ๊ณต ์งํ).""" | |
| now = _utcnow() | |
| events = ( | |
| db.query(OutboxEvent) | |
| .filter(OutboxEvent.task_id == task_id, OutboxEvent.published.is_(False)) | |
| .all() | |
| ) | |
| for event in events: | |
| event.published = True | |
| event.status = "PUBLISHED" | |
| event.published_at = now | |
| event.attempts += 1 | |
| if events: | |
| db.commit() | |
| def _task_dispatchable(task: PhotoTask | None, now: datetime) -> bool: | |
| """photo_task ๊ฐ ์ง๊ธ dispatch ๊ฐ๋ฅํ ์ํ์ธ์ง(์ฌ์๋ ๊ฒ์ดํธ ํฌํจ).""" | |
| if task is None: | |
| return False | |
| if task.status != TaskStatus.QUEUED: | |
| return False | |
| if task.dead_lettered_at is not None: | |
| return False | |
| locked_until = _aware(task.locked_until) | |
| if locked_until is not None and locked_until >= now: | |
| return False | |
| next_retry_at = _aware(task.next_retry_at) | |
| if next_retry_at is not None and next_retry_at > now: | |
| return False | |
| return True | |
| def relay_outbox( | |
| db: Session, | |
| *, | |
| enqueue: EnqueueFn = default_enqueue, | |
| limit: int = 100, | |
| now: datetime | None = None, | |
| ) -> int: | |
| """๋ฏธ๋ฐํ outbox ์ด๋ฒคํธ๋ฅผ enqueue. ๊ฒ์ดํธ ๋ฏธํต๊ณผ ์ด๋ฒคํธ๋ ๊ทธ๋๋ก ๋จ๊ธด๋ค. | |
| ์ด๋ฒคํธ ๊ฒ์ดํธ: published=False AND (next_retry_at IS NULL OR <= now) | |
| task ๊ฒ์ดํธ: _task_dispatchable() (QUEUED, dead-letter ์๋, lock ๋ง๋ฃ, next_retry ๋๋ฌ) | |
| """ | |
| now = now or _utcnow() | |
| events = ( | |
| db.query(OutboxEvent) | |
| .filter( | |
| OutboxEvent.status.in_(("PENDING", "FAILED")), | |
| OutboxEvent.published.is_(False), | |
| (OutboxEvent.next_retry_at.is_(None)) | |
| | (OutboxEvent.next_retry_at <= now), | |
| ) | |
| .order_by(OutboxEvent.created_at) | |
| .limit(limit) | |
| .all() | |
| ) | |
| # 1) ์ ๊ฒฉ์ฑ ํ์ . ์ฝ๊ธฐ ํธ๋์ญ์ ์ ๋ซ๋๋ค(enqueue ๋ ๋ธ๋ก์ปค I/O โ DB ๋ฝ ๋ฏธ๋ณด์ ). | |
| # publish ์ง์ task ์ ํ์ฌ dispatch_version ์ ์ฌํ์ธ: | |
| # event.dispatch_version != task.dispatch_version โ stale โ STALE ๋ก ๋งํน ํ skip. | |
| eligible = [] | |
| stale_ids = [] | |
| for ev in events: | |
| task = db.get(PhotoTask, ev.task_id) | |
| if task is not None and ev.dispatch_version != task.dispatch_version: | |
| stale_ids.append(ev.id) | |
| logger.info( | |
| "stale_dispatch_ignored: event=%s task=%s event_dv=%s task_dv=%s", | |
| ev.id, ev.task_id, ev.dispatch_version, task.dispatch_version, | |
| ) | |
| continue | |
| if _task_dispatchable(task, now): | |
| eligible.append((ev.id, ev.task_id, ev.attempts, task.dispatch_version)) | |
| for ev_id in stale_ids: | |
| db.execute( | |
| update(OutboxEvent) | |
| .where(OutboxEvent.id == ev_id) | |
| .values(status="STALE", last_error="stale_dispatch_ignored") | |
| .execution_options(synchronize_session=False) | |
| ) | |
| db.commit() | |
| # 2) enqueue(payload ์ dispatch_version) + ์งง์ ํธ๋์ญ์ ์ผ๋ก published ๋งํน. | |
| published = 0 | |
| for ev_id, task_id, attempts, dversion in eligible: | |
| async_result = enqueue(task_id, dversion) | |
| db.execute( | |
| update(OutboxEvent) | |
| .where(OutboxEvent.id == ev_id) | |
| .values( | |
| published=True, | |
| status="PUBLISHED", | |
| published_at=_utcnow(), | |
| attempts=attempts + 1, | |
| ) | |
| .execution_options(synchronize_session=False) | |
| ) | |
| db.execute( | |
| update(PhotoTask) | |
| .where( | |
| PhotoTask.task_id == task_id, | |
| PhotoTask.celery_job_id.is_(None), | |
| ) | |
| .values(celery_job_id=_job_id(async_result)) | |
| .execution_options(synchronize_session=False) | |
| ) | |
| db.commit() | |
| published += 1 | |
| return published | |
| def repair_stuck_queued(db: Session, *, limit: int = 100, now: datetime | None = None) -> int: | |
| """dispatch ๊ฐ๋ฅํ๋ฐ ํ์ฌ dispatch_version ์ outbox ๊ฐ ์๋ task ์ ์ด๋ฒคํธ ๋ณด์ถฉ. | |
| ์ง์ enqueue ํ์ง ์๋๋ค. ๋ฉฑ๋ฑ helper(ensure_dispatch_event, rearm_published=False)๋ก | |
| ๊ฐ์ dispatch_version ์ค๋ณต ์์ฑ์ ๋ง๋๋ค. next_retry_at ์ task ๊ฐ์ ์น๊ณ. | |
| ๋ฐํ: ์๋ก ๋ง๋ (=created) outbox ์ด๋ฒคํธ ์. | |
| """ | |
| now = now or _utcnow() | |
| tasks = ( | |
| db.query(PhotoTask) | |
| .filter( | |
| PhotoTask.status == TaskStatus.QUEUED, | |
| PhotoTask.dead_lettered_at.is_(None), | |
| ) | |
| .limit(limit) | |
| .all() | |
| ) | |
| created = 0 | |
| for task in tasks: | |
| # ๊ฒ์ดํธ: lock ๋ง๋ฃ + next_retry ๋๋ฌ | |
| if not _task_dispatchable(task, now): | |
| continue | |
| outcome = task_repo.ensure_dispatch_event( | |
| db, | |
| task.task_id, | |
| task.dispatch_version, | |
| next_retry_at=task.next_retry_at, | |
| rearm_published=False, | |
| ) | |
| if outcome == "created": | |
| created += 1 | |
| db.commit() | |
| return created | |