SilverElixir commited on
Commit
8fdbe53
·
1 Parent(s): 85833be

Cap Telegram flood-wait sleep so long RetryAfter cannot hold the chat lock

Browse files
Files changed (3) hide show
  1. bot.py +6 -0
  2. docs/ENVIRONMENT.md +2 -0
  3. lumen_transport_calls.py +5 -2
bot.py CHANGED
@@ -315,6 +315,12 @@ TELEGRAM_DOWNLOAD_MAX_BYTES = _env_number("TELEGRAM_DOWNLOAD_MAX_BYTES", 20 * 10
315
  TELEGRAM_GET_FILE_TIMEOUT = _env_number("TELEGRAM_GET_FILE_TIMEOUT", 15, min_value=1)
316
  # Cooldown после HTML-мусора от прокси вместо JSON: без него десятки вызовов/сек валят лавину WARNING (см. _tg_call).
317
  TELEGRAM_PROXY_COOLDOWN_SEC = _env_number("TELEGRAM_PROXY_COOLDOWN_SEC", 20, min_value=0)
 
 
 
 
 
 
318
  # Лимит ожидания следующего куска для любого провайдера: зависший стрим иначе держит лок чата бесконечно.
319
  STREAM_CHUNK_TIMEOUT_SEC = _env_number("STREAM_CHUNK_TIMEOUT_SEC", 30, min_value=1)
320
  # ── Паттерн "живой печати" при стриминге (см. lumen_typing_pace.py) ──
 
315
  TELEGRAM_GET_FILE_TIMEOUT = _env_number("TELEGRAM_GET_FILE_TIMEOUT", 15, min_value=1)
316
  # Cooldown после HTML-мусора от прокси вместо JSON: без него десятки вызовов/сек валят лавину WARNING (см. _tg_call).
317
  TELEGRAM_PROXY_COOLDOWN_SEC = _env_number("TELEGRAM_PROXY_COOLDOWN_SEC", 20, min_value=0)
318
+ # Потолок одного сна по Retry-After в _tg_call: длинный flood-wait иначе держит
319
+ # лок чата сверх бюджета маршрута (аудит 10.2026). Сам ретрай сохраняется.
320
+ TELEGRAM_FLOOD_SLEEP_MAX_SEC = _env_number("TELEGRAM_FLOOD_SLEEP_MAX_SEC", 10, min_value=1)
321
+ # TIKTOK_GROUP_REQUIRE_MENTION=1 — ссылки TikTok в группах без обращения к боту
322
+ # только пишутся в фон, а не качаются (аудит H2, 10.2026). По умолчанию выключено.
323
+ TIKTOK_GROUP_REQUIRE_MENTION = os.getenv("TIKTOK_GROUP_REQUIRE_MENTION", "0") == "1"
324
  # Лимит ожидания следующего куска для любого провайдера: зависший стрим иначе держит лок чата бесконечно.
325
  STREAM_CHUNK_TIMEOUT_SEC = _env_number("STREAM_CHUNK_TIMEOUT_SEC", 30, min_value=1)
326
  # ── Паттерн "живой печати" при стриминге (см. lumen_typing_pace.py) ──
docs/ENVIRONMENT.md CHANGED
@@ -66,6 +66,7 @@ Before publishing, configure the same `LUMEN_PROXY_SECRET` on HF and every Deno
66
  | `TELEGRAM_MEDIA_TIMEOUT` | `25s` | Timeout for downloading media files from Telegram. |
67
  | `TELEGRAM_DOWNLOAD_MAX_BYTES` | `20971520` (20 MB) | Cap on a single Telegram download (Bot API doesn't serve bigger files via `getFile`); bigger files are refused before downloading with an honest user-facing message. |
68
  | `TELEGRAM_GET_FILE_TIMEOUT` | `15s` | Timeout for the `getFile` metadata call before a download. |
 
69
  | `TTS_MAX_CHARS` | `800` | Size of one `/tts` synthesis chunk; up to `TTS_MAX_PARTS` (5) chunks are synthesized, so the accepted maximum is `TTS_MAX_CHARS × TTS_MAX_PARTS` (4000 by default). |
70
  | `TTS_SYNTH_TIMEOUT_SEC` | `60s` | Timeout for a single TTS synthesis call. Also passed into the SDK as `http_options.timeout`, so a hung provider cannot hold the chat lock. |
71
  | `TTS_TOTAL_BUDGET_SEC` | `240s` | Total deadline for all chunks of one multi-chunk TTS request; past it the text is cut and the user is told only the beginning was voiced. |
@@ -100,6 +101,7 @@ Before publishing, configure the same `LUMEN_PROXY_SECRET` on HF and every Deno
100
  | `TIKTOK_SLIDESHOW_MAX_BYTES` | `200 MB` | Total RAM cap for one slideshow post; slides past it are skipped (a typical post is under 50 MB). |
101
  | `TIKTOK_SLIDE_DOWNLOAD_CONCURRENCY` | `8` | Max slideshow slides downloaded in parallel; keeps one large post from hogging the shared HTTP connection pool. |
102
  | `TIKTOK_VIDEO_SLIDE_PROBE_CONCURRENCY` | `4` | Max concurrent `ffprobe`/`ffmpeg` processes when probing "live" video slides in a slideshow. |
 
103
 
104
  ## Persistent storage
105
 
 
66
  | `TELEGRAM_MEDIA_TIMEOUT` | `25s` | Timeout for downloading media files from Telegram. |
67
  | `TELEGRAM_DOWNLOAD_MAX_BYTES` | `20971520` (20 MB) | Cap on a single Telegram download (Bot API doesn't serve bigger files via `getFile`); bigger files are refused before downloading with an honest user-facing message. |
68
  | `TELEGRAM_GET_FILE_TIMEOUT` | `15s` | Timeout for the `getFile` metadata call before a download. |
69
+ | `TELEGRAM_FLOOD_SLEEP_MAX_SEC` | `10s` | Cap on a single `RetryAfter` sleep in Telegram calls, so a long flood-wait cannot hold the chat lock past the route budget (the retry itself is kept). |
70
  | `TTS_MAX_CHARS` | `800` | Size of one `/tts` synthesis chunk; up to `TTS_MAX_PARTS` (5) chunks are synthesized, so the accepted maximum is `TTS_MAX_CHARS × TTS_MAX_PARTS` (4000 by default). |
71
  | `TTS_SYNTH_TIMEOUT_SEC` | `60s` | Timeout for a single TTS synthesis call. Also passed into the SDK as `http_options.timeout`, so a hung provider cannot hold the chat lock. |
72
  | `TTS_TOTAL_BUDGET_SEC` | `240s` | Total deadline for all chunks of one multi-chunk TTS request; past it the text is cut and the user is told only the beginning was voiced. |
 
101
  | `TIKTOK_SLIDESHOW_MAX_BYTES` | `200 MB` | Total RAM cap for one slideshow post; slides past it are skipped (a typical post is under 50 MB). |
102
  | `TIKTOK_SLIDE_DOWNLOAD_CONCURRENCY` | `8` | Max slideshow slides downloaded in parallel; keeps one large post from hogging the shared HTTP connection pool. |
103
  | `TIKTOK_VIDEO_SLIDE_PROBE_CONCURRENCY` | `4` | Max concurrent `ffprobe`/`ffmpeg` processes when probing "live" video slides in a slideshow. |
104
+ | `TIKTOK_GROUP_REQUIRE_MENTION` | `0` | Set to `1` so TikTok links in groups without addressing the bot are only recorded as background instead of downloaded. |
105
 
106
  ## Persistent storage
107
 
lumen_transport_calls.py CHANGED
@@ -219,7 +219,10 @@ async def _tg_call(method: Any, *args: Any, call_timeout: float | None = None, r
219
  break
220
  if attempt < retries:
221
  # Флуд-контроль ждём по retry_after, остальное — коротким бэкоффом.
222
- await asyncio.sleep(_tg_retry_after_sec(exc) or 0.5 * (attempt + 1))
 
 
 
223
  if last_exc is not None and "message is not modified" in str(last_exc).lower():
224
  # "message is not modified" — валидный ответ API (семантический не-op), а не сбой: засчитываем успех, иначе повторные edit_text накручивали бы счётчик.
225
  bot._tg_proxy_breaker.note_success()
@@ -250,7 +253,7 @@ async def telegram_api_call(method: str, payload: dict, *, request_timeout: floa
250
  # См. _handle_proxy_failure — выключатель срабатывает по счётчику
251
  # подряд идущих сбоев (см. _TelegramProxyCircuitBreaker), а не на первый же сбой.
252
  await bot._handle_proxy_failure(f"вызове {method}")
253
- # Цепочку не рвём (from exc): Sentry должен видеть первопричину, а не только обёртку.
254
  raise RuntimeError(f"Network error in telegram_api_call for {method}: {exc_str}") from exc
255
  if not isinstance(data, dict) or not data.get("ok"):
256
  # Прокси round-trip'нул нормально и вернул валидный JSON — сам факт, что
 
219
  break
220
  if attempt < retries:
221
  # Флуд-контроль ждём по retry_after, остальное — коротким бэкоффом.
222
+ # Сон ограничен капом: длинный retry_after иначе держит лок чата
223
+ # сверх бюджета маршрута (аудит 10.2026). Ретрай после капа остаётся.
224
+ wait = _tg_retry_after_sec(exc) or 0.5 * (attempt + 1)
225
+ await asyncio.sleep(min(wait, bot.TELEGRAM_FLOOD_SLEEP_MAX_SEC))
226
  if last_exc is not None and "message is not modified" in str(last_exc).lower():
227
  # "message is not modified" — валидный ответ API (семантический не-op), а не сбой: засчитываем успех, иначе повторные edit_text накручивали бы счётчик.
228
  bot._tg_proxy_breaker.note_success()
 
253
  # См. _handle_proxy_failure — выключатель срабатывает по счётчику
254
  # подряд идущих сбоев (см. _TelegramProxyCircuitBreaker), а не на первый же сбой.
255
  await bot._handle_proxy_failure(f"вызове {method}")
256
+ # Цепочку сохраняем через from exc: Sentry должен видеть первопричину, а не только обёртку.
257
  raise RuntimeError(f"Network error in telegram_api_call for {method}: {exc_str}") from exc
258
  if not isinstance(data, dict) or not data.get("ok"):
259
  # Прокси round-trip'нул нормально и вернул валидный JSON — сам факт, что