Files
LibreMail/docs
JMR-devandClaude Opus 4.8 cc067408b8 perf(mail): enable IMAP connection reuse by default with a hardened cache
An on-device drilldown proved Gmail server-side throttles LibreMail's
connect-per-operation IMAP: every op was a fresh CONNECT+TLS+LOGIN, and
full-history backfill's body+attachment prefetch generated ~601 connections in
~22 min, tripping (and sustaining) Gmail's per-account rate/bandwidth clamp
(body download collapsed to ~4 KB/s). The `live` gauge peaked at only 5 (Gmail
allows ~15), so it is connection *volume*, not count. Outlook IMAP on the same
device opened in 2-3 s. Reusing one warm socket per account (~601 -> ~1) removes
the throttle's trigger. This wires the reuse path the #125 spike built and left
OFF (issue #357 Part 2 — connection reuse only; prefetch is a separate PR).

How it is enabled (with a safety switch):
- New `BuildConfig.IMAP_CONNECTION_REUSE` (default true) drives the production
  `ImapClient` no-arg `@Inject` constructor. To disable if a server misbehaves,
  flip it to "false" in app/build.gradle.kts — a build-config change, no Kotlin
  edit. The internal `ImapClient(reuseConnections, reuseIdleTimeoutMillis)`
  constructor stays the test/harness seam.
- Universal: applies to all providers (incl. Outlook). No per-provider caps or
  throttling here — that is a separate effort (#356/#360-#364).

Hardening `ImapConnectionCache` for production (was a spike):
- Transparent stale recovery: broadened drop detection to Angus's own
  `iap.ConnectionException` (and a MessagingException caused by one) — the real
  signal `folder.open()` throws on a server-dropped idle socket, which the
  IOException-only check missed, so the reconnect now actually fires. A dropped
  reused socket is rebuilt once and the op retried, so callers see no spurious
  error; a genuine app error (e.g. message-not-found) is never retried.
- Idle eviction: `evictIdle()` closes a connection unused past the reuse idle
  timeout (default 5 min), swept every 2 min by `IdleService`; skips any
  in-use connection.
- Teardown: `IdleService` also tears down reused connections on the low-battery
  push-teardown path (#88/#89/#90), mirroring the IDLE connection teardown.
- Concurrency: one connection per account behind a per-account mutex; the
  eviction sweep takes the lock non-blockingly so it never stalls or interrupts
  an in-flight op. Coexists with IMAP IDLE (its own separate connection).
- PII-free AppLog on the lifecycle (open / reuse-hit / reconnect-stale / evict /
  teardown) keyed by an opaque per-cache ordinal, plus the #358 ImapPerf
  breadcrumb (connect~=0ms on a reuse hit).

Tests (all via the fast gate, no emulator):
- ImapConnectionCacheTest: reuse, retry-once stale recovery, narrow drop
  detection, deterministic idle eviction (injected clock), teardown.
- ImapFolderOpenLatencyTest (GreenMail + counting proxy): N ops share one
  connection/LOGIN; a force-dropped socket is transparently reconnected; an app
  error does not reconnect; idle eviction LOGS-OUT and the next op reconnects.
- Correctness suites (ImapClientTest/ImapClientBackfillTest/MailBackfillerTest)
  pinned to reuse-off to keep their connect-per-op assertions unchanged.

Fast gate green: assembleDebug, testDebugUnitTest, compileDebugAndroidTestKotlin,
lintDebug, ktlintCheck, detekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:47:01 -05:00
..