diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 0697c07..f7a3c94 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -21,6 +21,7 @@ from .tag_alias import TagAlias from .tag_allowlist import TagAllowlist from .tag_reference_embedding import TagReferenceEmbedding from .tag_suggestion_rejection import TagSuggestionRejection +from .task_run import TaskRun __all__ = [ "Base", @@ -46,4 +47,5 @@ __all__ = [ "TagAllowlist", "TagReferenceEmbedding", "TagSuggestionRejection", + "TaskRun", ] diff --git a/backend/app/models/task_run.py b/backend/app/models/task_run.py new file mode 100644 index 0000000..e8eac05 --- /dev/null +++ b/backend/app/models/task_run.py @@ -0,0 +1,48 @@ +"""FC-3i: task_run — per-Celery-task lifecycle audit row. + +One row inserted by the task_prerun signal at task start, updated by +task_postrun / task_failure / task_retry. The shape supports the +SystemActivity dashboard's three panes: per-queue queue+worker summary, +recent failures (24h, grouped by error_type), and full paginated +activity history. + +Retention: ok rows pruned after 24h, error/timeout after 7d (see +backend.app.tasks.maintenance.prune_task_runs). + +Recovery: rows stuck in 'running' for >5 min flipped to 'error' by +backend.app.tasks.maintenance.recover_stalled_task_runs (Beat 5 min). +""" + +from datetime import datetime + +from sqlalchemy import DateTime, Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class TaskRun(Base): + __tablename__ = "task_run" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + celery_task_id: Mapped[str] = mapped_column( + String(64), nullable=False, index=True, + ) + queue: Mapped[str] = mapped_column(String(32), nullable=False, index=True) + task_name: Mapped[str] = mapped_column(String(128), nullable=False, index=True) + target_id: Mapped[int | None] = mapped_column(Integer, nullable=True) + started_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, index=True, + ) + finished_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, index=True, + ) + duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True) + status: Mapped[str] = mapped_column( + String(16), nullable=False, default="running", index=True, + ) + error_type: Mapped[str | None] = mapped_column(String(128), nullable=True) + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + retry_count: Mapped[int | None] = mapped_column(Integer, nullable=True) + worker_hostname: Mapped[str | None] = mapped_column(String(128), nullable=True) + args_summary: Mapped[str | None] = mapped_column(String(255), nullable=True)