COR-223, COR-224 - Report a cut that did not happen, and name the lease queue - #117
Conversation
The header and the Swift doc told callers to serialize acquireConnection and releaseConnection "on a queue of your choosing". They cannot: the pool's connection list is appended to by acquireConnection on the caller's thread and walked by operationRunningStateChanged() on the session's dispatch queue, with no lock between them. Any other queue serializes the callers against each other and against nothing else. MCOIMAPAsyncConnection.deinit already documented the strict rule, hops to that queue, and cross-referenced the text that contradicted it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
interruptCurrentCommand() cancels the session's stream only once libetpan has one. Before that - DNS, the TCP connect, an implicit-TLS handshake - there is nothing to cancel, yet interruptRunningOperation() answered true regardless, because it raised its flag on identity alone. Measured against a black-holed address: the operation stayed blocked and the caller was told its command had been broken. interrupt() now reports whether it broke anything and the queue passes that up. The blocked connect itself is still not interruptible - that needs a change in libetpan - so this makes the report honest and does not shorten the hang. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Resolve the public virtual API compatibility breaks, serial-queue requirement, and Windows archive update.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR improves IMAP interruption reporting and clarifies lease-operation threading requirements.
Changes:
- Propagates whether interruption actually cancelled a stream.
- Updates Swift and C++ interruption documentation.
- Clarifies lease serialization on the session queue.
- Regenerates mirrored headers.
File summaries
| File | Reviewed changes |
|---|---|
src/swift/imap/IMAPSession.swift |
Clarifies lease queue requirements. |
src/swift/imap/IMAPBaseOperation.swift |
Documents boolean interruption results. |
src/include/MailCore/MCOperationQueue.h |
Updates the interruption contract. |
src/include/MailCore/MCOperation.h |
Changes the interrupt return type. |
src/include/MailCore/MCIMAPSession.h |
Documents stream cancellation results. |
src/include/MailCore/MCIMAPOperation.h |
Updates the IMAP interruption API. |
src/include/MailCore/MCIMAPAsyncSession.h |
Clarifies lease threading. |
src/core/imap/MCIMAPSession.h |
Mirrors session API changes. |
src/core/imap/MCIMAPSession.cpp |
Reports whether a stream was cancelled. |
src/core/basetypes/MCOperationQueue.h |
Mirrors the queue contract. |
src/core/basetypes/MCOperationQueue.cpp |
Forwards interruption results. |
src/core/basetypes/MCOperation.h |
Mirrors the interrupt return type. |
src/core/basetypes/MCOperation.cpp |
Returns false by default. |
src/async/imap/MCIMAPOperation.h |
Updates the override declaration. |
src/async/imap/MCIMAPOperation.cpp |
Returns the session cancellation result. |
src/async/imap/MCIMAPAsyncSession.h |
Mirrors lease-threading documentation. |
Review details
Suppressed comments (3)
src/async/imap/MCIMAPAsyncSession.h:128
- This contract also requires a serial queue.
setDispatchQueue()accepts any dispatch queue, including concurrent/global queues; placing acquire/release/start on one does not serialize them, so themSessionsappend/walk race can remain. Please state or enforce the serial-queue requirement here and regenerate the public mirror.
start() on this session, and do it on the session's own dispatch queue: the pool's
bookkeeping is touched from that queue as well - operationRunningStateChanged() walks
the connection list that a new connection is appended to - so any other queue
serializes the callers against each other and against nothing else. */
src/core/imap/MCIMAPSession.cpp:3783
- These C++ changes are under
src, which is part of the content hashed for the Windows prebuilt archive. The new digest-namedmailcore2-windows-<digest>.zipmust be built and published for this revision; otherwise the Windows prebuilt check/consumers will fail even though the macOS verification passes.
bool IMAPSession::interruptCurrentCommand()
src/swift/imap/IMAPSession.swift:177
- This contract also requires a serial queue.
dispatchQueueis caller-configurable, and a concurrent/global queue does not serialize acquire/release/start, so the append/walk race COR-224 is still possible. Please state or enforce that the session queue must be serial in this API and the mirrored C++ documentation.
acquireConnection and releaseConnection with every start() on this session, and do it on the
session's own dispatch queue: the pool's bookkeeping is touched from that queue as well, so
any other queue serializes the callers against each other and against nothing else.
- Files reviewed: 16/16 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
bugbot run |
There was a problem hiding this comment.
🟡 Changes recommended
Publish the Windows prebuilt artifact for this revision so the Windows check can pass.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 1
- Review effort level: Lite
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 d8b121c. Configure here.
Two independent small fixes in the same layer, one commit each.
COR-223 — the cancel fallback lies when it cuts nothing
IMAPSession::interruptCurrentCommand()cancels the stream only whenmImap != NULL && mImap->imap_stream != NULL. libetpan assignsimap_streamat the top ofmailimap_connect, so everything before that — DNS,mail_tcp_connect_timeout, and on an implicit-TLS session the handshake — runs with no stream this call can reach.OperationQueue::interruptRunningOperation()nevertheless returnedtrue, because it raised its flag purely onmRunningOperation == op.Measured on a throwaway probe against
192.0.2.1(TEST-NET-1, SYNs dropped), before the change:After it, the same probe reports
falseand the operation is — unchanged — still blocked.interrupt()now returns whether it broke anything and the queue passes that up.Operation::interrupt(),IMAPOperation::interrupt()andIMAPSession::interruptCurrentCommand()change fromvoidtobool; no virtual is added, removed or reordered, so no vtable slot moves. The whole interrupt chain is a fork addition —upstream/masterhas none of these three methods — so this costs nothing at the pending upstream merge.IMAPOperationis the only override ofinterrupt()in the repository, andIMAPAsyncConnection::interruptCurrentCommand()is the only caller ofinterruptRunningOperation().This does not shorten the hang. A connect blocked in
connect(2)is still not interruptible; making it so means changingmail_tcp_connect_timeoutin the libetpan fork, which is out of scope here. The operation still runs to the session timeout — the caller is simply no longer told otherwise. Downstream, Spark reads the result only to decide whether to write a trace line, so behaviour there is unchanged and the log stops lying.No test is added. Every existing assertion passes against the old code too:
LeaseTestTCPEndpointandSilentTCPEndpointboth accept the TCP connection, soimap_streamis always assigned before the client blocks. Covering the newfalseneeds a black-holed destination, which is environment-dependent and slow; the probe above was thrown away rather than committed. Declining that deliberately rather than silently.One thing I did not verify: once the stream exists, whether libetpan's TLS handshake inside
mailimap_socket_starttls()actually observesmailstream_cancel(). If it does not, atruethere is the same shape of dishonesty one phase later — same libetpan work to fix, not addressed here.COR-224 — documentation only
The header and
IMAPSession.swifttold callers to serializeacquireConnection/releaseConnectionwith everystart()"on a queue of your choosing". That is wrong:acquireConnectionappends tomSessionson the caller's thread whileoperationRunningStateChanged()walks it on the session's dispatch queue, with no lock. Confirmed by printingpthread_self()at both sites — two distinct threads, interleaved, with an append landing between two walks.MCOIMAPAsyncConnection.deinitalready documented the strict rule, hops to that queue, and cross-referenced the text that contradicted it. No executable change.Verification
testRepeatedCancelDuringIdleDoesNotHangOrCrashfrom COR-221.unittest.testSummarylocale/date-format mismatches, which are green in CI.src/include/MailCore/regenerated withconfigure-headers.shand checked byte-identical to their sources.🤖 Generated with Claude Code
Note
Medium Risk
Changing virtual
interrupt()/interruptCurrentCommand()return types is an ABI/API break for any external subclass, though the tree only overridesIMAPOperation::interrupt(); interrupt logic touches connection teardown and queue locking.Overview
Interrupt reporting (COR-223): The operation interrupt path now returns
boolinstead ofvoid, so callers can tell when nothing was actually aborted.IMAPSession::interruptCurrentCommand()returnsfalsewhen there is no libetpan stream yet (connect blocked in DNS/TCP/TLS) or after teardown;OperationQueue::interruptRunningOperation()propagates that instead of reporting success whenever the target operation was merely the running one. Docs/Swift comments forinterruptCurrentCommand()are aligned with that semantics. This does not make pre-stream connects interruptible—only the reported outcome changes.Lease threading docs (COR-224):
acquireConnection/releaseConnectiondocumentation now requires serializing with everystart()on the session’s own dispatch queue, because pool bookkeeping (e.g.operationRunningStateChanged()) runs there—not on an arbitrary caller queue. Documentation-only; no runtime behavior change.Reviewed by Cursor Bugbot for commit d8b121c. Bugbot is set up for automated code reviews on this repo. Configure here.