fix(coderd/x/chatd/mcpclient): enforce MCP connect budget and unblock session cleanup - #28400
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f218858d35
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… session cleanup A black-holed MCP server (TCP accepted, no response, or SYNs silently dropped) stalled every chat generation step for about two minutes: go-sdk v1.7.0 detaches the context inside StreamableClientTransport.Connect, so the nominal 10s connect budget did not hold (observed 12s for a 2s deadline in tests, and ~125s kernel TCP timeouts in production), and step-end session cleanup blocked synchronously on the same detached requests. - Enforce the connect budget externally: run Connect+ListTools in a goroutine and select on the budget context. On timeout, abandon the goroutine and leave a reaper that drains its late result and closes any session that still materialized. - Stop using bare http.DefaultTransport: clone it and bound dials at 5s and response headers at 60s (toolCallTimeout, not connectTimeout, so slow JSON-response tools within the tool-call budget are not killed; SSE streams only need headers within the bound). http.Client.Timeout stays unset to keep SSE alive. - Close sessions in detached goroutines during cleanup so a server that wedges mid-turn cannot stall the generation loop at step boundaries.
f218858 to
7d60361
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Problem
During the Aug 19 dev.coder.com incident, chats stalled for up to ~15 minutes per turn. chatd reconnects to every configured MCP server on every generation step, and one configured server (
registry.coder.com/mcp) was black-holing requests from the deployment's egress IPs: TCP/requests were silently dropped, and each connect attempt hung until the kernel gave up (~125s), far past the nominal 10s connect budget.The budget does not hold because go-sdk v1.7.0 detaches the context inside
StreamableClientTransport.Connect, and its error paths block on HTTP work bound to that detached context. Reproduced in a test: a bareConnectwith a 2s deadline against a black-holed server returns after 12s. Step-end session cleanup (session.Close()) runs synchronously in the generation loop and blocks on the same detached requests, so a server that wedges mid-turn stalls every step boundary too.Changes
connectOne): runConnect+ListToolsin a goroutine and select on the budget context. On timeout, abandon the goroutine and leave a reaper that drains its late result and closes any session that still materialized, so nothing leaks and the caller returns within the budget.http.DefaultTransport: MCP traffic now rides a cloned transport with a 5s dial timeout (converts SYN black-holes into fast errors) and a 60sResponseHeaderTimeout.http.Client.Timeoutstays unset so long-lived SSE streams are unaffected.ConnectAll's cleanup func): the sessions are discarded regardless, and a wedgedClose(DELETE bound to the SDK's detached context) must not stall the generation loop at step boundaries.One deliberate deviation from the incident plan:
ResponseHeaderTimeoutis 60s (matchingtoolCallTimeout) instead of ~10s. The same HTTP client serves tool-call POSTs, and a JSON-response MCP server sends no headers until the tool finishes, so a 10s bound would falsely kill legitimate 10-60s tools that fit today's tool-call budget. The real 10s connect budget is enforced by the external select, not the transport.Tests
ConnectAllreturns within the budget and the healthy server's tools are present; closing the black-holed connections makes the reaper exit (red without the fix: 1s budget took 11s).ConnectAllreturns promptly, the late result is reaped.go test ./coderd/x/chatd/...passes (including goleak inchatd).Part of the MCP connect-stall incident follow-up; observability (connect durations in logs/debug runs) comes in a stacked follow-up PR.