MailMaintenanceGate serializes only the backfill↔prune pair. The other two
pairs — sync↔backfill and sync↔prune — are deliberately ungated (foreground
sync uses its own syncMutex to stay UI-responsive) and rely on a disjoint-by-UID
argument for safety, with no test covering it (issue #53).
Add MailSyncConcurrencyTest: a real MailSyncer and a real MailBackfiller/
MailPruner wired to one shared in-memory message store, with CompletableDeferred
gates (mirroring MailMaintenanceGateTest) that park one actor mid-critical-
section while the other's whole critical section runs. Each interleaving is
driven to the boundary (window edge / count floor) where an overlap would
surface as a lost, duplicated, or wrongly-deleted row:
- sync's windowed reconcile runs while a backfill is parked mid-paging just
below the window (tightest edge: lowestSyncedUid == minWindowUid);
- a backfill's below-window page lands after a concurrent full sync of the
window (stale-boundary ordering);
- a count-retention prune runs while a foreground sync is parked in its fetch;
- a full foreground sync runs while a prune is parked inside its critical
section, before it touches the message table.
All four pass: the disjointness invariant holds unlocked. No production code
changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>