Reconcile lost upload responses without replaying frame writes
This commit is contained in:
@@ -7,6 +7,7 @@ from uuid import uuid4
|
||||
|
||||
from pigway_frame_flow.display.cloud_library import CloudLibrary
|
||||
from pigway_frame_flow.display.retention import Retention
|
||||
from pigway_frame_flow.display.upload_reconciliation import UploadReconciliation
|
||||
from pigway_frame_flow.drivers.base import DriverRegistry
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
@@ -49,6 +50,7 @@ class DisplayService:
|
||||
self.lock = threading.RLock()
|
||||
self.retention = Retention(self)
|
||||
self.cloud_library = CloudLibrary(self)
|
||||
self.upload_reconciliation = UploadReconciliation(self)
|
||||
self.expiration_checked: dict[str, float] = {}
|
||||
self.unknown_checked: dict[str, float] = {}
|
||||
# A killed write cannot be replayed safely; pre-write work can resume.
|
||||
@@ -169,8 +171,9 @@ class DisplayService:
|
||||
if result:
|
||||
row = self.db.one("SELECT result FROM operations WHERE id=?", (id,))
|
||||
previous = json.loads(row["result"]) if row and row["result"] else {}
|
||||
if "photo_attempts" in previous:
|
||||
result = {**result, "photo_attempts": previous["photo_attempts"]}
|
||||
for key in ("photo_attempts", "upload_intent"):
|
||||
if key in previous:
|
||||
result = {**result, key: previous[key]}
|
||||
self.db.execute(
|
||||
"UPDATE operations SET status=?,updated_at=?,result=COALESCE(?,result),error=? WHERE id=?",
|
||||
(
|
||||
@@ -627,6 +630,8 @@ class DisplayService:
|
||||
raise FlowError(
|
||||
E.CONFLICT, "Temporary override requires an existing image", 409
|
||||
)
|
||||
if asset:
|
||||
self.upload_reconciliation.prepare(id, driver, asset, previous)
|
||||
self._start_write(id)
|
||||
if asset:
|
||||
self.update(id, "UPLOADING")
|
||||
@@ -690,22 +695,31 @@ class DisplayService:
|
||||
)
|
||||
|
||||
def sync(self):
|
||||
with self.lock:
|
||||
self._sync()
|
||||
|
||||
def _sync(self):
|
||||
# Late acknowledgements only resolve a known display target. Never replay
|
||||
# writes or infer success from gallery presence alone.
|
||||
for op in self.db.all(
|
||||
"SELECT * FROM operations WHERE status='UNKNOWN' AND action='display' AND write_started=1"
|
||||
):
|
||||
expected = json.loads(op["result"] or "{}").get("image_id")
|
||||
if not expected:
|
||||
intent = json.loads(op["result"] or "{}").get("upload_intent")
|
||||
if not expected and not intent:
|
||||
continue
|
||||
checked = time.monotonic()
|
||||
if checked - self.unknown_checked.get(op["id"], float("-inf")) < 60:
|
||||
continue
|
||||
self.unknown_checked[op["id"]] = checked
|
||||
try:
|
||||
device = self.registry.get(op["device_id"]).device(refresh=True)
|
||||
driver = self.registry.get(op["device_id"])
|
||||
device = driver.device(refresh=True)
|
||||
if intent:
|
||||
expected = self.upload_reconciliation.check(op, driver, device)
|
||||
if (
|
||||
device.current_content.id == expected
|
||||
expected
|
||||
and device.current_content.id == expected
|
||||
and device.current_content.synced
|
||||
):
|
||||
self.update(op["id"], "COMPLETED")
|
||||
|
||||
Reference in New Issue
Block a user