fix: bound remote fetch connection pool acquire wait by fetch timeout - #78
Open
cmyui wants to merge 3 commits into
Open
fix: bound remote fetch connection pool acquire wait by fetch timeout#78cmyui wants to merge 3 commits into
cmyui wants to merge 3 commits into
Conversation
The remote client acquired its pooled connection with no timeout, so when all pooled connections were busy, concurrent fetches blocked indefinitely in pool.acquire() -- a wait that fetch_timeout_millis never covered, since it only bounds the socket read after acquisition. Pass fetch_timeout_millis as the acquire timeout and map the pool's existing EmptyPoolError to FetchException(408). 408 lands in the non-retryable bucket of __should_retry_fetch by design: retrying a starved pool would re-enter the same queue and add pressure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.
Reviewed by Cursor Bugbot for commit ac735dc. Configure here.
Raising FetchException(408) put the pool-wait timeout in the non-retryable 4xx bucket, where __fetch_internal swallows the error and fetch_v2 returns None with no signal (flagged by Bugbot). Raise a builtin TimeoutError instead -- the same classification as the socket read timeout -- so both violations of fetch_timeout_millis behave identically: empty variants at fetch_retries=0, bounded retries then raise when retries are configured, and async callbacks receive the error. Adds an end-to-end parity test on fetch_v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmyui
commented
Jul 25, 2026
|
|
||
| conn = self._connection_pool.acquire() | ||
| try: | ||
| conn = self._connection_pool.acquire(timeout=self.config.fetch_timeout_millis / 1000) |
Author
There was a problem hiding this comment.
alternativly, add a new parameter (e.g. fetch_pool_acquire_timeout_millis) to avoid behavior changes to existing clients with their existing configuration
…millis Default None preserves today's unbounded acquire wait exactly; setting the new config bounds it, surfacing exhaustion as TimeoutError with the same retry classification as a socket read timeout. Reworked from reusing fetch_timeout_millis so the PR carries no default-behavior change; whether to bound by default is posed to maintainers in the PR description instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Sibling to #77, independently mergeable. #77 makes the connection pool's size configurable; this PR makes the time a fetch may wait for a connection from that pool configurable — opt-in, with no default-behavior change.
Today the remote client acquires its pooled connection with no timeout:
With the pool's default
timeout=None, the blocking branch is an unboundedwhile self.is_pool_empty(): self._lock.wait()— so when all pooled connections are busy (with the current pool of 1: whenever two fetches overlap during evaluation-API latency), concurrent callers queue indefinitely.fetch_timeout_millisnever applies to this wait; it only bounds the socket read after acquisition. In production we observed request handlers pinned inacquire()for the entire duration of an upstream latency event, and the queued backlog continued failing requests for minutes after the API recovered (details and magnitude in #77's description).Change
Adds
fetch_pool_acquire_timeout_millistoRemoteEvaluationConfig, defaultNone:None(default): behavior is exactly today's — the acquire waits indefinitely. No existing user sees any change.acquire(timeout=...)raisingEmptyPoolError— implemented but previously unused), and exhaustion is mapped to a builtinTimeoutErrorwith a descriptive message.TimeoutErrorgets the same retry classification as the socket read timeout (socket.timeoutis the same type on Python ≥ 3.10), so when enabled, both ways a fetch can stall — waiting for a connection, or waiting on the read — are indistinguishable to callers: withfetch_retries=0(default)fetch_v2returns the usual empty variants result; with retries configured it backs off and raises after exhaustion (each attempt's wait bounded); async callbacks receive the error.(An earlier revision raised
FetchException(408), which landed in the non-retryable 4xx path where__fetch_internalreturns silently —fetch_v2produced a bareNonewith no error signal. Credit to the Bugbot comment below for flagging it; that swallow behavior for non-retryable 4xx responses predates this PR and may be worth a separate issue.)Tests: config defaults to
None; pool-exhausted fetch with the timeout set raisesTimeoutErrorpromptly instead of hanging; the timeout is classified retryable like a read timeout; end-to-endfetch_v2parity (empty dict atfetch_retries=0, notNone); existing fetch-options test updated for the newacquire(timeout=...)signature.Question for maintainers: what should the long-term default be?
This PR deliberately ships with no default-behavior change so it's an easy merge. But we'd value your opinion on the end state:
fetch_timeout_millisby default? We suspect users who setfetch_timeout_millisbelieve it already bounds their fetch latency, and an indefinite silent hang is arguably worse than a timely, handleable error — but that's a default-behavior change you may want to schedule for a major version.connection_pool_max_size)?Happy to rework this PR to whichever you prefer.
Checklist
Nonepreserves existing behavior exactly.