From 70abee69e913a6256febb4d573f4534a6f9bda7a Mon Sep 17 00:00:00 2001 From: Huang X <1436387+kyohwang@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:02:26 +0800 Subject: [PATCH] fix(telegram): avoid polling restart hang after stall detection --- src/telegram/polling-session.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/telegram/polling-session.ts b/src/telegram/polling-session.ts index 784c8b2d759..6925b8784ae 100644 --- a/src/telegram/polling-session.ts +++ b/src/telegram/polling-session.ts @@ -15,6 +15,7 @@ const TELEGRAM_POLL_RESTART_POLICY = { const POLL_STALL_THRESHOLD_MS = 90_000; const POLL_WATCHDOG_INTERVAL_MS = 30_000; +const POLL_STOP_GRACE_MS = 15_000; type TelegramBot = ReturnType; @@ -176,6 +177,11 @@ export class TelegramPollingSession { const fetchAbortController = this.#activeFetchAbort; let stopPromise: Promise | undefined; let stalledRestart = false; + let forceCycleTimer: ReturnType | undefined; + let forceCycleResolve: (() => void) | undefined; + const forceCyclePromise = new Promise((resolve) => { + forceCycleResolve = resolve; + }); const stopRunner = () => { fetchAbortController?.abort(); stopPromise ??= Promise.resolve(runner.stop()) @@ -209,12 +215,24 @@ export class TelegramPollingSession { `[telegram] Polling stall detected (no getUpdates for ${formatDurationPrecise(elapsed)}); forcing restart.`, ); void stopRunner(); + void stopBot(); + if (!forceCycleTimer) { + forceCycleTimer = setTimeout(() => { + if (this.opts.abortSignal?.aborted) { + return; + } + this.opts.log( + `[telegram] Polling runner stop timed out after ${formatDurationPrecise(POLL_STOP_GRACE_MS)}; forcing restart cycle.`, + ); + forceCycleResolve?.(); + }, POLL_STOP_GRACE_MS); + } } }, POLL_WATCHDOG_INTERVAL_MS); this.opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true }); try { - await runner.task(); + await Promise.race([runner.task(), forceCyclePromise]); if (this.opts.abortSignal?.aborted) { return "exit"; } @@ -249,9 +267,18 @@ export class TelegramPollingSession { return shouldRestart ? "continue" : "exit"; } finally { clearInterval(watchdog); + if (forceCycleTimer) { + clearTimeout(forceCycleTimer); + } this.opts.abortSignal?.removeEventListener("abort", stopOnAbort); - await stopRunner(); - await stopBot(); + await Promise.race([ + stopRunner(), + new Promise((resolve) => setTimeout(resolve, POLL_STOP_GRACE_MS)), + ]); + await Promise.race([ + stopBot(), + new Promise((resolve) => setTimeout(resolve, POLL_STOP_GRACE_MS)), + ]); this.#activeRunner = undefined; if (this.#activeFetchAbort === fetchAbortController) { this.#activeFetchAbort = undefined;