Repository navigation
[Server] Stop overwriting client responses while polling for them - #545
Closed
chr-hertel wants to merge 1 commit into
Closed
chr-hertel wants to merge 1 commit into
chr-hertel wants to merge 1 commit into
Conversation
chr-hertel
requested review from
CodeWithKyrian,
Nyholm and
soyuka
as code owners
October 7, 2026 20:09
chr-hertel
commented
Oct 7, 2026
chr-hertel
force-pushed
the
claude/session-lost-update
branch
from
October 7, 2026 20:11
f43de77 to
c25ce09
Compare
While a request to the client is pending over HTTP, the process holding the SSE stream polls the session every 100ms, and every poll emptied the outgoing queue and saved the whole session, even with nothing queued. The client's answer arrives as a separate POST, usually in another process, which stores it in the same session. A poll that read the session just before that store and saved just after wrote the old copy back, the answer was lost, and the request timed out. Leave the session untouched when there is nothing to dequeue, so the waiting loop only reads until the answer is there. This showed up as intermittent timeouts of the multi-worker php -S tests in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RjzbQfz2DVm9xHmGsqM7d
chr-hertel
force-pushed
the
claude/session-lost-update
branch
from
October 7, 2026 20:28
c25ce09 to
21429a5
Compare
Member
Author
|
Side effect of introducing client tests - will be closed in favor of #535 |
guillaume-sainthillier
added a commit
to guillaume-sainthillier/php-sdk
that referenced
this pull request
Oct 8, 2026
Ported from modelcontextprotocol#545: one worker polls the session for a client's answer while another stores it. Saving the session on every poll used to overwrite that answer. The store fixture moves to Session/Fixture and gains a hook after the next read. Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A request from the server to the client (sampling, elicitation, roots) intermittently timed out over Streamable HTTP when the server runs in more than one process. In CI this showed up as rare 30–90s hangs in the multi-worker
php -Stests, so far inDualEraEndpointTest::testAsksForInput(#540) and inHttpClientCommunicationTeston #541.Cause
While such a request is pending, the process holding the SSE stream polls the session every 100 ms. Each poll calls
Protocol::consumeOutgoingMessages(), which:The client's answer arrives as a separate POST, usually handled by another process, which stores it in the same session. If that store lands between steps 1 and 3, step 3 writes the old copy back, and the answer is lost. The waiting request never sees it and times out.
FileSessionStorewrites atomically, but nothing protects a read-modify-write, so the session's last writer wins.Fix
consumeOutgoingMessages()leaves the session untouched when there is nothing to dequeue. While it waits, the polling loop then only reads, until the answer is there.Test
ProtocolSessionRaceTestinterleaves the two processes deterministically. A store decorator runs the answering worker'sprocessInput()right after the waiting worker's read.main, the answer is gone (checkResponse()returnsnull).Existing unit tests, including the ones counting
save()calls with messages queued, are unchanged and green.Not covered
Writes that do have something to persist are still unprotected read-modify-writes of the whole session. Two processes changing a session at the same moment can still lose one change. Closing that fully needs locking or a compare-and-swap in
SessionStoreInterface, which is a larger change. This PR removes the write that happened on every poll, which is the one that made this race likely.🤖 Generated with Claude Code
https://claude.ai/code/session_014RjzbQfz2DVm9xHmGsqM7d
Generated by Claude Code