Files
pad/cmd
xarmian 028db39217 feat(collab): periodic op-log GC sweeper for dormant items (TASK-1309) (#471)
The Yjs collab dumb-relay accumulates op-log rows indefinitely in
item_yjs_updates. DOC-1307 surfaced 45-second p50 cold-reconnect
latency on a single item with 5000 accumulated rows. Without GC,
busy items keep growing.

This adds a periodic background sweeper that prunes the entire
op-log for items that are both DORMANT (no recent activity) AND
FULLY FLUSHED (items.content has captured every op-log row).
Whole-log only — Yjs op streams are causally linked, prefix-pruning
corrupts replay; future cold connects lazy-seed from items.content.

Components:
- Store.ListDormantOpLogItemsBefore (joins items, filters watermark)
- Store.PruneItemOpLogIfDormantBefore (atomic conditional DELETE)
- Store.GetItemContentFlushedOpLogID (per-item watermark getter)
- RoomManager.PruneSweep (per-item-locked, active-room-skip)
- Server.StartOpLogGC / stopOpLogGC (mirrors orphan_gc.go pattern)
- cmd/pad/main.go env vars PAD_OPLOG_GC_INTERVAL / PAD_OPLOG_GC_MIN_AGE
- New (item_id, created_at) index for the dormancy query
- New items.content_flushed_op_log_id column (id-based watermark,
  monotonic, no clock-skew or second-granularity false positives)
  + content_flushed_at (informational timestamp)

Watermark policy:
- Server-driven full-content writes (CLI / MCP / version restore /
  PruneAndApply) advance content_flushed_op_log_id to MAX(op-log.id)
  via subquery, atomic with the content UPDATE
- Browser collab-snapshot 5s flushes do NOT advance the watermark —
  they can't prove their markdown captured every peer's ops, so
  letting them stamp would risk later GC-pruning unsynced peer edits
- Schema-mismatch rebuild (TASK-1268) logs a WARN when it drops
  unflushed ops (data loss is unavoidable on schema bumps but
  visible)

Stop ordering: collab.Close() now runs BEFORE bg.Wait() so a GC
goroutine waiting on an itemLock behind an active Join can drain.

Migration backfill: items WITH existing op-log rows keep NULL
watermark (don't certify); items WITHOUT op-log rows get a
synthetic 0 watermark (vacuous, harmless — no rows to compare
against).

Tests:
- 6 RoomManager.PruneSweep tests (dormant prune / default minAge /
  empty / bails-on-Close / skips-active-room / skips-row-added-mid-
  sweep via fakeOpLog hook)
- 5 Server.OpLogGC tests (prunes-dormant / start-idempotent /
  preserves-unflushed / backfill-doesnt-certify-unflushed /
  no-collab-noop)
- TestCollabSnapshotDoesNotAdvanceOpLogWatermark in store
- TestCollabSnapshotQueryOverridesBodyVersionSource in server
  (regression for body-attacker bypass)

Seven rounds of Codex review — caught 5 P1s and 4 P2s I would have
shipped under self-review:
1. Prefix-prune corrupts Yjs replay
2. Stop ordering deadlock
3. Missing index
4. Best-effort flush ⇒ data loss
5. Backfill over-certifies via metadata-PATCH
6. Second-granularity timestamp comparison
7. Schema-mismatch path drops unflushed silently
8. Browser flush stamps watermark beyond Y.Doc
9. Body version_source bypasses server policy
2026-05-09 18:19:33 -04:00
..