Some checks failed
CI Trade-In / backend-tests (pull_request) Failing after 2m29s
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Deep review of PR #3494 found the rate limiter unusable as designed: - H1: acquire() waited unbounded even for interactive HTTP handlers (support.py, glitchtip.py already pass a narrow `timeout` — reuse it as the queue wait cap instead of editing those handlers, which are out of scope here). New TelegramRateLimitedError (subclass of TelegramError) gives a fast, honest 502 instead of hanging past the caller's own budget. - H2: the limiter is per-process (in-memory), but two processes write to the same group (uvicorn API + bot worker) — giving each the same 18/min doubled the platform ceiling. Split into telegram_group_rate_limit_api_per_minute (12) and _bot_per_minute (6), sum kept below ~20. - M1: bridge.py sends without an explicit timeout inherited "wait forever", stalling the single-threaded poll loop (open DB session) past the SIGTERM drain window. Bounded via rate_limit_max_wait=20s at the six call sites. - M2: _locks/_sent_at grew unbounded on every unique DM chat_id. Added opportunistic cleanup of fully-expired entries. - L1: the "queue full" warning now logs once per acquire() call, not once per sleep iteration. - Corrected a factual error in the docstring: TELEGRAM_SUPPORT_CHAT_ID and TELEGRAM_ALERTS_CHAT_ID are the SAME group on prod (topics differ only). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG
58 lines
2.8 KiB
Python
58 lines
2.8 KiB
Python
"""Общий на приложение `TelegramClient` (#tg-connection-resilience).
|
||
|
||
Зачем: до этого три HTTP-ручки (`api.v1.support` ×2, `api.v1.glitchtip`) делали
|
||
`TelegramClient(token)` на КАЖДЫЙ входящий запрос, а клиент внутри пересоздавал
|
||
`httpx.AsyncClient` на каждую попытку — то есть keep-alive не было ни на каком
|
||
уровне и каждый запрос начинался с полного TCP+TLS-хендшейка до
|
||
api.telegram.org. Здесь живёт один экземпляр на процесс: создаётся в lifespan
|
||
(`app.main`), закрывается на shutdown там же.
|
||
|
||
Воркер бота (`app.tgbot_main`) сюда НЕ ходит — у него свой процесс без ASGI и
|
||
свой экземпляр на всё время жизни поллинга.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from app.core.config import settings
|
||
from app.services.tgbot.client import TelegramClient
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_client: TelegramClient | None = None
|
||
|
||
|
||
def get_telegram_client() -> TelegramClient:
|
||
"""Общий клиент приложения. Ленив: создаётся при первом обращении.
|
||
|
||
Ленивость (а не «только из lifespan») нужна из-за kill-switch: при пустом
|
||
`TELEGRAM_BOT_TOKEN` в lifespan создавать нечего, а тесты ручек поднимают
|
||
приложение без прохода через startup.
|
||
"""
|
||
global _client
|
||
if _client is None:
|
||
_client = TelegramClient(
|
||
settings.telegram_bot_token,
|
||
relay_base_url=settings.telegram_relay_base_url,
|
||
relay_secret=settings.telegram_relay_secret,
|
||
# API-роль (review H2, #3471) — см. докстринг настройки в
|
||
# app.core.config: бюджет группы разделён статически между этим
|
||
# процессом и app.tgbot_main, суммарно ниже площадочного лимита.
|
||
group_rate_limit_per_minute=settings.telegram_group_rate_limit_api_per_minute,
|
||
)
|
||
return _client
|
||
|
||
|
||
def init_telegram_client() -> TelegramClient:
|
||
"""Явное создание на старте приложения (lifespan)."""
|
||
return get_telegram_client()
|
||
|
||
|
||
async def close_telegram_client() -> None:
|
||
"""Закрывает общий клиент на shutdown. Идемпотентно."""
|
||
global _client
|
||
client, _client = _client, None
|
||
if client is not None:
|
||
await client.aclose()
|
||
logger.info("tg shared client: пул соединений закрыт")
|