feat(gateway): per-server requestTimeoutMs for stdio live calls - #854
feat(gateway): per-server requestTimeoutMs for stdio live calls#854justinkeltner wants to merge 1 commit into
Conversation
| if let Some(ms) = server.request_timeout_ms() { | ||
| ds.set_call_timeout(Duration::from_millis(ms.max(1))); |
There was a problem hiding this comment.
🔴 Startup catalog reads inherit live-call timeout
When set_call_timeout runs before initial catalog loading, an unresponsive advertised catalog inherits the configured live-call timeout. Large values can stall startup for each catalog request.
Prompt for agents
In src-tauri/src/bin/toolport-gateway.rs, connect_one applies ServerEntry::request_timeout_ms through DownstreamServer::set_call_timeout immediately after the stdio handshake, but the common success path then calls load_resources_prompts. Those initial resources/list, resources/templates/list, and prompts/list requests use the transport's current timeout, so a large live-call setting also enlarges startup catalog waits. Defer applying the stdio live-call timeout until after load_resources_prompts completes, while preserving the configured timeout for the returned server and every reconnect. Add a transport-level or gateway test with an advertised but unresponsive catalog to verify startup retains its bounded/default budget and subsequent live calls use the configured value.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe gateway reads each server’s ChangesStdio call timeout
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The change enables longer-running stdio tools while preserving existing connection and discovery limits. A very large configured timeout could delay failure detection for a hung server, so the PR is mergeable with explicit owner awareness or follow-up to define an upper bound. Sequence Diagram(s)sequenceDiagram
participant ServerEntry
participant ToolportGateway
participant DownstreamServer
participant Transport
ServerEntry->>ToolportGateway: read requestTimeoutMs
ToolportGateway->>DownstreamServer: connect
ToolportGateway->>DownstreamServer: set_call_timeout
DownstreamServer->>Transport: set_read_timeout
DownstreamServer->>Transport: restore timeout after catalog refresh
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The implementation addresses issue Full details: Out of Scope Changes checkExplanation The changes remain within scope. They modify stdio timeout configuration, registry compatibility handling, reconnect behavior, and catalog-refresh restoration. No unrelated code changes are identified. Full details: Docstring CoverageExplanation Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 2 files. (1 skipped: 1 too large.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Warning |
|
Thanks for this, the shape is right. Plan on my side: #852 merges first, then this needs a rebase. Two things once it's rebased:
One decision to make in the PR: #852 strips |
|
Thanks for the review.. all three points make sense. Plan sounds good. On the typed field: Agreed, once #852 lands I'll rebase and read server.request_timeout_ms directly, dropping the unknown_fields getter. Cleaner that way. On the Devin finding: You're right, that's a real sequencing bug. I'll go with option A — apply the timeout after load_resources_prompts. Rationale: the initial catalog load is a connect-time handshake-adjacent operation, and it should stay bounded by STDIO_READ_TIMEOUT (30s) so a hung server still fails fast. The widened deadline is only for live tool calls after the connection is healthy. The reconnect path (re-spawned adapters via connect_one) will inherit the same behavior automatically since it goes through the same flow. On the export/import seam: Since this PR makes stdio honor the field, the two seams should keep requestTimeoutMs for stdio entries — stripping it would silently drop a live setting on team export/shared import, which feels like a footgun. I'll update the doc comment on the field to say it applies to both transports (remote HTTP/SSE and stdio), and adjust the export/import logic + tests to preserve it for stdio. One suggestion for a follow-up (not this PR): Now that requestTimeoutMs is user-facing for both transports, it'd be worth surfacing it in the Server dialog UI under a timeout/deadline section. Right now users have to hand-edit registry.json to discover or change it, which is opaque for a setting that can silently break things if too low (or hold connections open if too high). Happy to open a separate issue/PR for that if you're interested. |
Remote HTTP/SSE servers can carry a per-server requestTimeoutMs (upstream PR btsouth#852), but stdio live calls stay pinned to the 30-second STDIO_READ_TIMEOUT constant, so a local server with legitimately long synchronous tools (checkpoint + SSH execution, approval polling) is killed at 30s - and each such call counts as a health failure, tripping the circuit breaker after three occurrences. Honor the same requestTimeoutMs field for stdio: ServerEntry reads it out of the existing unknown_fields flatten map (no struct-literal ripple; older binaries already preserve the key on re-save), and DownstreamServer gains a call_timeout that widens only the post-handshake read deadline through set_call_timeout. Connect, handshake, and probe budgets are deliberately unchanged so a hung server still fails fast. Measured on 1.17.0: a 45s probe call is answered by the stock gateway at 30.007s with 'timed out waiting for tools/call response' and at 45.009s with requestTimeoutMs: 90000.
02ab0f0 to
0d29cd9
Compare
|
Rebased on #852. All three items addressed: typed field is read directly, set_call_timeout moved after load_resources_prompts, and the export/import seams now preserve requestTimeoutMs for stdio. Happy to add the Server dialog UI for this in a follow-up PR if you'd like, or feel free to wire it up yourself — the field is already in the TypeScript types from #852. |
Closes #853. Companion to #852 (remote HTTP/SSE) — same field name, so one registry concept covers both transports.
What and why
Remote HTTP/SSE servers can carry a per-server
requestTimeoutMs(#852), but stdio live calls stay pinned to the 30-secondSTDIO_READ_TIMEOUTconstant, so a local server with legitimately long synchronous tools is killed at 30 s — and each such call counts as a health failure, tripping the circuit breaker after three occurrences (BREAKER_FAILURE_THRESHOLD).This honors the same
requestTimeoutMsfield for stdio servers:ServerEntry::request_timeout_ms()reads the field out of the existingunknown_fieldsflatten map — noServerEntryshape change, so the 55+ struct-literal sites are untouched and older binaries already preserve the key on re-save (the documented mixed-version contract). When a typed field lands, this getter is the only site to update.DownstreamServergainscall_timeout(defaults toSTDIO_READ_TIMEOUT) with aset_call_timeoutsetter; the three catalog-refresh restore sites use it instead of the const.connect_oneapplies the configured value right afterconnect, clamped to >= 1 ms so a zero entry degrades to fast visible failure, not an inverted deadline.connect_oneis also the reconnect factory, so re-spawned adapters re-inherit the value.What deliberately does not change
Connect handshake (10 s), launcher cold-start budget (120 s), the
server/discoverprobe (750 ms), and every batch health-probe path keep their current bounds — a hung server still fails fast and the grid still can't stall on one slow probe. Only the post-handshake read deadline widens, per server.Testing
cargo test --no-default-features --lib— 1283 passed / 0 failed(includes new
server_entry_request_timeout_ms_round_trips_and_survives_resave)requestTimeoutMs: 90000answered at 45.009 s; the same call without thefield is killed at 30.007 s on v1.17.0
the field set on a 38-tool stdio server
npm run test(frontend untouched by this change)Notes
unknown_fieldsread is deliberate for review-ability of the stdio/HTTPsymmetry; if you'd rather have a typed
request_timeout_msfield onServerEntry(touching ~55 construction sites), say the word and I'll rework.config error — easy to align either way.
Note
Apply per-server
requestTimeoutMsto stdio live calls ingateway.connect_oneAfter the initial catalog load completes, the connection handler sets the downstream server's request timeout to the configured per-server
requestTimeoutMsvalue. The value is clamped to a minimum of 1ms. Connection, handshake, probe, and initial catalog-load operations continue to use the default bounded read timeout.requestTimeoutMsthat is too low could cause live calls to time out prematurely; reviewers should check the clamping logic in toolport-gateway.rs.Macroscope summarized 0d29cd9.