Files
asf/packages/asf-domain/asf_domain/job.py
T
2026-09-08 07:21:34 -04:00

107 lines
3.5 KiB
Python

"""Job model — persistent asynchronous job state.
RabbitMQ delivers work messages. MongoDB is the source of truth for job
lifecycle. Worker processes update Job records as they progress through
queued → running → completed|failed|cancelled.
Do not use RabbitMQ queues as the permanent job database.
"""
from __future__ import annotations
from typing import Any, Optional, Dict
from pydantic import Field
from asf_domain.base import (
ASFBaseModel,
TenantScopedModel,
ASFFieldID,
UTCDatetime,
JobStatus,
JobPriority,
JobType,
generate_id,
)
class JobUsage(ASFBaseModel):
"""Optional resource / cost usage tracked for a job."""
input_tokens: Optional[int] = Field(None, ge=0)
output_tokens: Optional[int] = Field(None, ge=0)
gpu_seconds: Optional[float] = Field(None, ge=0)
cpu_seconds: Optional[float] = Field(None, ge=0)
estimated_cost: Optional[float] = Field(
None,
ge=0,
description="Estimated cost in tenant currency (EUR/USD). Approximate, used for metering only.",
)
extra: Dict[str, Any] = Field(default_factory=dict)
class JobBase(ASFBaseModel):
type: JobType = Field(
...,
description="Logical job class. Used for routing to the correct RabbitMQ queue.",
)
priority: JobPriority = JobPriority.NORMAL
payload: Dict[str, Any] = Field(
default_factory=dict,
description="The actual work payload (e.g. {artifact_id, prompt, model_preference}).",
)
correlation_id: Optional[str] = Field(
None,
max_length=128,
description="Optional correlation id propagated across logs, events, child jobs.",
)
class JobCreate(JobBase):
"""Create payload. tenant_id/project_id/requested_by are injected server-side."""
project_id: Optional[ASFFieldID] = None
model: Optional[str] = Field(
None,
max_length=128,
description="Preferred model / engine, only meaningful for AI job types. "
"Ultimate routing decision belongs to the AI Gateway.",
)
class JobUpdate(ASFBaseModel):
"""Updates issued by workers as the job progresses."""
status: Optional[JobStatus] = None
worker: Optional[str] = Field(
None, max_length=255, description="Identifier of the worker process claiming this job."
)
started_at: Optional[UTCDatetime] = None
completed_at: Optional[UTCDatetime] = None
result: Optional[Dict[str, Any]] = None
error: Optional[str] = Field(None, max_length=2000)
error_detail: Optional[Dict[str, Any]] = None
usage: Optional[JobUsage] = None
progress_percent: Optional[int] = Field(None, ge=0, le=100)
class Job(TenantScopedModel, JobBase):
"""Persistent job record — the authoritative source of truth."""
id: ASFFieldID = Field(default_factory=generate_id)
project_id: Optional[ASFFieldID] = None
status: JobStatus = JobStatus.QUEUED
requested_by: Optional[ASFFieldID] = Field(
None, description="User that requested the job, or null for system jobs."
)
worker: Optional[str] = None
model: Optional[str] = None
created_at: UTCDatetime = ...
started_at: Optional[UTCDatetime] = None
completed_at: Optional[UTCDatetime] = None
result: Optional[Dict[str, Any]] = None
error: Optional[str] = None
error_detail: Optional[Dict[str, Any]] = None
usage: JobUsage = Field(default_factory=JobUsage)
progress_percent: Optional[int] = Field(None, ge=0, le=100)
retries: int = Field(0, ge=0, description="How many times this job has been retried.")