COR-205 - Rank a pooled connection by the reconnect it owes - #112
Conversation
…R-205 The tie-break asked isDisconnected(), which is only mState == STATE_DISCONNECTED. Nothing but the constructor and unsetup() ever writes that state: a command that fails or is interrupted leaves the session LOGGEDIN or SELECTED and raises mShouldDisconnect instead, and connectIfNeeded then pays disconnect + connect + login on the next command - strictly more than a closed socket costs. The pool therefore ranked its most expensive connection as its cheapest and, at equal queue length, handed that one to the lease every time; before the tie-break it was at least a coin flip on pool order. The state is a normal one, not an edge case: interruptCurrentCommand raises the flag deliberately and leaves the connection pooled to heal on its next command, which is exactly the connection a lease is likely to meet. needsReconnect() answers the question the selection actually has. isDisconnected() keeps its meaning - the idle timer asks it whether the socket is already gone, and a flagged connection still holds one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one case the socket state alone gets backwards, and the eight tests of the tie-break did not cover it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
🟡 Changes recommended
Preserve existing exported virtual API/vtable compatibility before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates IMAP connection-pool selection to prefer connections that do not require reconnection after failures or interruptions.
Changes:
- Adds reconnect-aware session state tracking.
- Uses reconnect readiness for equal-queue tie-breaking.
- Adds regression coverage for interrupted connections.
- Requires preserving existing exported virtual API/vtable compatibility.
File summaries
| File | Summary |
|---|---|
unittest/IMAPConnectionLeaseTests.swift |
Adds interrupted-connection leasing coverage. |
src/include/MailCore/MCIMAPSession.h |
Exposes reconnect state and atomic flag. |
src/include/MailCore/MCIMAPAsyncConnection.h |
Updates the async connection API. |
src/core/imap/MCIMAPSession.h |
Mirrors session declarations. |
src/core/imap/MCIMAPSession.cpp |
Implements reconnect-state detection. |
src/async/imap/MCIMAPAsyncSession.cpp |
Applies reconnect readiness during selection. |
src/async/imap/MCIMAPAsyncConnection.h |
Mirrors the async API change. |
src/async/imap/MCIMAPAsyncConnection.cpp |
Forwards reconnect-state detection. |
Review details
Suppressed comments (2)
src/core/imap/MCIMAPSession.h:319
- The concurrency comment immediately above still names
IMAPAsyncConnection::isDisconnected, even though selection now callsneedsReconnect()and reads this atomic flag as well. Update the comment in both header copies to document the actual cross-thread predicate.
std::atomic<bool> mShouldDisconnect;
src/include/MailCore/MCIMAPSession.h:319
- The concurrency comment immediately above still says selection reads
mStatethroughIMAPAsyncConnection::isDisconnected, but selection now callsneedsReconnect()and also readsmShouldDisconnect. Update that rationale in both header copies so it does not document the old predicate.
std::atomic<bool> mShouldDisconnect;
- Files reviewed: 8/8 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.
…OR-205 needsReconnect() is only as good as the flag it reads, and two commands reported ErrorConnection without setting it: NOOP, and the delimiter LIST inside login(). Every other command in the file arms the flag on that branch. NOOP is the one that matters here - it is what the render pool's keep-alive runs, so a keep-alive that met a dead socket left the connection pooled, LOGGEDIN and looking ready to the selection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up from a zero-context review of this branch, landed in b2bb1e2. The flag had two holes. Test. The interrupted-connection test answers LOGIN and its follow-ups now, so the command whose stream dies is the NOOP on a fully logged-in connection rather than the LOGIN itself. It still fails if the selection is put back to asking about the socket alone. Two findings declined, both pre-existing and outside this change:
|
Review of #112: between isDisconnected() and lastLoginTime() it shifted the vtable slot of every virtual after it, which an exported class does not get to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…R-205 Review of #112: mailimap_noop can also fail to parse, and that branch reported success and left the connection LOGGEDIN with a parser out of step - ready, as far as the selection could tell. Reported and flagged like every other command wrapper in the file now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ 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 86c8f1d. Configure here.
Follow-up to #111, from a review of the merged change.
The inversion
The tie-break added in #111 asks
IMAPSession::isDisconnected(), which is onlymState == STATE_DISCONNECTED. Nothing but the constructor andunsetup()ever writes that state — a command that fails or is interrupted leaves the sessionLOGGEDIN/SELECTEDand raisesmShouldDisconnectinstead (61 sites), andconnectIfNeededthen pays disconnect + connect + login on the next command, strictly more than the connect + login a closed socket costs.So the pool ranked its most expensive connection as its cheapest, and at equal queue length handed exactly that one to the lease — every time, where before the tie-break it was a coin flip on pool order.
The state is a normal one on this branch:
interruptCurrentCommand()raises the flag deliberately and leaves the connection pooled to heal on its next command (5f27dfd, COR-201), and any stream or parse error does the same.The change
IMAPSession::needsReconnect()—mState == STATE_DISCONNECTED || mShouldDisconnect— is what the selection asks now.isDisconnected()keeps its meaning on purpose:IMAPAsyncConnection::tryAutomaticDisconnect()asks it whether the socket is already gone, and a flagged connection still holds one; teaching that predicate about the flag would leave those sockets open until the next command.mShouldDisconnectisstd::atomic<bool>for the same reasonmStatebecame atomic in #111: the selection reads it from the thread that starts an operation.Tests
testAcquirePrefersTheLiveConnectionOverAnInterruptedOne— a connection whose NOOP was interrupted stays connected and owing a reconnect; the lease must go to the other one. It fails if the predicate is put back to the socket state alone. Lease suite 24/24; the only failure in the repository is the pre-existingtestSummary(date locale).🤖 Generated with Claude Code
Note
Medium Risk
Changes IMAP connection-pool selection and cross-thread visibility of teardown state; wrong ranking would add extra handshakes but not auth/data corruption.
Overview
Fixes inverted connection-pool tie-breaking from the prior lease work: equal queue depth no longer favors connections that still look “connected” but owe a tear-down + reconnect after a failed or interrupted command.
Adds
IMAPSession::needsReconnect()(disconnectedormShouldDisconnect) and renamesIMAPAsyncConnection::isDisconnected()toneedsReconnect()for pool selection insessionWithMinQueue.isDisconnected()stays socket-only so automatic idle disconnect logic is unchanged.Makes
mShouldDisconnectstd::atomic<bool>for cross-thread reads during selection, and sets the flag on login LIST and NOOP stream/parse failures so interrupted streams are ranked correctly.Adds
testAcquirePrefersTheLiveConnectionOverAnInterruptedOneto assertacquireConnectionpicks the healthy connection when the other owes a reconnect.Reviewed by Cursor Bugbot for commit 86c8f1d. Bugbot is set up for automated code reviews on this repo. Configure here.