From 581f337021a1d2c9f7e66854e91fe187e69ba0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=B0=BD=EC=9A=B1?= Date: Sun, 12 Jul 2026 11:07:27 +0900 Subject: [PATCH] fix(edge): daemon periodic ops (sync/heartbeat/flush) can't crash the loop Wrap sync_config, send_heartbeat, and queue.flush operations in separate try/except to ensure daemon survives periodic operation exceptions. Critical for unattended 24/7 pilot devices where a locked database or disk error would otherwise kill the entire loop. Per-frame step() handling unchanged. --- edge/daemon.py | 27 +++++++++++++++------------ tests/test_daemon.py | 13 +++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/edge/daemon.py b/edge/daemon.py index 408892b..7bd04a3 100644 --- a/edge/daemon.py +++ b/edge/daemon.py @@ -13,18 +13,21 @@ def run_loop(runtime, client, queue, *, clock, sleep, iterations=None, counters["frames"] += 1 except Exception: counters["errors"] += 1 - if sync_every and i % sync_every == 0: - cfg = client.fetch_config() - if cfg is not None: - from edge.zones import zones_from_config - runtime.apply_zones(zones_from_config(cfg)) - counters["syncs"] += 1 - if heartbeat_every and i % heartbeat_every == 0: - if client.send_heartbeat({"fps": 0.0, "frames": counters["frames"]}): - counters["beats"] += 1 - if flush_every and i % flush_every == 0: - queue.flush() - counters["flushes"] += 1 + try: + if sync_every and i % sync_every == 0: + cfg = client.fetch_config() + if cfg is not None: + from edge.zones import zones_from_config + runtime.apply_zones(zones_from_config(cfg)) + counters["syncs"] += 1 + if heartbeat_every and i % heartbeat_every == 0: + if client.send_heartbeat({"fps": 0.0, "frames": counters["frames"]}): + counters["beats"] += 1 + if flush_every and i % flush_every == 0: + queue.flush() + counters["flushes"] += 1 + except Exception: + counters["errors"] += 1 if frame_interval: sleep(frame_interval) return counters diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 1a675be..2d01880 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -41,3 +41,16 @@ def test_loop_survives_step_exception(): sync_every=99, heartbeat_every=99, flush_every=99) assert rt.steps == 3 # kept going past the exception assert counters["errors"] == 1 + + +def test_loop_survives_flush_exception(): + class BoomQueue(_Queue): + def flush(self): + self.flushes += 1 + raise RuntimeError("db locked") + rt = _Runtime() + counters = run_loop(rt, _Client(), BoomQueue(), clock=lambda: 0.0, + sleep=lambda s: None, iterations=5, + sync_every=99, heartbeat_every=99, flush_every=2) + assert rt.steps == 5 # loop kept running past the flush exceptions + assert counters["errors"] >= 1