|
1 | | -// Cloud: regression test for the CONFIRMED production "session brick" failure (observed |
2 | | -// 3/3 against executor.sh prod on 2026-07-04). The recipe: |
| 1 | +// Cloud: regression test for a session-DO startup race. |
3 | 2 | // |
4 | | -// 1. Open an MCP session (real SDK client init → mcp-session-id), make a |
5 | | -// call, close the client. |
6 | | -// 2. Idle PAST the session timeout so the DO's alarm runs disposeIdleRuntime |
| 3 | +// 1. Open an MCP session (real SDK client init, capture mcp-session-id), |
| 4 | +// make a call, close the client. |
| 5 | +// 2. Idle past the session timeout so the DO's alarm runs disposeIdleRuntime |
7 | 6 | // (runtime torn down, `initialized = false`, transport cleared). |
8 | | -// 3. Fire an SSE GET (listen stream) and a POST (tools/list) CONCURRENTLY on |
| 7 | +// 3. Fire an SSE GET (listen stream) and a POST (tools/list) concurrently on |
9 | 8 | // the same session id. |
10 | | -// 4. The session must survive: the concurrent pair and follow-up calls should |
11 | | -// succeed instead of leaving the DO permanently bricked. |
| 9 | +// 4. The session must survive: the concurrent pair and follow-up calls |
| 10 | +// should succeed. |
12 | 11 | // |
13 | | -// Mechanism: after |
14 | | -// idle-dispose, a request carrying a session id runs |
15 | | -// validateMcpSessionOwner → onStart → server.connect(_transport). The SDK's |
16 | | -// own serve() streaming handler then does its own agent.fetch(Upgrade:websocket), |
17 | | -// waking the DO, whose Agent base ALSO drives onStart → a SECOND |
18 | | -// server.connect on a server whose _transport is already set → |
19 | | -// "Already connected to a transport. Call close() before connecting to a new |
20 | | -// transport." Two concurrent GET+POST restores interleave and collide. |
21 | | -// |
22 | | -// Observed locally (workerd, MCP_SESSION_TIMEOUT_MS=3000): the concurrent pair |
23 | | -// itself usually answers 200/200 — one restore wins the race — but the race |
24 | | -// leaves the DO's McpServer half-connected, and EVERY subsequent request 500s |
25 | | -// with the "Already connected" error. Same permanent grave as prod; prod's |
26 | | -// slower timing surfaced the 500 on the pair itself. |
| 12 | +// Mechanism guarded against: a request carrying a session id runs |
| 13 | +// validateMcpSessionOwner, which restores via onStart when the runtime is |
| 14 | +// disposed. The SDK's serve() streaming handler separately wakes the DO |
| 15 | +// through agent.fetch, and the Agent base also drives onStart. Without |
| 16 | +// serialization across both entry paths, two onStart calls interleave and the |
| 17 | +// second server.connect throws "Already connected to a transport", after |
| 18 | +// which every request on the session fails the same way. The concurrent pair |
| 19 | +// itself can answer 200/200 (one restore wins the race), so the assertions |
| 20 | +// cover the follow-up calls, which is where the failure persists. |
27 | 21 | // |
28 | 22 | import { writeFileSync } from "node:fs"; |
29 | 23 | import { join } from "node:path"; |
|
0 commit comments