fix(telegram): default fallback transport to platform keepalive limits (stop CLOSE_WAIT fd leak) - #58804
Conversation
NousResearch#58790) httpx drops a client-level `limits` when a custom transport is supplied, so the NousResearch#31599 CLOSE_WAIT fix (platform_httpx_limits wired via _with_limits) is bypassed on the fallback-IP path: the general request pool leaks half-closed sockets until the process hits the macOS fd limit and wedges. Make TelegramFallbackTransport apply platform_httpx_limits() to its inner AsyncHTTPTransports by default so the fallback path gets the same keepalive tuning as the proxy/direct branches. Refs NousResearch#31599, NousResearch#30230. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Competing/companion fix cluster for #58790 (Telegram fallback-IP |
|
Thanks for this — you correctly identified the same root cause as #58803 (httpx drops the client-level limits kwarg when a custom transport is supplied). We went with #58803's implementation because it forwards the tuned _pool_limits (which carries max_connections = connection_pool_size) into the transport, keeping the fallback pool consistent with the proxy/direct branches, and it shipped a regression test. Your approach self-defaulted to the raw platform_httpx_limits(), which leaves max_connections unbounded and diverges slightly from the other branches. Merged via #58982 (#58982) — both of you are credited in the PR body. Closing as duplicate. Appreciate the detailed writeup! |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Simple one-line fix. Replaces deprecated len() truthiness with idiomatic Python in hermes_cli. Well-scoped.
Reviewed by Hermes Agent
Summary
When Telegram is reached via the fallback-IP transport (
api.telegram.orgnot directly reachable, soTelegramFallbackTransportis active), the CLOSE_WAIT-safe keepalive tuning added in #31599 is silently dropped, and the general request pool leaksCLOSE_WAITsockets until the process hits its file-descriptor limit and wedges.Root cause
platform_httpx_limits()(keepalive_expiry=2.0, max_keepalive=10) is injected into the PTB client viahttpx_kwargs["limits"]. But at the fallback-IP construction site a customtransportis also supplied:httpx ignores the client-level
limitsargument whenever a customtransportis passed —limitsonly ever configures the default transport that httpx builds internally. So the inner transports created inTelegramFallbackTransport.__init__run with httpx's defaults (keepalive_expiry=5.0,max_keepalive=20), not the tuned values:The polling pool is periodically reset by
_drain_polling_connections(), but the general request pool (send_message/editMessageText— heavy during streaming status edits) is not, and its keepalive tuning is bypassed. Peer-initiated FINs pile up asCLOSE_WAITand are never reaped.The in-code comment at the injection site already notes "
limitshere wins" — that reasoning holds against otherlimitskwargs, but not against a customtransport, which is the blind spot this PR closes.Observed impact (production)
On a long-lived gateway (10 days, no restart) behind a network that forces the fallback path:
macOS per-process soft limit is 256, so both wedged with
OSError: [Errno 24] Too many open files: kanban sqlite opens fail, channel-directory writes fail, and — user-visibly — new Telegram send/stream sockets can't be created, so the bot hangs forever on "receiving stream response". Restarting the gateway clears it (fds 289 → ~54) but it recurs on the same ~10-day cadence.Fixes #58790.
Fix
Have
TelegramFallbackTransportdefault its innerAsyncHTTPTransports to the sharedplatform_httpx_limits()when the caller doesn't pass an explicitlimits. Since httpx drops the client-levellimitswhenever a custom transport is supplied, the tuning has to live on the transport itself — so the fallback path now gets the same CLOSE_WAIT-safe keepalive (keepalive_expiry=2.0,max_keepalive=10) the proxy/direct branches already receive via_with_limits().Single-file change (
plugins/platforms/telegram/telegram_network.py), fully contained in the fallback path:adapter.py; the call site keepsTelegramFallbackTransport(fallback_ips).limits=is still honored if ever passed.Testing
httpx.AsyncHTTPTransport(limits=platform_httpx_limits())carrieskeepalive_expiry=2.0/max_keepalive_connections=10, and thathttpx.AsyncClient(transport=..., limits=...)ignores the client-levellimits(the root gotcha).send_message/editMessageTextover the fallback path no longer accumulatesCLOSE_WAITbeyondmax_keepalive._with_limits()still applies).