US02-02: Persist and Coordinate Durable Jobs (#55)
This commit was merged in pull request #55.
This commit is contained in:
71
photo_pipeline/models/jobs.py
Normal file
71
photo_pipeline/models/jobs.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""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()
|
||||
)
|
||||
Reference in New Issue
Block a user