Repository navigation
Conversation
|
Thank you so much, this solved my problem with randomly hanging tests where I run hundreds of tools quickly. |
The Client's message handling loop uses `repeat { ... } while true` around
the `for try await data in stream` loop. When the transport stream finishes
(subprocess death, pipe EOF), the for-await exits normally, but the repeat
loop immediately re-enters `connection.receive()`, gets an instantly-finishing
stream, and spins at 100% CPU indefinitely.
Server.swift does not have this bug — it uses a flat `do { for try await }`
without an outer repeat. Match that pattern in Client.
The `catch` for `isResourceTemporarilyUnavailable` (with `continue` back into
the repeat) was the original motivation for the outer loop, but that error
cannot occur on a finished stream — it only matters during active I/O.
Removing the retry-on-EAGAIN is safe because the stream itself handles
transient read errors internally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1ea8365 to
42c30a1
Compare
… entries 5 and 5a Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DMgYCJ9tYavK3NhBCDTsn
…odelcontextprotocol#275 commit 8e36cfa, robertoscipionecom) Manifest entry 10. Upstream commit 8e36cfa extracted from refs/pull/275/head, unmodified: same author, same patch-id (e588896c…), cherry-picked with -x so the commit records its own provenance. Client.send suspended on a checked continuation only a matching response could resume, and the request task was unstructured, so cancelling the caller never reached it: a caller that gave up stayed suspended until the server answered, which for a dead server is never. Three linked fixes — forward cancellation from RequestContext.value to the request task, call cancelRequest from the task cancellation handler, and remember ids cancelled before their continuation registered. Two of modelcontextprotocol#275's three commits are deliberately skipped, not adapted: 40c5951 (loop termination) and fded08a (its test) duplicate manifest entries 5 and 5a from modelcontextprotocol#221. Taking them would conflict in Client.swift and produce a duplicate testMessageLoopStopsWhenStreamFinishes declaration that does not compile. The full PR head stays fetchable as branch pr/275. Two defects in this commit are carried as accepted, not fixed; see FORK.md entry 10. A cancellation landing before the send completes is notified before the request is sent, so the peer can ignore an unknown-id cancellation and then execute the request. And cancelledBeforeRegistration cannot distinguish 'not registered yet' from 'already completed', so cancelling a finished request leaves an entry until disconnect. Both need the request-state redesign that belongs upstream, not a fork patch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eb9yGSXH1TVkg5Afsu9phk
…ests without a reply (upstream modelcontextprotocol#298, EhsanAzish80) Supersedes the contributions of modelcontextprotocol#264, modelcontextprotocol#260 and modelcontextprotocol#270 (manifest entries 1-3), which modelcontextprotocol#298 combines. Conflicts resolved in favour of modelcontextprotocol#298 in HTTPServerTypes.swift, StatelessHTTPServerTransport.swift, Server.swift, CancellationTests.swift and HTTPServerTransportTests.swift. Fork changes re-applied on top: - modelcontextprotocol#258's Server.init description/websiteUrl/icons parameters - modelcontextprotocol#221's MockTransport receiveCallCount / finishStream - entry 1a's collision regression tests and their makeStatelessPOSTRequest authorization overload Behavior: a cancelled original POST now ends as HTTP 200 with an empty text/event-stream and no JSON-RPC body (previously -32002 from modelcontextprotocol#260). Follow-up on when to use -32800: fork issue #41. Full suite: 589 tests in 43 suites passed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Hi @piersdd, thanks for this PR! I've reviewed it and it looks good to merge. Could you please rebase it onto the latest This already has an approval, so once it's rebased and green I'll merge it. (Optional: a small regression test, like the one in #275, would be welcome but isn't required.) If you're no longer able to work on this, no problem: just let us know. If we don't hear back in about two weeks, we may carry the change forward in a new PR, with credit to you. |
Summary
When a transport stream finishes (e.g. subprocess death closes the pipe), the Client's message handling loop spins at 100% CPU indefinitely.
The root cause is the
repeat { ... } while trueouter loop inClient.connect(transport:)(line 217). When thefor try await data in streamloop exits normally on stream EOF, therepeatre-entersconnection.receive(), gets an instantly-finishing stream, and loops again — consuming a full CPU core.The
Task.isCancelledcheck at the top of therepeatbody only helps ifdisconnect()was called externally. On a normal subprocess/transport death, no cancellation is signalled — the stream simply finishes.Server.swiftin this same package does not have this bug — it uses a flatdo { for try await ... }without an outerrepeatloop.Fix
Remove the
repeat { ... } while truewrapper, matching theServer.swiftpattern. The message handling task now:connection.receive()oncefor try awaitThe
catchforisResourceTemporarilyUnavailable(which usedcontinueto re-enter the repeat) is also removed. That EAGAIN-style error is a transient I/O condition handled internally by the transport's read loop — it does not surface through theAsyncThrowingStreamthatreceive()returns.Reproduction
StdioTransportto a subprocesssampleor Activity MonitorThe tight loop shows up as:
Test plan
disconnect()still terminates the loop correctly (cancels the task)