"""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() )