107 lines
3.5 KiB
Python
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.")
|