feat: complete client runtime foundation
This commit is contained in:
@@ -4,7 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -13,6 +15,10 @@ class CommandConflict(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class JobConflict(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandAcceptance:
|
||||
duplicate: bool
|
||||
@@ -25,6 +31,13 @@ class ClientStore:
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.database.parent.mkdir(parents=True, exist_ok=True)
|
||||
existed = self.database.exists()
|
||||
if existed:
|
||||
metadata = self.database.stat()
|
||||
if not stat.S_ISREG(metadata.st_mode):
|
||||
raise RuntimeError("client database is not a regular file")
|
||||
if metadata.st_mode & (stat.S_IRWXG | stat.S_IRWXO):
|
||||
raise RuntimeError("client database file permissions are unsafe")
|
||||
with self._connect() as connection:
|
||||
version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||
if version > 1:
|
||||
@@ -35,6 +48,8 @@ class ClientStore:
|
||||
connection.execute("PRAGMA user_version = 1")
|
||||
if connection.execute("PRAGMA foreign_key_check").fetchall():
|
||||
raise RuntimeError("client database foreign-key check failed")
|
||||
if not existed:
|
||||
os.chmod(self.database, 0o600)
|
||||
|
||||
def accept_command(
|
||||
self, command_id: str, command_json: str, acknowledgement_json: str
|
||||
@@ -45,7 +60,10 @@ class ClientStore:
|
||||
with self._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
existing = connection.execute(
|
||||
"SELECT payload_sha256, command_json, acknowledgement_json FROM commands WHERE command_id = ?",
|
||||
"""
|
||||
SELECT payload_sha256, command_json, acknowledgement_json
|
||||
FROM commands WHERE command_id = ?
|
||||
""",
|
||||
(command_id,),
|
||||
).fetchone()
|
||||
if existing:
|
||||
@@ -75,6 +93,78 @@ class ClientStore:
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def job_snapshot_rows(
|
||||
self, job_ids: list[str]
|
||||
) -> list[dict[str, object]]:
|
||||
if not job_ids:
|
||||
return []
|
||||
placeholders = ",".join("?" for _ in job_ids)
|
||||
with self._connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT job_id, definition_json, state, revision,
|
||||
last_event_sequence, committed
|
||||
FROM jobs WHERE job_id IN ({placeholders}) ORDER BY job_id
|
||||
""",
|
||||
job_ids,
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def save_job(
|
||||
self,
|
||||
job_id: str,
|
||||
definition_json: str,
|
||||
state: str,
|
||||
revision: int,
|
||||
last_event_sequence: int,
|
||||
committed: bool,
|
||||
) -> None:
|
||||
definition = _canonical(json.loads(definition_json))
|
||||
with self._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
existing = connection.execute(
|
||||
"""
|
||||
SELECT definition_json, state, revision,
|
||||
last_event_sequence, committed
|
||||
FROM jobs WHERE job_id = ?
|
||||
""",
|
||||
(job_id,),
|
||||
).fetchone()
|
||||
if existing and existing["definition_json"] != definition:
|
||||
raise JobConflict("job definition is immutable")
|
||||
if existing and (
|
||||
revision < existing["revision"]
|
||||
or last_event_sequence < existing["last_event_sequence"]
|
||||
or (existing["committed"] and not committed)
|
||||
):
|
||||
raise JobConflict("job cursor cannot move backwards")
|
||||
if existing and (
|
||||
revision == existing["revision"]
|
||||
and last_event_sequence == existing["last_event_sequence"]
|
||||
and (
|
||||
state != existing["state"]
|
||||
or int(committed) != existing["committed"]
|
||||
)
|
||||
):
|
||||
raise JobConflict("equal job cursor has conflicting state")
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO jobs (
|
||||
job_id, definition_json, state, revision,
|
||||
last_event_sequence, committed
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(job_id) DO UPDATE SET
|
||||
state = excluded.state,
|
||||
revision = excluded.revision,
|
||||
last_event_sequence = excluded.last_event_sequence,
|
||||
committed = excluded.committed
|
||||
""",
|
||||
(
|
||||
job_id, definition, state, revision,
|
||||
last_event_sequence, int(committed),
|
||||
),
|
||||
)
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(self.database, isolation_level=None, timeout=5)
|
||||
connection.row_factory = sqlite3.Row
|
||||
|
||||
Reference in New Issue
Block a user