first
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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.")
|
||||
Reference in New Issue
Block a user