"""MazeNET topology status state machine. Seven states — six active in v1. ``degraded`` is schema-reserved for the future Healer worker and has no transitions into it from v1 code paths. """ from __future__ import annotations class TopologyStatus: PENDING = "pending" DEPLOYING = "deploying" ACTIVE = "active" DEGRADED = "degraded" FAILED = "failed" TEARING_DOWN = "tearing_down" TORN_DOWN = "torn_down" ALL: frozenset[str] = frozenset( {PENDING, DEPLOYING, ACTIVE, DEGRADED, FAILED, TEARING_DOWN, TORN_DOWN} ) # Directed transitions. torn_down is terminal. degraded is unreachable # in v1 (Healer would be the only writer), but its outbound edges stay # defined so when Healer lands the state machine already accepts them. _LEGAL: dict[str, frozenset[str]] = { TopologyStatus.PENDING: frozenset( {TopologyStatus.DEPLOYING, TopologyStatus.TORN_DOWN} ), TopologyStatus.DEPLOYING: frozenset( { TopologyStatus.ACTIVE, TopologyStatus.FAILED, TopologyStatus.DEGRADED, TopologyStatus.TEARING_DOWN, } ), TopologyStatus.ACTIVE: frozenset( {TopologyStatus.DEGRADED, TopologyStatus.TEARING_DOWN} ), TopologyStatus.DEGRADED: frozenset( {TopologyStatus.ACTIVE, TopologyStatus.TEARING_DOWN} ), TopologyStatus.FAILED: frozenset({TopologyStatus.TEARING_DOWN}), TopologyStatus.TEARING_DOWN: frozenset( {TopologyStatus.TORN_DOWN, TopologyStatus.DEGRADED} ), TopologyStatus.TORN_DOWN: frozenset(), } class TopologyStatusError(ValueError): """Raised when an illegal topology status transition is attempted.""" class TopologyNotEditable(RuntimeError): """Raised when a pending-only mutation hits a non-pending topology. Pre-deploy edits (update_lan, delete_lan, update/delete decky, delete_edge) are only legal while the topology is ``pending``. After deploy the mutator's reconciler + topology_mutations table take over. """ def __init__(self, *, status: str, reason: str = "") -> None: self.status = status self.reason = reason super().__init__( f"topology not editable (status={status!r})" + (f": {reason}" if reason else "") ) class VersionConflict(RuntimeError): """Raised when a topology write is supplied a stale ``expected_version``. Optimistic concurrency guard: the caller passed the version it last observed, and the topology has since been mutated by someone else. The caller should re-read and retry. """ def __init__(self, *, current: int, expected: int) -> None: self.current = current self.expected = expected super().__init__( f"topology version conflict: expected {expected}, current is {current}" ) def assert_transition(current: str, new: str) -> None: """Validate ``current → new`` or raise :class:`TopologyStatusError`.""" if current not in TopologyStatus.ALL: raise TopologyStatusError(f"unknown current status: {current!r}") if new not in TopologyStatus.ALL: raise TopologyStatusError(f"unknown new status: {new!r}") if new not in _LEGAL[current]: raise TopologyStatusError( f"illegal transition: {current!r} → {new!r}" ) def legal_next(current: str) -> frozenset[str]: """Return the set of legal successor statuses from ``current``.""" if current not in _LEGAL: raise TopologyStatusError(f"unknown status: {current!r}") return _LEGAL[current]