| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Independent confirmation on v0.6.0 (Apache + mod_php), driving a real MCP server rather than a unit test: 20 concurrent tools/list + resources/list pairs, each on a freshly initialized session, both requests released from a thread barrier.
This PR removes the cross-delivery completely — 0 in 140 pairs after it, against 11 in 40 before. Both shapes described in #467 are gone: the JSON array carrying another request's response, and the 202 the other request got instead of its own. Two notes from the same runs:
Real-client impact, for the record: Claude Code issues tools/list and resources/list concurrently on every connect, so a plain connection hits this. On a hit it cancels the orphaned request after 30 s with notifications/cancelled … "Request timed out" and silently ends up with no resources. The patch was applied by hand onto v0.6.0 (the files have moved since), so line numbers differ but the logic is unchanged. |
Sorry, something went wrong.
|
Correction to my numbers above, and a data point on v0.8.1. The residual-loss figure in my comment — "one lost response survives both (1 in 60 pairs)" — came from too small a sample. Larger runs put it at 6 in 180 pairs on v0.6.0 with both PRs applied, so roughly 1 in 30. That residual is still the lost update of #275 and still outside this PR's scope, so nothing here changes; I am correcting it because "1 in 60" understates how often a real client ends up waiting on a response that was silently dropped. The cross-delivery result did not move, and it is the one I would stand behind: 0 in 180 pairs. v0.8.1 still has the bug. Same harness against stock v0.8.1: 65 % of pairs correct, versus 67 % on stock v0.6.0. So the v0.8.0 refactor did not incidentally fix this and the PR is still needed against current releases — worth saying explicitly, because the files have moved enough that four hunks had to be finished by hand when porting it there.
One caveat on that 17, since it would be easy to misread as "v0.8.1 loses more than v0.6.0": those runs shared a busy machine and a single run contributed 5 of them. I would not claim a difference between the two bases without re-measuring on an idle box. The zero cross-delivery is the robust part of both patched rows. Same setup as before — Apache + mod_php, 20 concurrent tools/list + resources/list pairs per run, each pair on a freshly initialized session, both requests released from a thread barrier. |
Sorry, something went wrong.
|
Thanks for the input @michalcharvat - going with concurrency tests here was a good idea 👍 |
Sorry, something went wrong.
There was a problem hiding this comment.
Hi @vbcherepanov, thanks for working on this, also started reviewing on detail level but got confused about the json_encode/decode handling we need to do here and i wonder if this is solving the issue on the right layer.
and ended up similar to the discussion in #275 - let's sort it there at first.
Sorry, something went wrong.
| * | ||
| * @return array{list<int|string|null>, bool} | ||
| */ | ||
| private static function expectedResponses(string $body): array |
There was a problem hiding this comment.
| private static function expectedResponses(string $body): array | |
| private function expectedResponses(string $body): array |
Sorry, something went wrong.
| * are passed, only the responses to those requests are returned. | ||
| * | ||
| * @param callable(Uuid $sessionId): array<int, array{message: string, context: array<string, mixed>}> $provider | ||
| * @param callable(Uuid $sessionId, list<int|string|null>|null $responseIds=): array<int, array{message: string, context: array<string, mixed>}> $provider |
There was a problem hiding this comment.
| * @param callable(Uuid $sessionId, list<int|string|null>|null $responseIds=): array<int, array{message: string, context: array<string, mixed>}> $provider | |
| * @param callable(Uuid $sessionId, list<int|string|null>|null $responseIds): array<int, array{message: string, context: array<string, mixed>}> $provider |
Sorry, something went wrong.
| * @param array{message: string, context: array<string, mixed>} $message | ||
| * @param list<int|string|null> $responseIds | ||
| */ | ||
| private static function isResponseTo(array $message, array $responseIds): bool |
There was a problem hiding this comment.
| private static function isResponseTo(array $message, array $responseIds): bool | |
| private function isResponseTo(array $message, array $responseIds): bool |
Sorry, something went wrong.
| foreach ($queue as $message) { | ||
| if (self::isResponseTo($message, $responseIds)) { | ||
| $consumed[] = $message; | ||
| } else { | ||
| $remaining[] = $message; | ||
| } | ||
| } | ||
|
|
||
| if ([] !== $consumed) { | ||
| $session->set(self::SESSION_OUTGOING_QUEUE, $remaining); | ||
| $session->save(); | ||
| } |
There was a problem hiding this comment.
this make sense as solution but introduces an issue of potentially piling up $remaining responses in the memory, right?
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Fixes #467.
With PHP-FPM, requests of the same session run in parallel and put their responses into the same session queue. createJsonResponse() took the whole queue, so one request got all responses as a JSON array and the others got 202 with nothing.
Now the transport collects the ids from its own POST body and takes only those responses. The rest stays in the queue. Batches are still answered with an array, single messages with one object, and an invalid message without a usable id still gets its error.
Protocol::consumeOutgoingMessages() gets the ids as an optional second argument. Without it nothing changes, so the SSE path and StdioTransport work as before.
The reproducer is testConcurrentPostsSharingASessionEachReceiveTheirOwnResponse: the second POST is handled completely between the first one queueing its response and reading the queue. It fails on main.
Not part of this PR:
AI disclosure: I used Claude Code to investigate the issue and help write the reproducer test. The fix itself is my own.