Support kind=llm profiles (instruction/schema), optional multi-event posts via #eN URLs, and recover stale running/queued parse jobs after worker crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
"""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
|