feat: add archive client foundation
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Durable command inbox and client recovery cursors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CommandConflict(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandAcceptance:
|
||||
duplicate: bool
|
||||
acknowledgement_json: str
|
||||
|
||||
|
||||
class ClientStore:
|
||||
def __init__(self, database: Path):
|
||||
self.database = database
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.database.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._connect() as connection:
|
||||
version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||
if version > 1:
|
||||
raise RuntimeError(
|
||||
f"client database schema {version} is newer than supported"
|
||||
)
|
||||
connection.executescript(_SCHEMA)
|
||||
connection.execute("PRAGMA user_version = 1")
|
||||
if connection.execute("PRAGMA foreign_key_check").fetchall():
|
||||
raise RuntimeError("client database foreign-key check failed")
|
||||
|
||||
def accept_command(
|
||||
self, command_id: str, command_json: str, acknowledgement_json: str
|
||||
) -> CommandAcceptance:
|
||||
payload = _canonical(json.loads(command_json))
|
||||
acknowledgement = _canonical(json.loads(acknowledgement_json))
|
||||
digest = hashlib.sha256(payload.encode()).hexdigest()
|
||||
with self._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
existing = connection.execute(
|
||||
"SELECT payload_sha256, command_json, acknowledgement_json FROM commands WHERE command_id = ?",
|
||||
(command_id,),
|
||||
).fetchone()
|
||||
if existing:
|
||||
if existing["payload_sha256"] != digest or existing["command_json"] != payload:
|
||||
raise CommandConflict("command ID was reused with different content")
|
||||
return CommandAcceptance(True, existing["acknowledgement_json"])
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO commands (
|
||||
command_id, payload_sha256, command_json,
|
||||
acknowledgement_json, state
|
||||
) VALUES (?, ?, ?, ?, 'received')
|
||||
""",
|
||||
(command_id, digest, payload, acknowledgement),
|
||||
)
|
||||
return CommandAcceptance(False, acknowledgement)
|
||||
|
||||
def list_active_job_cursors(self) -> list[dict[str, object]]:
|
||||
with self._connect() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT job_id, revision, last_event_sequence, state, committed
|
||||
FROM jobs WHERE state NOT IN ('JOB_STATE_SUCCEEDED',
|
||||
'JOB_STATE_FAILED', 'JOB_STATE_CANCELLED')
|
||||
ORDER BY job_id
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(self.database, isolation_level=None, timeout=5)
|
||||
connection.row_factory = sqlite3.Row
|
||||
connection.execute("PRAGMA foreign_keys = ON")
|
||||
connection.execute("PRAGMA journal_mode = WAL")
|
||||
connection.execute("PRAGMA synchronous = FULL")
|
||||
connection.execute("PRAGMA busy_timeout = 5000")
|
||||
return connection
|
||||
|
||||
|
||||
def _canonical(value: object) -> str:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS commands (
|
||||
command_id TEXT PRIMARY KEY,
|
||||
payload_sha256 TEXT NOT NULL,
|
||||
command_json TEXT NOT NULL,
|
||||
acknowledgement_json TEXT NOT NULL,
|
||||
state TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
job_id TEXT PRIMARY KEY,
|
||||
definition_json TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
last_event_sequence INTEGER NOT NULL,
|
||||
committed INTEGER NOT NULL CHECK(committed IN (0, 1))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
event_id TEXT PRIMARY KEY,
|
||||
job_id TEXT NOT NULL REFERENCES jobs(job_id),
|
||||
sequence INTEGER NOT NULL,
|
||||
event_json TEXT NOT NULL,
|
||||
UNIQUE(job_id, sequence)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_journal (
|
||||
operation_id TEXT PRIMARY KEY,
|
||||
job_id TEXT NOT NULL,
|
||||
intent_json TEXT NOT NULL,
|
||||
result_json TEXT,
|
||||
state TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
Reference in New Issue
Block a user