"""nisse-dev's state store (architecture L8): the shared one, with the dev agent's own roles, views and tables (build VM blocks, D3).""" import sqlite3 from collections.abc import Mapping from dataclasses import dataclass from nisse_core.state import store as state from nisse_core.state.store import Migration, utc_now _VM_BLOCKS = """ CREATE TABLE vm_blocks ( host TEXT PRIMARY KEY, cause TEXT NOT NULL, reason TEXT NOT NULL, work_item TEXT, since TEXT NOT NULL ) """ def _add_vm_blocks(conn: sqlite3.Connection) -> None: conn.execute(_VM_BLOCKS) @dataclass(frozen=True) class VMBlock: """A build VM nothing may run on until it is recovered (D3): every project on it waits.""" host: str cause: str # broken (the driver said so), unreachable, host key (#137) reason: str work_item: str | None # the item that found it since: str class StateStore(state.StateStore): AGENT = "nisse-dev" SCHEMA_VERSION = 5 SCHEMA = (_VM_BLOCKS,) MIGRATIONS: Mapping[int, Migration] = {4: _add_vm_blocks} # planner and executor are the old loop's (until M2.8), kept so its # records still read. ACTORS = state.ACTORS | { "planner", "executor", "toolchain", # the toolchain step (D3): the driver's model, apart "memory", # the memory pass at an item's close (L6.4), apart too } # What each role is shown of the others' work (``nisse_core.log.views``). SHOWN: Mapping[str, Mapping[str, str]] = { "driver": {"helper": "report"}, "helper": {"driver": "narrate"}, # The toolchain step works in its own transcript: the driver sees # only its report (a tool result, or facts), the step the driver's # work. "toolchain": {"driver": "narrate"}, # The memory pass at an item's close (L6.4): the item as it went. "memory": {"driver": "narrate"}, } # --- build VMs (D3) ---------------------------------------------------- def block_vm( self, host: str, *, cause: str, reason: str, work_item: str | None ) -> None: """Block a build VM; a second block on it keeps the first.""" row = self._conn.execute( "INSERT INTO vm_blocks (host, cause, reason, work_item, since) " "VALUES (?, ?, ?, ?, ?) ON CONFLICT(host) DO NOTHING RETURNING host", (host, cause, reason, work_item, utc_now()), ).fetchone() if row is not None: self.oplog.emit("vm.block", cause=cause, host=host, work_item=work_item) def reblock_vm(self, host: str, *, cause: str, reason: str) -> None: """A blocked VM's block, for another cause (an unreachable VM that answers with a new host key).""" self._conn.execute( "UPDATE vm_blocks SET cause = ?, reason = ? WHERE host = ?", (cause, reason, host), ) self.oplog.emit("vm.block", cause=cause, host=host) def unblock_vm(self, host: str, *, cause: str) -> VMBlock | None: """Lift a build VM's block; returns it, or None if there was none.""" row = self._conn.execute( "DELETE FROM vm_blocks WHERE host = ? RETURNING *", (host,) ).fetchone() if row is None: return None self.oplog.emit("vm.unblock", cause=cause, host=host) return VMBlock(**dict(row)) def vm_blocks(self) -> dict[str, VMBlock]: rows = self._conn.execute("SELECT * FROM vm_blocks").fetchall() return {row["host"]: VMBlock(**dict(row)) for row in rows}