130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Background job models: generic, reusable infrastructure for long-running tasks.
|
|
|
|
A `BackgroundJob` record tracks the lifecycle of one async task that must not block
|
|
the calling HTTP request. Any caller (HTTP route, AI tool, scheduled task) can:
|
|
|
|
1. Register a handler once via `registerJobHandler(jobType, handler)`.
|
|
2. Submit work via `startJob(jobType, payload, ...)` which returns a `jobId`
|
|
immediately and runs the handler in the background.
|
|
3. Poll `getJobStatus(jobId)` (HTTP `GET /api/jobs/{jobId}`) until `status` is
|
|
one of {SUCCESS, ERROR, CANCELLED}.
|
|
|
|
See `modules.serviceCenter.services.serviceBackgroundJobs.mainBackgroundJobService`.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional
|
|
from enum import Enum
|
|
from datetime import datetime, timezone
|
|
import uuid
|
|
|
|
from pydantic import Field
|
|
|
|
from modules.datamodels.datamodelBase import PowerOnModel
|
|
from modules.shared.i18nRegistry import i18nModel
|
|
|
|
|
|
class BackgroundJobStatusEnum(str, Enum):
|
|
"""Lifecycle status of a background job."""
|
|
PENDING = "PENDING"
|
|
RUNNING = "RUNNING"
|
|
SUCCESS = "SUCCESS"
|
|
ERROR = "ERROR"
|
|
CANCELLED = "CANCELLED"
|
|
|
|
|
|
TERMINAL_JOB_STATUSES = {
|
|
BackgroundJobStatusEnum.SUCCESS,
|
|
BackgroundJobStatusEnum.ERROR,
|
|
BackgroundJobStatusEnum.CANCELLED,
|
|
}
|
|
|
|
|
|
@i18nModel("Hintergrund-Job")
|
|
class BackgroundJob(PowerOnModel):
|
|
"""Generic record describing a long-running asynchronous task.
|
|
|
|
Scope: the combination of `mandateId` and optionally `featureInstanceId`
|
|
is used for access control on `GET /api/jobs/{jobId}`.
|
|
"""
|
|
|
|
id: str = Field(
|
|
default_factory=lambda: str(uuid.uuid4()),
|
|
description="Primary key",
|
|
json_schema_extra={"label": "ID"},
|
|
)
|
|
jobType: str = Field(
|
|
...,
|
|
description="Handler key registered via registerJobHandler() (e.g. 'trusteeAccountingSync')",
|
|
json_schema_extra={"label": "Typ"},
|
|
)
|
|
mandateId: Optional[str] = Field(
|
|
None,
|
|
description="Mandate scope (used for access checks). None for system-wide jobs.",
|
|
json_schema_extra={
|
|
"label": "Mandanten-ID",
|
|
"fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label"},
|
|
},
|
|
)
|
|
featureInstanceId: Optional[str] = Field(
|
|
None,
|
|
description="Feature instance scope (optional)",
|
|
json_schema_extra={
|
|
"label": "Feature-Instanz",
|
|
"fk_target": {"db": "poweron_app", "table": "FeatureInstance", "labelField": "label"},
|
|
},
|
|
)
|
|
triggeredBy: Optional[str] = Field(
|
|
None,
|
|
description="UserId or 'ai-tool:<toolName>' / 'scheduler:<jobName>'",
|
|
json_schema_extra={"label": "Ausgeloest von"},
|
|
)
|
|
|
|
status: str = Field(
|
|
default=BackgroundJobStatusEnum.PENDING.value,
|
|
description="Current lifecycle status",
|
|
json_schema_extra={"label": "Status"},
|
|
)
|
|
progress: int = Field(
|
|
default=0,
|
|
description="Progress 0..100 (best-effort; may stay 0 for handlers that cannot estimate)",
|
|
json_schema_extra={"label": "Fortschritt"},
|
|
)
|
|
progressMessage: Optional[str] = Field(
|
|
None,
|
|
description="Human-readable current step (e.g. 'Importing journal entries...')",
|
|
json_schema_extra={"label": "Fortschritts-Nachricht"},
|
|
)
|
|
|
|
payload: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Job input parameters (JSON)",
|
|
json_schema_extra={"label": "Eingabe"},
|
|
)
|
|
result: Optional[Dict[str, Any]] = Field(
|
|
None,
|
|
description="Handler return value on success (JSON)",
|
|
json_schema_extra={"label": "Ergebnis"},
|
|
)
|
|
errorMessage: Optional[str] = Field(
|
|
None,
|
|
description="Truncated error message on failure (full stack trace in logs)",
|
|
json_schema_extra={"label": "Fehler"},
|
|
)
|
|
|
|
createdAt: float = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc).timestamp(),
|
|
description="When the job was submitted (UTC unix)",
|
|
json_schema_extra={"label": "Eingereicht", "frontend_type": "timestamp"},
|
|
)
|
|
startedAt: Optional[float] = Field(
|
|
None,
|
|
description="When the handler began running (UTC unix)",
|
|
json_schema_extra={"label": "Gestartet", "frontend_type": "timestamp"},
|
|
)
|
|
finishedAt: Optional[float] = Field(
|
|
None,
|
|
description="When the handler reached a terminal status (UTC unix)",
|
|
json_schema_extra={"label": "Beendet", "frontend_type": "timestamp"},
|
|
)
|