Fix provider timeout failover - #536
Conversation
|
| Filename | Overview |
|---|---|
| packages/backend/src/utils/timeout.ts | Significantly simplified: drops the global route-level timeout signal and AbortController wiring; now just exposes a resolveTimeoutMs closure that the dispatcher uses per attempt |
| packages/backend/src/services/dispatcher.ts | Core of the fix: adds createAttemptTimeout and buildTimeoutError, wires per-attempt AbortSignal and cleanup; a missing cleanup before enforceContextLimit throw leaves a benign timer leak |
| packages/backend/src/routes/inference/chat.ts | Timeout status is now recorded in the error path (504 branch) rather than the early-return path; logger.info ternary inside the client_disconnected branch is now dead code (always 'cancelled') |
| packages/backend/src/routes/inference/gemini.ts | Same pattern as chat.ts — stale upstream_timeout ternary in the logger inside the client_disconnected-only branch |
| packages/backend/src/routes/inference/messages.ts | Same pattern as chat.ts — stale upstream_timeout ternary in the logger inside the client_disconnected-only branch |
| packages/backend/src/routes/inference/responses.ts | Same pattern as chat.ts — stale upstream_timeout ternary in the logger inside the client_disconnected-only branch |
| packages/backend/src/services/response-handler.ts | Minor comment update only; the abort-listener comment at line 359 still references old wireUpstreamTimeout() timeout wiring that no longer exists |
| packages/backend/src/services/tests/dispatcher-abort.test.ts | Adds per-attempt timeout tests: verifies route signal isolation, buildTimeoutError shape, and fixes missing afterEach timer reset |
| packages/backend/src/utils/tests/timeout.test.ts | New test file covering resolveTimeoutMs with per-provider and null (global fallback) timeout values |
Comments Outside Diff (3)
-
packages/backend/src/routes/inference/chat.ts, line 148-150 (link)The ternary inside this
client_disconnected-only branch can never evaluate to'timeout'—e?.routingContext?.codeis always'client_disconnected'here. The same pattern exists ingemini.ts,messages.ts, andresponses.ts. -
packages/backend/src/services/response-handler.ts, line 355-363 (link)This comment still describes the old behavior where
wireUpstreamTimeout()calledabortController.abort()when a timeout fired. With the new design, per-attempt timeouts fire an isolatedtimeoutControllerand are cleared before the streaming response is returned; this listener is now exclusively for client-disconnect detection.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
-
packages/backend/src/services/dispatcher.ts, line 378-380 (link)Timer leak when
enforceContextLimitthrowscreateAttemptTimeout(which starts the per-attemptsetTimeout) is called at line 297, before the maintry/catchblock at line 410. IfenforceContextLimitthrows aContextLengthExceededErrorhere, control escapes the loop iteration without hittingattemptTimeout.cleanup(), leaking the timer. The practical impact is low becausetimeoutId.unref?.()is called so the leaked timer won't block process shutdown, but wrapping this call in a small try/finally (or moving it inside the maintryblock) would eliminate the leak entirely.
Reviews (1): Last reviewed commit: "fix: handle provider timeouts per attemp..." | Re-trigger Greptile
Summary
Verification
Fixes #522
Fixes #531