Files
siftlode/backend/app/runstatus.py
T
peter 45c0ee7d58 feat(scheduler): every background run is a durable row
What a job last did lived in the API process's memory and was gone at
every restart; the audit log kept a run only when it was manual, failed
or changed something. Now each run is a job_runs row written at its
start and its end in its own session: who started it, its outcome as
data, what it spent of the YouTube quota (quota_events.job_run_id) and
the lines it logged (log_events.run_id, filterable in the log console).

- a run a stopped process left open is marked interrupted at startup
- "run now" is a one-shot job on the scheduler, which now always starts
  (interval jobs only with SCHEDULER_ENABLED); the manual actor is reset
  after the run, since pool threads are reused
- the dashboard's last run comes from job_runs, so it survives a deploy
- runs leave the audit log: migration 0109 moves the old job_run rows
  over (and back on downgrade); the audit page's run folding goes
- job_run_retention_days (default 30) prunes old runs in audit_gc
2026-10-17 23:08:55 +02:00

36 lines
1.3 KiB
Python

"""The words a background run's row is written with — the `job_runs` table's status and trigger.
One source for the literals: the scheduler writes them, the startup sweep rewrites `running`, the
model's CHECK constraints are built from these sets, and the dashboard reads them back. A run's
status is never typed as a string anywhere else (the download pipeline's `cancelled`/`canceled`
drift, `downloads/status.py`, is why).
"""
from enum import StrEnum
class RunStatus(StrEnum):
RUNNING = "running"
OK = "ok"
ERROR = "error"
# Sync was paused: the run was due and deliberately did nothing.
SKIPPED = "skipped"
# The process stopped while the run was going (a deploy, a crash): set at the next startup, so a
# row never claims to be running in a process that no longer exists.
INTERRUPTED = "interrupted"
class RunTrigger(StrEnum):
# The run's interval came round.
SCHEDULED = "scheduled"
# An admin pressed "run now" (or "run all").
MANUAL = "manual"
# A user's own action started work on one target (a channel's full history), outside the
# recurring jobs.
AD_HOC = "ad_hoc"
def sql_in(values: type[StrEnum]) -> str:
"""`'a', 'b'` — the IN-list a CHECK constraint names these values with."""
return ", ".join(f"'{v.value}'" for v in values)