fix(daemon): heartbeat empty /sync to cloud so brains.last_sync_at advances#198
Conversation
…at advances When the dashboard 'Sync Now' button is pressed and there are no new local events past the watermark, the daemon was short-circuiting and returning success WITHOUT calling the cloud. This left brains.last_sync_at stale, so the dashboard kept showing 'last sync Nh ago' even though the button worked. Fix: still POST an empty payload to api.gradata.ai/api/v1/sync as a heartbeat. The cloud sync endpoint already updates last_sync_at to now() in the no-data path. Verified end-to-end: cloud brain row advanced from 08:25 UTC to 23:43 UTC after one button press.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary
WalkthroughThe ChangesEmpty sync heartbeat POST
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
Suggested labels
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.20.0)OpenGrep fatal error (exit code 2): �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m Comment |
…194) (#200) * feat(sync): write-through Brain.correct() -> /api/v1/ingest (day 3 of #194) Every Brain.correct(draft, final) now enqueues the correction into a local sync_queue table and a background daemon thread drains the queue by POSTing each row to api.gradata.ai/api/v1/ingest. Removes the dependency on session-end hooks and the cron sync band-aid. See /home/olive/gradata-office-hours-memo.md for the architectural rationale. ## Components - src/gradata/_sync_queue.py: enqueue/peek/mark_synced/mark_failed CRUD + enqueue_correction helper - src/gradata/_sync_worker.py: daemon-thread SyncWorker with start/stop(drain)/_tick, handles 2xx + dedup + 422 poison + 429 backoff + 5xx + network - src/gradata/brain.py: Brain.__init__ starts worker when API key resolvable AND GRADATA_DISABLE_WRITE_THROUGH != 1; Brain.correct enqueues post-local-write; Brain.close drains and stops - src/gradata/_migrations/__init__.py + onboard.py: sync_queue table + idx_sync_queue_pending (idempotent, no-op if #195 lands first) ## Env vars - GRADATA_API_KEY (existing): write-through requires it - GRADATA_DISABLE_WRITE_THROUGH=1: opt-out, fall back to hook+cron path - GRADATA_CLOUD_INGEST_URL (default https://api.gradata.ai/api/v1/ingest) - GRADATA_SYNC_TICK_SEC (default 30) ## Failure modes - Cloud unreachable: row stays pending, retried next tick - 422 permanent: poison row marked synced + failed so it doesn't block batch - 429: bail batch, retry next tick - No API key: worker doesn't start, no enqueue, local correct() unaffected - _write_through_enqueue exception: caught, local correct() always succeeds ## Tests - tests/test_sync_queue.py (9): existing CRUD primitives + enqueue_correction - tests/test_sync_worker.py (9): HTTPServer stub /ingest with all status branches + at-least-once + stop-drain - tests/test_brain_write_through.py (5): enqueue happy path, disabled, no-key, cloud-unreachable, sabotaged enqueue 38 passed locally. Series: #195 (day 1 SDK queue), Gradata/gradata-cloud#58 merged (day 2 cloud receiver), this PR (day 3 wire-up). * fix(tests): update test_sync_with_no_events_returns_zero for PR #198 heartbeat PR #198 changed empty /sync behavior to POST an empty heartbeat to cloud so brains.last_sync_at advances. The pre-#198 test still asserted mock_post.assert_not_called() — now expects exactly one call. --------- Co-authored-by: data-engineer <data-engineer@gradata.ai>
Bug
Dashboard 'Sync Now' returned 'Synced 0 events' (success) but 'last sync' stayed at e.g. 15h ago because the daemon short-circuited the empty-payload case and never hit the cloud. brains.last_sync_at therefore stayed stale.
Fix
When no new local events past the watermark, still POST an empty payload to api.gradata.ai/api/v1/sync as a heartbeat. The cloud /sync endpoint already updates brains.last_sync_at to now() in this path. Failures are logged at info level (non-fatal).
Verify
Before:
3b49e9c6 last_sync= 2026-05-15T08:25:36+00:00
After one POST /sync with watermark=329 (no new events):
3b49e9c6 last_sync= 2026-05-15T23:43:58+00:00
Follow-up to #196 and #197.