Persist desired and verified reconciliation state

This commit is contained in:
Lyncoln
2026-09-19 18:00:48 -04:00
parent 3fdcbd1a54
commit 270c66322e
2 changed files with 605 additions and 0 deletions
+365
View File
@@ -33,6 +33,29 @@ class ObservationStatus:
return self.last_error is None
@dataclass(frozen=True)
class DesiredState:
object: NormalizedObject
selected_at: datetime
@dataclass(frozen=True)
class AppliedState:
endpoint_id: str
object: NormalizedObject
applied_at: datetime
verification_attempted_at: datetime | None
verified_at: datetime | None
verification_error: str | None
@property
def is_verified(self) -> bool:
return (
self.verified_at is not None
and self.verification_error is None
)
def _utc(value: datetime, label: str) -> datetime:
if value.tzinfo is None:
raise RuntimeError(f"{label} must be timezone-aware")
@@ -153,6 +176,63 @@ class SQLiteStateStore:
"""
)
versions = self.connection.execute(
"SELECT version FROM schema_meta"
).fetchall()
if len(versions) != 1:
raise RuntimeError(
"SQLite state schema has invalid version metadata"
)
version = versions[0]["version"]
if version not in (1, 2):
raise RuntimeError(
"Unsupported SQLite state schema version: "
f"{version!r}"
)
if version == 1:
self.connection.executescript(
"""
CREATE TABLE desired_state (
object_type TEXT NOT NULL,
logical_key TEXT NOT NULL,
payload_json TEXT NOT NULL,
selected_at TEXT NOT NULL,
PRIMARY KEY (
object_type,
logical_key
)
);
CREATE TABLE applied_state (
endpoint_id TEXT NOT NULL,
object_type TEXT NOT NULL,
logical_key TEXT NOT NULL,
local_key TEXT,
payload_json TEXT NOT NULL,
applied_at TEXT NOT NULL,
verification_attempted_at TEXT,
verified_at TEXT,
verification_error TEXT,
PRIMARY KEY (
endpoint_id,
object_type,
logical_key
)
);
CREATE INDEX applied_state_by_object
ON applied_state (
object_type,
logical_key
);
UPDATE schema_meta
SET version = 2;
"""
)
def close(self) -> None:
self.connection.close()
@@ -328,6 +408,291 @@ class SQLiteStateStore:
for row in rows
]
def replace_desired_state(
self,
object_type: str,
objects: Iterable[NormalizedObject],
selected_at: datetime,
) -> None:
rows = list(objects)
selected_timestamp = _timestamp(
selected_at,
"Desired-state selection timestamp",
)
prepared: list[tuple[str, str, str, str]] = []
logical_keys: set[str] = set()
for normalized in rows:
if normalized.object_type != object_type:
raise RuntimeError(
"Desired object type does not match snapshot type"
)
if normalized.local_key is not None:
raise RuntimeError(
"Canonical desired objects cannot contain "
"an endpoint-local key"
)
if normalized.logical_key in logical_keys:
raise RuntimeError(
"Duplicate desired logical key: "
f"{normalized.logical_key}"
)
logical_keys.add(normalized.logical_key)
prepared.append(
(
object_type,
normalized.logical_key,
_serialize_payload(normalized.payload),
selected_timestamp,
)
)
with self.connection:
self.connection.execute(
"""
DELETE FROM desired_state
WHERE object_type = ?
""",
(object_type,),
)
self.connection.executemany(
"""
INSERT INTO desired_state (
object_type,
logical_key,
payload_json,
selected_at
) VALUES (?, ?, ?, ?)
""",
prepared,
)
def load_desired_state(
self,
object_type: str | None = None,
) -> list[DesiredState]:
sql = """
SELECT
object_type,
logical_key,
payload_json,
selected_at
FROM desired_state
"""
parameters: tuple[str, ...] = ()
if object_type is not None:
sql += " WHERE object_type = ?"
parameters = (object_type,)
sql += " ORDER BY object_type, logical_key"
rows = self.connection.execute(sql, parameters).fetchall()
return [
DesiredState(
object=NormalizedObject(
object_type=row["object_type"],
logical_key=row["logical_key"],
payload=_deserialize_payload(row["payload_json"]),
),
selected_at=_parse_timestamp(row["selected_at"]),
)
for row in rows
]
def record_applied(
self,
endpoint_id: str,
object: NormalizedObject,
applied_at: datetime,
) -> None:
if not endpoint_id:
raise RuntimeError("Applied-state endpoint_id cannot be empty")
payload_json = _serialize_payload(object.payload)
applied_timestamp = _timestamp(
applied_at,
"Application timestamp",
)
with self.connection:
self.connection.execute(
"""
INSERT INTO applied_state (
endpoint_id,
object_type,
logical_key,
local_key,
payload_json,
applied_at,
verification_attempted_at,
verified_at,
verification_error
) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL)
ON CONFLICT(
endpoint_id,
object_type,
logical_key
) DO UPDATE SET
local_key = excluded.local_key,
payload_json = excluded.payload_json,
applied_at = excluded.applied_at,
verification_attempted_at = NULL,
verified_at = NULL,
verification_error = NULL
""",
(
endpoint_id,
object.object_type,
object.logical_key,
object.local_key,
payload_json,
applied_timestamp,
),
)
def _record_verification(
self,
endpoint_id: str,
object: NormalizedObject,
attempted_at: datetime,
*,
succeeded: bool,
error: str | None,
) -> None:
attempted_timestamp = _timestamp(
attempted_at,
"Verification timestamp",
)
payload_json = _serialize_payload(object.payload)
if succeeded:
verified_at = attempted_timestamp
verification_error = None
else:
verified_at = None
verification_error = str(
error or "verification failed"
)
with self.connection:
cursor = self.connection.execute(
"""
UPDATE applied_state
SET
verification_attempted_at = ?,
verified_at = COALESCE(?, verified_at),
verification_error = ?
WHERE
endpoint_id = ?
AND object_type = ?
AND logical_key = ?
AND payload_json = ?
""",
(
attempted_timestamp,
verified_at,
verification_error,
endpoint_id,
object.object_type,
object.logical_key,
payload_json,
),
)
if cursor.rowcount != 1:
raise RuntimeError(
"Verification does not match the currently "
"applied object"
)
def record_verification_success(
self,
endpoint_id: str,
object: NormalizedObject,
verified_at: datetime,
) -> None:
self._record_verification(
endpoint_id,
object,
verified_at,
succeeded=True,
error=None,
)
def record_verification_failure(
self,
endpoint_id: str,
object: NormalizedObject,
error: str,
attempted_at: datetime,
) -> None:
self._record_verification(
endpoint_id,
object,
attempted_at,
succeeded=False,
error=error,
)
def load_applied_state(
self,
endpoint_id: str | None = None,
object_type: str | None = None,
) -> list[AppliedState]:
clauses: list[str] = []
parameters: list[str] = []
if endpoint_id is not None:
clauses.append("endpoint_id = ?")
parameters.append(endpoint_id)
if object_type is not None:
clauses.append("object_type = ?")
parameters.append(object_type)
sql = """
SELECT
endpoint_id,
object_type,
logical_key,
local_key,
payload_json,
applied_at,
verification_attempted_at,
verified_at,
verification_error
FROM applied_state
"""
if clauses:
sql += " WHERE " + " AND ".join(clauses)
sql += """
ORDER BY
endpoint_id,
object_type,
logical_key
"""
rows = self.connection.execute(
sql,
tuple(parameters),
).fetchall()
return [
AppliedState(
endpoint_id=row["endpoint_id"],
object=NormalizedObject(
object_type=row["object_type"],
logical_key=row["logical_key"],
local_key=row["local_key"],
payload=_deserialize_payload(row["payload_json"]),
),
applied_at=_parse_timestamp(row["applied_at"]),
verification_attempted_at=_parse_timestamp(
row["verification_attempted_at"]
),
verified_at=_parse_timestamp(row["verified_at"]),
verification_error=row["verification_error"],
)
for row in rows
]
def observation_status(
self,
endpoint_id: str,