Skip to content

Set TIMEOUT and CONNECTTIMEOUT on pycurl handles in CurlClient#2488

Open
jlmorton wants to merge 1 commit intocelery:mainfrom
jlmorton:fix/curl-client-missing-timeouts
Open

Set TIMEOUT and CONNECTTIMEOUT on pycurl handles in CurlClient#2488
jlmorton wants to merge 1 commit intocelery:mainfrom
jlmorton:fix/curl-client-missing-timeouts

Conversation

@jlmorton
Copy link

The CurlClient._setup_request() method configures many pycurl options but never sets TIMEOUT or CONNECTTIMEOUT on the curl handle. The Request class already defines request_timeout (30s) and connect_timeout (30s), but these values are never applied to the underlying curl transfer.

Without TIMEOUT, pycurl will wait indefinitely for a response on a socket that remains open but stops delivering data. This happens during network partitions, proxy restarts, or sidecar terminations — the TCP connection stays established (no RST, no FIN) but the remote end stops sending. The in-flight HTTP request hangs forever, which permanently breaks the SQS async polling loop: the promise callback from _get_from_sqs never fires, so _loop1 is never re-invoked and the consumer stops polling for messages. The worker process stays alive (event loop keeps running, liveness probes pass) but never consumes another message.

With TIMEOUT set, pycurl aborts the stalled transfer after request_timeout seconds and returns an error. The existing error handling in _process/_process_pending_requests picks this up, the transport reconnects, and the polling loop resumes normally.

The CurlClient._setup_request() method configures many pycurl options
but never sets TIMEOUT or CONNECTTIMEOUT on the curl handle. The
Request class already defines request_timeout (30s) and
connect_timeout (30s), but these values are never applied to the
underlying curl transfer.

Without TIMEOUT, pycurl will wait indefinitely for a response on a
socket that remains open but stops delivering data. This happens during
network partitions, proxy restarts, or sidecar terminations — the TCP
connection stays established (no RST, no FIN) but the remote end stops
sending. The in-flight HTTP request hangs forever, which permanently
breaks the SQS async polling loop: the promise callback from
_get_from_sqs never fires, so _loop1 is never re-invoked and the
consumer stops polling for messages. The worker process stays alive
(event loop keeps running, liveness probes pass) but never consumes
another message.

With TIMEOUT set, pycurl aborts the stalled transfer after
request_timeout seconds and returns an error. The existing error
handling in _process/_process_pending_requests picks this up, the
transport reconnects, and the polling loop resumes normally.
@auvipy auvipy requested review from auvipy and Copilot and removed request for Copilot March 12, 2026 12:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds explicit per-request timeout configuration to the pycurl-based async HTTP client to prevent indefinitely hanging sockets (notably impacting long-running async SQS polling).

Changes:

  • Apply request_timeout to the curl handle via pycurl.TIMEOUT.
  • Apply connect_timeout to the curl handle via pycurl.CONNECTTIMEOUT.
  • Document rationale in-code (network partitions / proxy termination leading to hangs).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +205 to +209
if request.request_timeout:
setopt(_pycurl.TIMEOUT, int(request.request_timeout))
if request.connect_timeout:
setopt(_pycurl.CONNECTTIMEOUT, int(request.connect_timeout))

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues with the new timeout handling:

  1. int() truncates float seconds and can disable timeouts. Example: request_timeout=0.5 passes the if check, but int(0.5) becomes 0; in libcurl/pycurl a TIMEOUT/CONNECTTIMEOUT of 0 means “no timeout”, reintroducing indefinite hangs.
  2. Curl handles are reused from a pool; when the if condition is false (e.g. request_timeout=None/0), the option is left unchanged and can leak the previous request’s timeout.
    Consider always setting a per-request value and using TIMEOUT_MS/CONNECTTIMEOUT_MS with rounding (and >=1ms) or explicitly unsetting/setting 0 when the timeout is disabled.
Suggested change
if request.request_timeout:
setopt(_pycurl.TIMEOUT, int(request.request_timeout))
if request.connect_timeout:
setopt(_pycurl.CONNECTTIMEOUT, int(request.connect_timeout))
request_timeout = request.request_timeout
connect_timeout = request.connect_timeout
# Always set per-request timeout values on the handle to avoid
# leaking settings between pooled Curl instances. Use millisecond
# resolution when supported, rounding and ensuring that any
# positive timeout is at least 1ms. A value of 0 disables the
# timeout in libcurl/pycurl.
if hasattr(_pycurl, 'TIMEOUT_MS'):
if request_timeout:
timeout_ms = max(1, int(round(request_timeout * 1000.0)))
else:
timeout_ms = 0
setopt(_pycurl.TIMEOUT_MS, timeout_ms)
else:
if request_timeout:
# Fall back to second granularity; ensure at least 1s for
# any positive timeout.
timeout_s = max(1, int(round(request_timeout)))
else:
timeout_s = 0
setopt(_pycurl.TIMEOUT, timeout_s)
if hasattr(_pycurl, 'CONNECTTIMEOUT_MS'):
if connect_timeout:
conn_timeout_ms = max(1, int(round(connect_timeout * 1000.0)))
else:
conn_timeout_ms = 0
setopt(_pycurl.CONNECTTIMEOUT_MS, conn_timeout_ms)
else:
if connect_timeout:
conn_timeout_s = max(1, int(round(connect_timeout)))
else:
conn_timeout_s = 0
setopt(_pycurl.CONNECTTIMEOUT, conn_timeout_s)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants