v0.4.0 - dual send mirror implementation #6

Merged
serversdown merged 9 commits from dev into main 2026-06-25 12:36:43 -04:00
Owner

[0.4.0] - 2026-06-25

Added — 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_url means existing installs are unchanged after auto-update.
  • Heartbeat mirror (best-effort). After the primary heartbeat POST, the same payload is fired at mirror_api_url when 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_url using 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 URL fields (blank = off). The mirror rides along with the primary — there is no separate enable flag (heartbeat-mirror active iff the primary heartbeat is on and mirror_api_url is set; event-mirror active iff primary forwarding is on and mirror_sfm_url is set).
  • New unit tests in test_event_forwarder.py covering 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.json keys (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.json

Changed

  • Bumped VERSION to 0.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_at is after the snapshot. SFM-side dedup covers any overlap. See docs/mirror-dual-send-design.md.

## [0.4.0] - 2026-06-25 ### Added — 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_url` means existing installs are unchanged after auto-update. - **Heartbeat mirror (best-effort).** After the primary heartbeat POST, the same payload is fired at `mirror_api_url` when 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_url` using 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 URL` fields (blank = off). The mirror rides along with the primary — there is no separate enable flag (heartbeat-mirror active iff the primary heartbeat is on *and* `mirror_api_url` is set; event-mirror active iff primary forwarding is on *and* `mirror_sfm_url` is set). - New unit tests in `test_event_forwarder.py` covering 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.json` keys (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.json` | ### Changed - Bumped `VERSION` to `0.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_at` is after the snapshot. SFM-side dedup covers any overlap. See `docs/mirror-dual-send-design.md`.
serversdown added 8 commits 2026-06-25 11:09:03 -04:00
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional second destination so each heartbeat and event is posted to a
mirror server (the office NAS) alongside the primary — dual-write to
de-risk the migration. Default off (blank mirror URLs); existing installs
unchanged.

- event_forwarder: mirror_reachable() fast-fail probe + mirror_forward_pass()
  (reliable event mirror with its OWN state file, total exception isolation,
  never raises into the primary path).
- series4_ingest: mirror_api_url/mirror_sfm_url/mirror_sfm_state_file config;
  best-effort heartbeat mirror; isolated event-mirror pass after the primary.
- settings dialog: new 'Mirror' tab.
- 9 new tests incl. the isolation invariant (down mirror = fast no-op that
  never touches primary state). 42 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewed-on: #5
Version bump across series4_ingest.py / tray / settings dialog / installer.iss.
CHANGELOG + README document the optional best-effort mirror server
(mirror_api_url / mirror_sfm_url / mirror_sfm_state_file, default off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
serversdown added 1 commit 2026-06-25 11:17:20 -04:00
main's v0.3.x event-forwarding (delivered via the event-db branch) is
content-identical to dev's, except for older version strings and the
pre-fix MLG timestamp handling that dev's 4742ed9 already superseded.
Keep dev's tree verbatim (-s ours); this only records main as merged so
the dev->main PR for v0.4.0 (mirror dual-send) merges without conflicts.
serversdown merged commit baf8aade2d into main 2026-06-25 12:36:43 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: serversdown/thor-watcher#6