fix(sync): gate PruneWorker & BackfillWorker on the encrypted-cache lock #229

Merged
JMR-dev merged 2 commits from fix-prune-backfill-cache-lock-gate into main 2026-07-03 16:14:49 +00:00
JMR-dev commented 2026-07-03 15:55:15 +00:00 (Migrated from github.com)

Fixes the latent thread-park found by the "same-class crash-state" audit (follow-up to the SQLCipher cold-start crash, #210).

Problem

PruneWorker and BackfillWorker were the only two pre-auth background DB entry points not gating on EncryptedCacheGuard.isCacheLocked() before opening the database (unlike SyncWorker / SendWorker / IdleService). With encryptCache + appLock on and a headless cold start where the user hasn't authenticated (WorkManager after reboot, or the periodic backfill/prune window while locked), the first DAO call runs prepareCache() → resolvePassphrase() → session.await(), parking the WorkManager thread until unlock and serializing other DB openers behind the held prepareCache mutex. Self-heals on unlock, but wastes wakelock/battery and makes zero prune/backfill progress while locked.

Fix

Switch MailPruner / MailBackfiller injection to dagger.Lazy and add the if (cacheGuard.isCacheLocked()) return Result.retry() guard before resolving it — mirroring SyncWorker / SendWorker.

Tests

New PruneWorkerTest / BackfillWorkerTest (JVM): locked → retry() with the Lazy dep never resolved (verify(exactly = 0) { lazyDep.get() }); unlocked → runs; failure → retry(). Preflight green (build, unit tests, androidTest compile, lint, ktlint, detekt).

Broader coverage (all pre-auth entry points + the suspending-prepareCache provisioner case) is tracked in #225; instrumented coverage in #226.

Closes #224

🤖 Generated with Claude Code

Fixes the latent thread-park found by the "same-class crash-state" audit (follow-up to the SQLCipher cold-start crash, #210). ## Problem `PruneWorker` and `BackfillWorker` were the only two pre-auth background DB entry points **not** gating on `EncryptedCacheGuard.isCacheLocked()` before opening the database (unlike `SyncWorker` / `SendWorker` / `IdleService`). With `encryptCache + appLock` on and a headless cold start where the user hasn't authenticated (WorkManager after reboot, or the periodic backfill/prune window while locked), the first DAO call runs `prepareCache()` → `resolvePassphrase()` → `session.await()`, **parking the WorkManager thread** until unlock and serializing other DB openers behind the held `prepareCache` mutex. Self-heals on unlock, but wastes wakelock/battery and makes zero prune/backfill progress while locked. ## Fix Switch `MailPruner` / `MailBackfiller` injection to `dagger.Lazy` and add the `if (cacheGuard.isCacheLocked()) return Result.retry()` guard before resolving it — mirroring `SyncWorker` / `SendWorker`. ## Tests New `PruneWorkerTest` / `BackfillWorkerTest` (JVM): locked → `retry()` with the `Lazy` dep **never resolved** (`verify(exactly = 0) { lazyDep.get() }`); unlocked → runs; failure → `retry()`. Preflight green (build, unit tests, androidTest compile, lint, ktlint, detekt). Broader coverage (all pre-auth entry points + the suspending-`prepareCache` provisioner case) is tracked in #225; instrumented coverage in #226. Closes #224 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.