fix(telegram): add timeout to start_polling() in network error handler

When the connection pool is in a degraded state after
_drain_polling_connections(), start_polling() can hang indefinitely
when both primary and fallback Telegram endpoints are unreachable. The
httpx client may hold a stale socket that neither connects nor times out
within PTB's internal flow, causing the reconnect ladder to stall at
attempt 1/10 forever.

Wrap start_polling() in asyncio.wait_for() with a 30-second timeout so a
hung call raises asyncio.TimeoutError and feeds back into the existing
retry ladder. This unblocks:
- The 10-retry ladder advances to attempt 2, 3, ...
- The heartbeat loop sees _polling_error_task.done() and can trigger recovery
- The reconnect watcher gets the adapter in _failed_platforms

Fixes #59614
This commit is contained in:
liuhao1024 2026-07-06 20:25:21 +08:00 committed by kshitij
parent ce038a0e05
commit 4aaaa206aa
2 changed files with 122 additions and 5 deletions

View file

@ -416,6 +416,12 @@ def _rich_normalize_linebreaks(text: str) -> str:
# reconnect/teardown ladder. This is an internal safety bound (not a user knob),
# applied identically at every stop() site so no path can hang on a dead socket.
_UPDATER_STOP_TIMEOUT = 15.0
# start_polling() can also hang when the connection pool is in a degraded state
# after _drain_polling_connections(), particularly when both primary and fallback
# Telegram endpoints are unreachable. Bounding start_polling() prevents the
# reconnect ladder from stalling indefinitely and allows the heartbeat loop to
# trigger its own recovery path. Refs: NousResearch/hermes-agent#59614
_UPDATER_START_TIMEOUT = 30.0
class TelegramAdapter(BasePlatformAdapter):
@ -2072,11 +2078,26 @@ class TelegramAdapter(BasePlatformAdapter):
try:
if not app:
raise RuntimeError("Telegram application was torn down during reconnect")
await app.updater.start_polling(
allowed_updates=Update.ALL_TYPES,
drop_pending_updates=False,
error_callback=self._polling_error_callback_ref,
)
# Guard start_polling() with a timeout: when the connection pool is
# in a degraded state (e.g., after _drain_polling_connections()), the
# httpx client may hold a stale socket that neither connects nor times
# out within PTB's internal flow. Bounding start_polling() prevents
# the reconnect ladder from stalling indefinitely and allows the
# heartbeat loop to trigger its own recovery path.
# Refs: NousResearch/hermes-agent#59614
try:
await asyncio.wait_for(
app.updater.start_polling(
allowed_updates=Update.ALL_TYPES,
drop_pending_updates=False,
error_callback=self._polling_error_callback_ref,
),
timeout=_UPDATER_START_TIMEOUT,
)
except asyncio.TimeoutError:
raise RuntimeError(
"start_polling() timed out — connection pool may be wedged"
)
logger.info(
"[%s] Telegram polling resumed after network error (attempt %d)",
self.name, attempt,