Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)

This reverts commit cd7a8dfde0.
This commit is contained in:
Teknium 2026-07-10 19:12:32 -07:00 committed by GitHub
parent 5e849942c3
commit 5ba2d167ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 129 deletions

View file

@ -3161,20 +3161,21 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
"""Abort in-flight TCP I/O by shutting down pool sockets.
def force_close_tcp_sockets(client: Any) -> int:
"""Abort in-flight TCP I/O by shutting down sockets WITHOUT closing FDs.
When a provider drops a connection mid-stream or the user issues an
interrupt we want to unblock httpx's reader/writer immediately rather
than waiting for the kernel's per-connection timeout. ``shutdown(SHUT_RDWR)``
achieves that: it sends FIN, breaks any pending ``recv``/``send`` with EOF
or ``EPIPE``.
or ``EPIPE``, but does NOT release the file descriptor.
By default (``release_fds=False``) this helper does **not** call
``socket.close()`` / release the FD. That default is load-bearing for
cross-thread abort paths (#29507):
Historically this helper also called ``socket.close()`` so the FD got
released immediately, but that's unsafe when (as is the case for both the
interrupt-abort path and stale-call kill path) the helper runs on a
different thread than the one driving the request:
* The Python ``socket.socket`` we close is the SAME object held by
* The Python ``socket.socket`` we close here is the SAME object held by
httpx's pool, so closing it via Python sets its ``_fd`` to -1 and
future operations on that Python object fail safely.
* BUT the SSL wrapper (``ssl.SSLSocket``'s underlying OpenSSL ``BIO``)
@ -3186,20 +3187,15 @@ def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
wrong file (issue #29507: 24-byte TLS application-data record
clobbering SQLite header bytes 5..28).
``shutdown()`` from any thread is FD-safe; ``close()`` is not when a
stranger thread still has the BIO holding the raw FD.
The fix is to let the owning thread own the close. ``shutdown()`` from any
thread is FD-safe; ``close()`` is not. The httpx connection's own close
path which runs from the worker thread when it unwinds will release
the FD via the same ``socket.socket`` object, and because Python's socket
close atomically swaps ``_fd`` to -1 *before* issuing ``os.close``, there
is no FD-aliasing window when only one thread closes.
When the **owning** thread is disposing of a client that is no longer
shared (``_close_openai_client`` after replace / request-complete), pass
``release_fds=True``. httpx's own ``client.close()`` does not reliably
``os.close()`` sockets that were already ``shutdown()``'d, so without an
explicit ``sock.close()`` those FDs stay in kernel CLOSED state forever
and accumulate under long-lived gateways (issue #61979 — ~1 CLOSED fd
per ~6 minutes through a local proxy path).
Returns the number of sockets shut down (and optionally closed). Field
kept as ``tcp_force_closed=N`` in log lines for backwards-compatible
parsing.
Returns the number of sockets shut down. (Field kept as
``tcp_force_closed=N`` in the log line for backwards-compatible parsing.)
"""
import socket as _socket
@ -3211,13 +3207,7 @@ def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
except OSError:
# Already shut down / not connected / FD invalid — all benign.
pass
# IMPORTANT (#29507): never release FDs from stranger-thread
# abort paths. Only the owning-thread close path may opt in.
if release_fds:
try:
sock.close()
except OSError:
pass
# IMPORTANT (#29507): do NOT call sock.close() here. See docstring.
shutdown_count += 1
except Exception as exc:
_ra().logger.debug("Force-close TCP sockets sweep error: %s", exc)