""" 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