SMOODEV-2498: add bounded connect timeout to smooai-fetch - #88
Conversation
api-prime's ~16s API stalls (SMOODEV-2481) were fresh SYNs black-holing to dead pod IPs still lingering in a ClusterIP's iptables (endpoint-removal race). smooai-fetch builds a fresh reqwest::Client per request/retry but set no connect timeout, so reqwest waited the full whole-request timeout (15s) before the retry could land on a live pod. Add FetchBuilder::with_connect_timeout(ms) (and FetchOptions.connect_timeout_ms, default None so existing consumers are unchanged). When set, do_single_request builds the client via reqwest::Client::builder().connect_timeout(...); when unset the path is behaviorally identical (reqwest::Client::new()). A bounded connect timeout fails a black-holed connect in ~the configured window while slow-but-alive handlers (whole-request timeout) are untouched. Covered by tests/connect_timeout_tests.rs: a connect to a non-routable IP returns a request error in ~the connect window (~0.5s) rather than hanging to the whole-request timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Consolidates PRs #88 (Rust), #89 (Go), #90 (Python), #91 (.NET) and #92 (TypeScript), open since 2026-07-10, onto current main. The work was sound; it had gone stale against traceparent injection and the redaction fix, both of which touch the same single-request site. The gap is real: without a connect timeout, a SYN to a dead pod IP still lingering in a ClusterIP's iptables burns the WHOLE-request budget before retry can try a live endpoint. Measured locally, an unbounded connect to a black hole fails after 10.5s (undici's own default); bounded at 500ms it fails in ~1s. Two changes from the original PRs: - undici is an OPTIONAL PEER dependency, not a runtime dependency. #92 added it to `dependencies`, which would have put a full HTTP stack in the tree of every consumer -- including browser bundles -- for an opt-in Node-only feature. It follows the @opentelemetry/api pattern this package already uses: devDependency for our own tests, optional peer for consumers, lazy import at the call site. - Requesting a connect timeout without undici installed now THROWS with an actionable message rather than silently continuing unbounded. A timeout that quietly isn't applied is the exact failure this feature exists to prevent. Per Spec C, the five regression tests read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from the committed spec/connect-timeout-corpus.json instead of each hard-coding them, so the thresholds cannot drift apart per language. The TS suite carries a positive control so a corpus that failed to load reads red rather than green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
…103) Consolidates PRs #88 (Rust), #89 (Go), #90 (Python), #91 (.NET) and #92 (TypeScript), open since 2026-07-10, onto current main. The work was sound; it had gone stale against traceparent injection and the redaction fix, both of which touch the same single-request site. The gap is real: without a connect timeout, a SYN to a dead pod IP still lingering in a ClusterIP's iptables burns the WHOLE-request budget before retry can try a live endpoint. Measured locally, an unbounded connect to a black hole fails after 10.5s (undici's own default); bounded at 500ms it fails in ~1s. Two changes from the original PRs: - undici is an OPTIONAL PEER dependency, not a runtime dependency. #92 added it to `dependencies`, which would have put a full HTTP stack in the tree of every consumer -- including browser bundles -- for an opt-in Node-only feature. It follows the @opentelemetry/api pattern this package already uses: devDependency for our own tests, optional peer for consumers, lazy import at the call site. - Requesting a connect timeout without undici installed now THROWS with an actionable message rather than silently continuing unbounded. A timeout that quietly isn't applied is the exact failure this feature exists to prevent. Per Spec C, the five regression tests read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from the committed spec/connect-timeout-corpus.json instead of each hard-coding them, so the thresholds cannot drift apart per language. The TS suite carries a positive control so a corpus that failed to load reads red rather than green. Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Landed via #103, which consolidates all five ports onto current main. The work here was sound — it had gone stale against traceparent injection (#95) and the credential-redaction fix (#98), both of which touch the same single-request site. Two changes from this PR in the consolidated version:
Per Spec C the five regression tests now read their knobs (black-hole URL, connect timeout, whole timeout, elapsed ceiling) from Closing in favour of #103. |
Problem
api-prime's ~16s API stalls (SMOODEV-2481) traced to fresh SYNs black-holing to dead pod IPs still lingering in a ClusterIP's iptables during an endpoint-removal race.
smooai-fetchbuilds a freshreqwest::Clientper request/retry (so there is no stale keepalive to fix) but set no connect timeout — so reqwest waited the full whole-request timeout (15s in api-prime) before the existing retry could land on a live pod.Fix
Add a bounded connect timeout so a black-holed connect fails in ~the configured window and the existing retry lands on a live endpoint, while slow-but-alive handlers (governed by the whole-request timeout) are untouched.
FetchBuilder::with_connect_timeout(ms)— mirrorswith_timeout.FetchOptions.connect_timeout_ms: Option<u64>— defaultNone, so existing consumers are behaviorally unchanged.do_single_requestbuilds viareqwest::Client::builder().connect_timeout(Duration::from_millis(ms)).build()?when set; falls back to the identicalreqwest::Client::new()path when unset.Test
tests/connect_timeout_tests.rs: a connect to a non-routable/black-hole IP (10.255.255.1:80) with a 500ms connect timeout and a 5s whole-request timeout returns aFetchError::Requestin ~0.5s (asserted< 3s) instead of hanging to the whole timeout. Full suite green (cargo test -p smooai-fetch --locked: 111 passed), fmt + clippy clean.🤖 Generated with Claude Code