Skip to content

COR-205 - IMAP: prefer a live connection when leasing one from the pool - #111

Merged
dbezverkhnii merged 3 commits into
spark2from
feature/COR-205-prefer-live-connection
Sep 14, 2026
Merged

dbezverkhnii merged 3 commits into
spark2from
feature/COR-205-prefer-live-connection

Conversation

@dbezverkhnii

@dbezverkhnii dbezverkhnii commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

IMAPAsyncSession::sessionWithMinQueue ranks pooled connections by queue length alone. A connection whose socket mailcore dropped after its idle delay therefore looks exactly as free as a warm one (operationsCount() == 0), and ties go to position in mSessions. With one warm and one cold connection free, the first asker — the most urgent one, since the queue starts it first — can be handed the cold socket and pay a full CONNECT + LOGIN, while the operation that asks second gets the warm one. A lease makes that stick for the whole operation.

Measured on Yahoo from the desktop client: a body on a warm connection answers in 0.33 s (UID FETCH … BODY.PEEK goes out on the same millisecond as the lease); the connection that has to come back up spends 2.36 s on the greeting, CAPABILITY, authentication, SELECT, COMPRESS, NAMESPACE and ID before it can ask for anything.

Change

Among equally free candidates — same queue length — prefer the one whose session is connected. The tie only: a shorter queue still wins outright, connected or not. Queue length counts commands without weighing them, so trading one unknown command against a bounded handshake would be the worse deal, and a lease keeps being handed an empty queue.

IMAPAsyncConnection::isDisconnected() exposes what IMAPSession already tracks; no other new API. With no disconnected candidate in the pool the loop is byte-for-byte what it was: the new clause can only fire when the chosen connection is disconnected, which cannot happen before the first assignment.

Tests

unittest/IMAPConnectionLeaseTests.swift — both pool positions, both acquireConnection paths (folder-matched and plain), a never-connected connection, the unpinned regular operation, and the all-reserved fallback; plus the guards for what must not change: a shorter queue wins even when it is the disconnected one, and two live idle connections keep their pool order. Five of them fail on this branch's base commit.

Whole suite: 23 in the lease file, all green; the only failure in the repository is the pre-existing testSummary (date locale).

Stand

Verified end-to-end against live Yahoo in the desktop client (mailcore pinned to this branch): the wire capture above is from that run, with the lease going to the keep-alive-warmed connection and the second, concurrent request paying the handshake. No regressions in either pool over ~25 minutes.

Worth stating plainly: the stand did not catch the old code making the wrong choice. Keep-alive warms a connection through the same acquireConnection, so the warm one is usually also the first free one in mSessions — exactly what the old rule picks. The two diverge only when the warm connection sits later in the array, a state that does occur (after an OAuth refresh recreates the pool, keep-alive held a later connection warm for ten minutes) but that no body fetch landed in during the runs.

🤖 Generated with Claude Code


Note

Medium Risk
Changes core async IMAP connection-pool selection and cross-thread session state reads; behavior is constrained to queue-length ties but affects latency-sensitive paths.

Overview
When two IMAP pool connections are equally idle (same queue length), connection selection now favors one that is still connected instead of breaking ties purely by pool order. That avoids handing the first lease or unpinned operation a socket that must pay a full connect/login handshake while a warm connection sits unused.

IMAPAsyncConnection::isDisconnected() exposes existing session liveness for that tie-break (appended last on the exported class to avoid shifting vtables). IMAPSession::mState is now std::atomic<int> so pool selection on another thread can read disconnect state safely while the connection thread updates it.

Shorter queues still win outright—the live preference applies only on ties. unittest/IMAPConnectionLeaseTests.swift adds coverage for leases, folder acquire, unpinned ops, exhausted-pool sharing, and guards that queue length and live-only ties behave as before.

Reviewed by Cursor Bugbot for commit fe7f5fa. Bugbot is set up for automated code reviews on this repo. Configure here.

dbezverkhnii and others added 2 commits September 14, 2026 13:25
… one COR-205

sessionWithMinQueue ranked connections by queue length alone, so a connection
whose socket mailcore dropped after its idle delay looked exactly as free as a
warm one and ties went to position in mSessions. With one warm and one cold
connection free, the first and most urgent asker paid for a CONNECT + LOGIN
about half the time - 2.8s against 0.27s on Yahoo - while the operation that
asked second got the warm one.

Among equally free connections the live one now wins. The tie only: a shorter
queue still wins outright, connected or not, so a lease keeps being handed an
empty queue and a command of unknown length is never traded against a bounded
handshake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both pool positions and both acquireConnection paths, the never-connected
connection, the unpinned operation and the all-reserved fallback, plus the
guards for what must not change: a shorter queue wins even when it is the
disconnected one, and two live idle connections keep their pool order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical findings identify an unsynchronized state read and ABI-breaking virtual-method changes.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates IMAP connection-pool selection to prefer connected sessions when queue lengths tie, reducing reconnect latency.

Changes:

  • Adds connection liveness accessors.
  • Applies liveness as a queue-selection tie-breaker.
  • Expands lease and connection-selection tests.
File summaries
File Summary
unittest/IMAPConnectionLeaseTests.swift Adds coverage for live versus disconnected connection selection.
src/include/MailCore/MCIMAPAsyncConnection.h Declares the liveness API; critical ABI-breaking virtual-method insertion (1 vote).
src/async/imap/MCIMAPAsyncSession.cpp Applies the connected-session tie-breaker.
src/async/imap/MCIMAPAsyncConnection.h Declares the internal method; critical data-race and ABI findings (1 vote each).
src/async/imap/MCIMAPAsyncConnection.cpp Forwards liveness state; critical unsynchronized state access/data-race finding (2 votes).
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/async/imap/MCIMAPAsyncConnection.h Resolved
Comment thread src/async/imap/MCIMAPAsyncConnection.h Resolved
…COR-205

Review of #111: the selection reads IMAPSession's state from the thread that
starts an operation, while the session writes it on its own; a plain int makes
that a data race rather than the benign staleness the comment claimed. The
field is atomic now, which leaves every existing read and write as it was
written and costs nothing on the paths that touch it - same size, same
alignment, lock-free for an int.

isDisconnected() also moved to the end of IMAPAsyncConnection: declared where
it was, it shifted the vtable slot of operationsCount() and of every virtual
after it, which an exported class does not get to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dbezverkhnii

Copy link
Copy Markdown
Collaborator Author

Both review findings are fixed in fe7f5fa.

Data race on the connection state. Right — the selection reads it from whatever thread starts an operation, while IMAPSession writes it on its own, so a plain int makes it a race and not the benign staleness the header claimed. mState is now std::atomic<int>: every existing read and write stays exactly as written, the field keeps its size and alignment, and it is lock-free for an int. Locking the read alone would have been theatre — the writers do not take that lock either.

Vtable layout. Also right, and free to fix: isDisconnected() now sits after every existing virtual in IMAPAsyncConnection, so operationsCount() and everything after it keep their slots. Both header copies stay byte-identical.

Lease suite is 23/23; the only failure in the repository is the pre-existing testSummary (date locale).

@dbezverkhnii

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fe7f5fa. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Publish the corresponding Windows prebuilt artifact before merging.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/async/imap/MCIMAPAsyncSession.cpp Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Core async pool selection and cross-thread state changes warrant final human review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@dbezverkhnii
dbezverkhnii merged commit 83e8afe into spark2 Sep 14, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants