diff --git a/docs/sync-and-op-log/vector-clocks.md b/docs/sync-and-op-log/vector-clocks.md index bab29beee7..217cc977c9 100644 --- a/docs/sync-and-op-log/vector-clocks.md +++ b/docs/sync-and-op-log/vector-clocks.md @@ -433,3 +433,46 @@ via **Dotted Version Vectors** (bound to server vnodes, not devices), **bounded reclaimable client IDs** (needs a registration/retirement protocol), or **periodic stable-cut GC** (needs all-to-all clock reporting). None apply to the current dumb-relay model. + +### Future option: staleness-informed eviction (issue #9105 — works in the dumb-relay model) + +Pruning today evicts the **lowest-counter** entries, but a low counter +correlates with _importance_ (a fresh import author has counter 1), not with +_deadness_ — the heuristic behind the #9089/#9096 preserve-set bugs. Issue +#9105 tracks the root cause: client IDs are minted per install/profile and +retired almost never, so clocks only grow toward MAX. The decision on #9105 +was to **park** the fix — post #9089/#9102 the worst case is the benign extra +round-trip of §5 — and record the agreed direction here. + +If pruning stops being rare in practice, evict the **stalest** entries instead +of the lowest-counter ones. Unlike the coordinator options above, this fits +the dumb-relay model with no wire-format change: + +- **Server:** the `sync_devices` table already stores `lastSeenAt` per + `(userId, clientId)`, updated on every upload — and uploads are the only + path that creates clock entries. A daily job already GCs rows unseen for + `retentionMs` (45 days), so absence from the registry reads as "stalest". +- **Client (all providers):** keep a small durable `clientId → last-merged-op +time` map, updated where remote clocks are merged (`mergeRemoteOpClocks`) — + every merged op carries its author's ID. Needs no server support, so it + covers WebDAV / LocalFile / Dropbox too. + +The safety profile is identical to today's pruning (entries are dropped either +way; a dropped ID that returns costs at most the extra round-trip of §5), but +victim selection is strictly better: a recently-seen ID — e.g. a fresh import +author — survives by definition, making the preserve-set invariant of +#9089/#9096 _emergent_ instead of hand-maintained at each prune site (the +explicit preserve sets stay as belt-and-braces). Staleness knowledge differs +per node, so nodes may evict different victims; that adds clock asymmetry but +no new failure class — comparison treats missing keys as zero, and clients +already prune with differing preserve sets. + +The supported GC today is a **full-state import**: the clock reset keeps only +`{import author, self}` (§7), and the once-per-session pruning snack points +users at it (sync all devices first — imports intentionally drop concurrent +ops, see `SyncImportFilterService`). + +**Revisit trigger:** client pruning WARN-logs `prunedIds` / `survivingIds` +into the exportable log history. If prune warnings appear in real bug +reports — especially ones evicting _live_ IDs — promote this from parked to +scheduled. diff --git a/src/app/imex/sync/sync.effects.ts b/src/app/imex/sync/sync.effects.ts index 80326e09ed..e18d640358 100644 --- a/src/app/imex/sync/sync.effects.ts +++ b/src/app/imex/sync/sync.effects.ts @@ -119,15 +119,15 @@ export class SyncEffects { this._snackService.open({ msg: T.F.SYNC.S.VECTOR_CLOCK_LIMIT_REACHED, // CUSTOM (not WARNING): this is a benign, self-healing cleanup, not - // an alert the user must act on. Neutral icon, auto-dismiss — but a - // touch longer than the 3s default so the sentence is readable. + // an alert the user must act on. Neutral icon, auto-dismiss — long + // enough to read the optional clear-for-good remedy (#9105). type: 'CUSTOM', ico: 'sync', translateParams: { originalSize, maxSize, }, - config: { duration: 5000 }, + config: { duration: 8000 }, }); }), ), diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4df195b9e7..711436ac77 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1668,7 +1668,7 @@ "UNKNOWN_ERROR": "Unknown Sync Error: {{err}}", "UPLOAD_ERROR": "Unknown Upload Error (Settings correct?): {{err}}", "UPLOAD_OPS_REJECTED": "{{count}} operation(s) were permanently rejected by the server and won't be synced.", - "VECTOR_CLOCK_LIMIT_REACHED": "Sync tidied up old device entries ({{originalSize}} → {{maxSize}}). This is normal — no action needed.", + "VECTOR_CLOCK_LIMIT_REACHED": "Sync tidied up old device entries ({{originalSize}} → {{maxSize}}). This is normal. To clear them for good: sync all devices, then export and re-import a backup (Settings → Import/Export).", "VERSION_TOO_OLD": "Your app version is too old for the synced data. Please update!", "VERSION_UNSUPPORTED": "Cannot sync: some synced data is from an app version that is no longer supported. Sync is paused before it; updating the app on your other devices may help.", "WEB_CRYPTO_NOT_AVAILABLE": "Encryption is not available on this device. On Android, encryption requires a secure context (HTTPS) which is not available. Please disable encryption in sync settings or sync from a desktop web browser."