Repository navigation
[Server] Let an event loop drive the fibers request handlers run in - #616
Open
webagil-kevin wants to merge 1 commit into
Open
webagil-kevin wants to merge 1 commit into
webagil-kevin wants to merge 1 commit into
Conversation
webagil-kevin
requested review from
CodeWithKyrian,
Nyholm,
chr-hertel and
soyuka
as code owners
October 25, 2026 18:57
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical fiber lifecycle defects allow inline wake-ups to trigger FiberError and transport-resume failures to bypass request error handling, leaving requests without terminal JSON-RPC errors.
2 open issues
What changed in this PR
This PR addresses #615 by letting an event-loop scheduler drive request-handler fibers while they await I/O, instead of the server resuming them prematurely.
Changes:
- Adds a scheduler interface, settlement signal, and shared handler-fiber lifecycle management.
- Integrates scheduling into the builder, both protocol eras, client gateway, and transports.
- Adds regression tests and documents scheduler configuration with a Revolt example.
| File | Description |
|---|---|
tests/Unit/Server/Fiber/HandlerFibersTest.php |
Tests fiber ownership, suspension, and exception handling. |
tests/Unit/Server/Fiber/FakeEventLoop.php |
Provides a dependency-free scheduler for tests. |
tests/Unit/Server/Fiber/EventLoopToolCallTest.php |
Tests HTTP tool calls with and without scheduling in both eras. |
src/Server/Transport/StreamableHttpTransport.php |
Routes streamed handler resumes through shared fiber management. |
src/Server/Transport/StdioTransport.php |
Routes stdio handler resumes through shared fiber management. |
src/Server/Transport/BaseTransport.php |
Uses shared fiber management when resuming after yield failures. |
src/Server/Stateless/StatelessProtocol.php |
Adds scheduler-aware handler execution. |
src/Server/Protocol.php |
Adds scheduler-aware handshake-era handler execution. |
src/Server/Fiber/HandlerFiberState.php |
Tracks suspension ownership, exceptions, and runtime waits. |
src/Server/Fiber/HandlerFibers.php |
Centralizes handler-fiber lifecycle operations. |
src/Server/Fiber/FiberSettlement.php |
Adds a one-shot settlement signal. |
src/Server/Fiber/FiberSchedulerInterface.php |
Defines the runtime waiting contract. |
src/Server/ClientGateway.php |
Records gateway suspensions as server-owned. |
src/Server/Builder.php |
Exposes scheduler configuration for both protocols. |
docs/run/server-builder.md |
Documents scheduler setup with a Revolt example. |
CHANGELOG.md |
Records the scheduler API and event-loop handling changes. |
Review effort: Balanced
Comment on lines
+60
to
+61
| foreach ($callbacks as $callback) { | ||
| $callback(); |
Comment on lines
+90
to
+91
| if (null === $this->scheduler) { | ||
| throw new LogicException('A request handler suspended its fiber for something other than the server, an event loop awaiting I/O for instance: set the runtime\'s fiber scheduler with Builder::setFiberScheduler().'); |
This was referenced Nov 5, 2026
This branch has not been deployed
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.

Fixes #615
Reproduction: #615. Lines are on main at 57b45eb.
Why it fails
The server runs each request handler in a fiber it creates (
StatelessProtocol.php:675,Protocol.php:415) and resumes that fiber whenever it is suspended: withnullon any suspension that is not an elicitation (StatelessProtocol.php:707), and from the transports when no client request is pending (StreamableHttpTransport.php:316,StdioTransport.php:401). Under an event loop, a handler awaiting I/O suspends that same fiber for the loop, with nothing. The server resumes it withnull: the await returns early or throwsMust call resume() or throw() before calling suspend() again, and the loop later resumes a fiber that has moved on. Under Revolt, the event loop terminates.Change
Mcp\Server\Fiber\FiberSchedulerInterface::wait(FiberSettlement)andFiberSettlement: how the runtime lets itself run until a handler fiber is the server's again.HandlerFibers(@internal) creates, starts, resumes and suspends handler fibers.start()andresume()return once the fiber suspended for the server again, or returned; in between, the scheduler waits.ClientGatewaysuspends throughHandlerFibers::suspend(), so a suspension made after the loop resumed the fiber still reaches the server. ANotificationSuspensionorRequestSuspensionthat a handler passes to\Fiber::suspend()itself is read as the server's, as the existing tests do.Protocol,StatelessProtocol,BaseTransport,StreamableHttpTransportandStdioTransportgo throughHandlerFibers. A fiber it did not create is driven as before.Builder::setFiberScheduler().Protocol::__construct()andStatelessProtocol::__construct()take the scheduler as an optional last argument.LogicExceptionnamingBuilder::setFiberScheduler()(-32603), instead of being resumed withnull. The loop keeps running.CHANGELOG.md(0.10.0) and a "Fiber Scheduler" section indocs/run/server-builder.md, with a Revolt scheduler.Verification
Each job of
pipeline.yamlanddocs.yml, replayed in Docker on main (57b45eb) and with the change, with the same dependency resolution as CI:tests/Unit/Server/Fiber/:HandlerFibersTestruns a dependency-freeFakeEventLoop, which suspends the fiber with nothing and resumes it from its own turn, as Revolt does.EventLoopToolCallTestcalls a tool awaiting I/O over Streamable HTTP, in both eras, with and without a scheduler.composer validate --strict,php-cs-fixer fix --dry-run,phpstan analyseandmake docs-apiare clean, as on main.--prefer-lowest --prefer-stable, flex,SYMFONY_REQUIRE='>=6.4'): green on 8.1 to 8.5, with the deprecation count of main (2, 4, 4, 16, 17).http://localhost/) and the client suite, on2025-11-25and2026-07-28, pass their baselines, as on main.zensical build --strict): no issues, as on main.-32603and the event loop terminates;-32603with theLogicException's message, and the loop ends cleanly;slept 204 ms, then 100 ms.Not verified
react/async): its scheduler is the same few lines with aDeferredandawait(), not run.ServerandStreamableHttpTransportin-process on the fake loop, so no dev dependency is added. Happy to add an integration fixture onrevolt/event-loop, as a dev dependency, if you'd rather have one.