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>
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||
# IMAP connection-reuse spike (issue #125)
|
||||
|
||||
> **Update — shipped (issue #357 Part 2).** The real-device validation this spike deferred has since
|
||||
> run: an on-device drilldown proved Gmail server-side throttles LibreMail's connect-per-operation IMAP
|
||||
> (full-history backfill generated ~601 connections in ~22 min, tripping and sustaining a per-account
|
||||
> rate/bandwidth clamp; `live` peaked at only 5, so it is connection *volume*, not count). Connection
|
||||
> reuse is therefore now **ON by default**, gated by `BuildConfig.IMAP_CONNECTION_REUSE` as a safety
|
||||
> switch, with the cache hardened for production: transparent stale-connection recovery, idle eviction
|
||||
> (`ImapConnectionCache.evictIdle`, swept by `IdleService`), low-battery teardown, and per-account
|
||||
> mutex concurrency. The single-connection-vs-pool and per-provider-cap knobs below remain a separate
|
||||
> effort (#356/#360-#364); this change is connection reuse only. The sections below are the original
|
||||
> spike design, kept for context.
|
||||
|
||||
A time-boxed spike that **prototypes** the connection reuse the investigation
|
||||
(`issue-125-imap-folder-open.md`) recommended and defers. It exists to reduce uncertainty — *is
|
||||
per-account keep-alive feasible in this codebase, and does it actually collapse the per-open setup
|
||||
|
||||
Reference in New Issue
Block a user