trading-system-docs/tolaria/c5-sync-service/job_state_machine.py

112 lines
4.3 KiB
Python

"""
AUTH.4C3 — job_state_machine.py (ERWEITERT)
============================================
Deterministische Job-State-Machine für den Executor Command Channel.
States:
CREATED, READY, CLAIMED, EXECUTING, SUCCEEDED, FAILED, REJECTED, OUTCOME_UNKNOWN,
RECONCILED
Eigenschaften:
* Geschlossene Transition-Allowlist
* Ungültige Transition -> FAIL CLOSED (InvalidTransitionError)
* DELETE: KEIN RETRYABLE — ein gestarteter DELETE wird bei Lease-Expiry in
OUTCOME_UNKNOWN überführt, NICHT zurück zu READY (kein blinder Retry)
* SAVE: RETRYABLE nur bei transienten Fehlern (TolariaUnavailable)
* AUTH.4C3: OUTCOME_UNKNOWN -> RECONCILED -> SUCCEEDED (read-only Reconciliation).
RECONCILED ist ein persistenter Zwischenzustand, der die read-only
Reconciliation dokumentiert. SUCCEEDED bleibt der terminale Erfolgs-State.
KEIN stilles OUTCOME_UNKNOWN -> SUCCEEDED ohne Reconciliation-Evidence.
Isoliert implementiert (KEIN produktiver Container).
"""
from __future__ import annotations
from typing import Dict, FrozenSet, Optional, Tuple
# ---------------------------------------------------------------------------
# States
# ---------------------------------------------------------------------------
ST_CREATED = "CREATED"
ST_READY = "READY"
ST_CLAIMED = "CLAIMED"
ST_EXECUTING = "EXECUTING"
ST_SUCCEEDED = "SUCCEEDED"
ST_FAILED = "FAILED"
ST_REJECTED = "REJECTED"
ST_OUTCOME_UNKNOWN = "OUTCOME_UNKNOWN"
ST_RECONCILED = "RECONCILED" # AUTH.4C3: read-only Reconciliation abgeschlossen
ALL_STATES = frozenset({
ST_CREATED, ST_READY, ST_CLAIMED, ST_EXECUTING,
ST_SUCCEEDED, ST_FAILED, ST_REJECTED, ST_OUTCOME_UNKNOWN, ST_RECONCILED,
})
# ---------------------------------------------------------------------------
# Erlaubte Transitionen (geschlossene Allowlist)
# ---------------------------------------------------------------------------
# (from_state, to_state)
ALLOWED_TRANSITIONS: FrozenSet[Tuple[str, str]] = frozenset({
# CREATED
(ST_CREATED, ST_READY), # Schema validiert, Job akzeptiert
(ST_CREATED, ST_REJECTED), # Schema/Allowlist/Injection-Fehler
# READY
(ST_READY, ST_CLAIMED), # atomarer Claim
(ST_READY, ST_REJECTED), # Gate-Fehler vor Claim
# CLAIMED
(ST_CLAIMED, ST_EXECUTING), # Claim bestätigt
(ST_CLAIMED, ST_REJECTED), # Gate-Fehler nach Claim
# EXECUTING
(ST_EXECUTING, ST_SUCCEEDED), # bestätigter Erfolg
(ST_EXECUTING, ST_FAILED), # bestätigter Fehler, kein Mutationseffekt
(ST_EXECUTING, ST_OUTCOME_UNKNOWN), # unklares HTTP-Ergebnis
(ST_EXECUTING, ST_REJECTED), # Gate-Fehler während Execution
# OUTCOME_UNKNOWN (terminal für DELETE; SAVE kann via Human/Admin reaktiviert werden)
(ST_OUTCOME_UNKNOWN, ST_REJECTED), # expliziter Human-/Admin-Eingriff
# AUTH.4C3: read-only Reconciliation (nur SAVE)
(ST_OUTCOME_UNKNOWN, ST_RECONCILED), # TARGET_EXACT bestätigt (read-only)
(ST_RECONCILED, ST_SUCCEEDED), # terminal bestätigt, KEINE neue Mutation
})
# Terminal-States (keine weiteren Transitionen ohne expliziten Eingriff)
TERMINAL_STATES = frozenset({ST_SUCCEEDED, ST_FAILED, ST_REJECTED})
class InvalidTransitionError(ValueError):
"""Ungültige Transition -> FAIL CLOSED."""
def is_valid_transition(from_state: str, to_state: str) -> bool:
return (from_state, to_state) in ALLOWED_TRANSITIONS
def transition(from_state: str, to_state: str) -> str:
"""
Führt eine State-Transition aus. Wirft InvalidTransitionError bei ungültiger
Transition (FAIL CLOSED).
"""
if from_state not in ALL_STATES:
raise InvalidTransitionError(f"unknown from_state: {from_state!r}")
if to_state not in ALL_STATES:
raise InvalidTransitionError(f"unknown to_state: {to_state!r}")
if not is_valid_transition(from_state, to_state):
raise InvalidTransitionError(
f"invalid transition: {from_state} -> {to_state} (FAIL CLOSED)"
)
return to_state
def is_terminal(state: str) -> bool:
return state in TERMINAL_STATES
def is_retryable(state: str, job_type: str) -> bool:
"""
SAVE: RETRYABLE nur aus OUTCOME_UNKNOWN (transient) — via Human/Admin.
DELETE: NIE retryable — ein gestarteter DELETE wird nicht blind wiederholt.
"""
if job_type == "C5_DELETE_OBJECT":
return False
# SAVE
return state == ST_OUTCOME_UNKNOWN