"""Stale parse-job recovery helpers (running/queued left behind after worker crash).""" from __future__ import annotations import os from datetime import datetime, timezone from ..models import ParseJob # Jobs stuck in running/queued longer than this are considered abandoned. STALE_JOB_SECONDS = int(os.getenv("STALE_JOB_SECONDS", "900")) def _aware(dt: datetime | None) -> datetime | None: if dt is None: return None if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt def job_anchor_time(job: ParseJob) -> datetime | None: """Best available timestamp for staleness (prefer last_run_at).""" return _aware(job.last_run_at) or _aware(getattr(job, "created_at", None)) def is_stale_job(job: ParseJob, now: datetime | None = None, *, ttl: int | None = None) -> bool: if job.status not in ("running", "queued"): return False now = now or datetime.now(timezone.utc) anchor = job_anchor_time(job) if anchor is None: # No timestamp — treat long-lived running as stale immediately for recovery return job.status == "running" limit = ttl if ttl is not None else STALE_JOB_SECONDS return (now - anchor).total_seconds() >= limit