-
released this
2026-06-25 12:42:04 -04:00 | 3 commits to main since this releaseAdded — Mirror (dual-send)
- Optional best-effort mirror server. Each heartbeat and each forwarded event can now be sent to a second ("mirror") server in addition to the primary, so a standby box (e.g. the office NAS) stays a continuous replica during a migration — making the eventual cutover a non-event. Default off — blank
mirror_api_url/mirror_sfm_urlmeans existing installs are unchanged after auto-update. - Heartbeat mirror (best-effort). After the primary heartbeat POST, the same payload is fired at
mirror_api_urlwhen set. Bounded by the existing API timeout and fully wrapped — it can never delay or fail the primary heartbeat; its result is ignored except for a debug log line. - Event mirror (reliable, isolated). Events are re-forwarded to
mirror_sfm_urlusing a separate sha256 state file (mirror_sfm_state_file, blank →<log_dir>/thor_forwarded_mirror.json), so the mirror tracks its own delivery independently of the primary. A quick reachability probe (~3 s) skips the mirror pass when the mirror server is down, so a dead mirror never stalls the loop on per-event timeouts; skipped events stay pending in the mirror state and deliver when the mirror returns — no data loss. - Isolation invariant (the one rule). Nothing on the mirror path can delay or fail the primary: its own state file, its own try/except, the reachability guard + bounded timeouts, all exceptions swallowed-and-logged. The primary path stays exactly as reliable as before.
- Mirror tab in the Settings dialog with
Mirror API URL+Mirror SFM URLfields (blank = off). The mirror rides along with the primary — there is no separate enable flag (heartbeat-mirror active iff the primary heartbeat is on andmirror_api_urlis set; event-mirror active iff primary forwarding is on andmirror_sfm_urlis set). - New unit tests in
test_event_forwarder.pycovering mirror reachability, the down-mirror skip, separate-state idempotency, and the isolation invariant (a mirror failure leaves the primary's result + state untouched).
Configuration
New
config.jsonkeys (all default-empty = off; existing 0.3.x deployments don't change behaviour on auto-update):Key Default Notes mirror_api_url""Second heartbeat destination — same URL form as api_url(blank = off)mirror_sfm_url""Second SFM base URL, e.g. http://10.0.0.x:8200(blank = off)mirror_sfm_state_file""Override the mirror's forwarded-sha256 state file. Blank → <log_dir>\thor_forwarded_mirror.jsonChanged
- Bumped
VERSIONto0.4.0
Operational note (migration seeding). To make the mirror re-deliver only the gap since a one-time replica snapshot, copy the primary state file to the mirror state file and drop entries whose
forwarded_atis after the snapshot. SFM-side dedup covers any overlap. Seedocs/mirror-dual-send-design.md.Downloads
- Optional best-effort mirror server. Each heartbeat and each forwarded event can now be sent to a second ("mirror") server in addition to the primary, so a standby box (e.g. the office NAS) stays a continuous replica during a migration — making the eventual cutover a non-event. Default off — blank