Files
photoanalyzer/photo_pipeline/models/jobs.py

72 lines
3.0 KiB
Python

"""Durable jobs, their per-item progress, and their event history.
A job is a unit of long-running work whose lifecycle survives browser, API, and
worker interruption. Progress is derived from ``job_items`` (the durable source of
truth), never from browser memory. ``job_events`` is an append-only activity log.
Leases (owner + expiry + heartbeat) and a monotonic ``fencing_token`` let a
recovered worker be fenced out after a newer worker takes ownership.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from photo_pipeline.db import Base
class Job(Base):
__tablename__ = "jobs"
id: Mapped[str] = mapped_column(String, primary_key=True)
job_type: Mapped[str] = mapped_column(String, nullable=False)
state: Mapped[str] = mapped_column(String, nullable=False)
lock_key: Mapped[str | None] = mapped_column(String)
idempotency_key: Mapped[str | None] = mapped_column(String, unique=True)
lease_owner: Mapped[str | None] = mapped_column(String)
lease_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
fencing_token: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
error_code: Mapped[str | None] = mapped_column(String)
error_message: Mapped[str | None] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
class JobItem(Base):
__tablename__ = "job_items"
job_id: Mapped[str] = mapped_column(ForeignKey("jobs.id"), primary_key=True)
item_key: Mapped[str] = mapped_column(String, primary_key=True)
state: Mapped[str] = mapped_column(String, nullable=False)
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
error_code: Mapped[str | None] = mapped_column(String)
error_message: Mapped[str | None] = mapped_column(String)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
class JobEvent(Base):
__tablename__ = "job_events"
id: Mapped[str] = mapped_column(String, primary_key=True)
job_id: Mapped[str] = mapped_column(ForeignKey("jobs.id"), nullable=False)
event_type: Mapped[str] = mapped_column(String, nullable=False)
message: Mapped[str | None] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)