fix(telegram): eliminate 409 polling race between PTB retry loop and conflict handler - #30158
Closed
luyao618 wants to merge 1 commit into
Closed
fix(telegram): eliminate 409 polling race between PTB retry loop and conflict handler#30158luyao618 wants to merge 1 commit into
luyao618 wants to merge 1 commit into
Conversation
…conflict handler Resolves NousResearch#30130. Two independent retry paths were racing in the Telegram adapter: 1. PTB's internal `network_retry_loop` (max_retries=-1, exponential backoff) 2. Our `_handle_polling_conflict` handler that stops + restarts polling After our error callback scheduled a recovery task, PTB would independently retry `getUpdates` with a fresh httpx connection, briefly overlapping the stale one from the restart and triggering another 409. The result was a persistent ~31s cycle of Conflict → retry → resume → Conflict. This change: - Adds `_polling_conflict_recovery_in_progress` flag (try/finally guarded) so the conflict handler is idempotent — concurrent invocations from PTB's retry loop no-op cleanly. - Suppresses `_polling_error_callback` while a recovery is in progress so PTB's retry loop cannot queue overlapping handlers or race the restart. - Passes `timeout=timedelta(seconds=60)` to all three `start_polling()` call sites (initial startup, network-error retry, conflict-recovery retry). This raises the long-poll cycle from PTB's 10s default to 60s, dramatically shrinking the overlap window that caused the 409 race. Added unit tests in tests/gateway/test_telegram_conflict.py covering: - start_polling() is called with a long timeout (>10s) - conflict handler is idempotent under concurrent invocation
Contributor
Contributor
Author
|
Closing — open too long, no longer relevant. |
This was referenced Aug 3, 2026
Closed
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.
What does this PR do?
Fixes a persistent race condition in the Telegram gateway where polling enters a ~31-second cycle of "409 Conflict, retry, resume, Conflict", causing missed updates during the recovery window.
Two independent retry paths were racing inside
gateway/platforms/telegram.py:network_retry_loop(configured withmax_retries=-1) — after invoking our error callback, PTB still independently retriedgetUpdateswith exponential backoff (1s, 1.5s, ..., 30s)._handle_polling_conflicthandler —await updater.stop(), 20s sleep, drain pools, thenstart_polling()again.Because both ran as concurrent asyncio tasks and
start_polling()defaulted to a 10s long-poll timeout, PTB retriedgetUpdates(with a fresh httpx connection) overlapped with the stale one from the previous session, triggering another 409 almost immediately. The cycle repeated forever.Related Issue
Fixes #30130
Type of Change
Changes Made
gateway/platforms/telegram.py_polling_conflict_recovery_in_progressflag onTelegramAdapter(initialised in__init__)._handle_polling_conflictinto a guarded outer wrapper plus_handle_polling_conflict_inner. The wrapper sets the flag, runs the inner method undertry/finally, and clears the flag, making concurrent invocations no-op cleanly (idempotent)._polling_error_callbackearly-return while a conflict recovery is in progress so PTB retry loop cannot queue overlapping handlers or race the restart.timeout=timedelta(seconds=60)to all threestart_polling()call sites (initial startup, network-error retry path, conflict-recovery retry). This raises the long-poll cycle from PTB 10s default to 60s, dramatically shrinking the overlap window between sessions.timedeltato the existingdatetimeimport.tests/gateway/test_telegram_conflict.pytest_start_polling_passes_long_timeoutassertsstart_polling()is invoked with atimeoutkwarg greater than 10s.test_handle_polling_conflict_is_idempotentasserts that while the recovery flag is set, additional invocations do not trigger another restart attempt or advance the conflict counter; once the flag clears, a new invocation proceeds normally.How to Test
pytest tests/gateway/test_telegram_conflict.py -qto 8 passed.Checklist
Code
fix(telegram): ...)Documentation and Housekeeping
Notes
python-telegram-bot==22.6is pinned in this repo.Updater.start_pollingtimeoutsignature isint | datetime.timedelta(verified against the installed package), sotimedelta(seconds=60)is the correct API surface.read_timeout(configured viaHERMES_TELEGRAM_HTTP_READ_TIMEOUT) is intentionally left alone, it is user-tunable and the 60s long-poll is the more impactful knob.MAX_CONFLICT_RETRIES) is preserved unchanged.