fix(glitchtip): медленный отказ Telegram отвечает 502 до таймаута GlitchTip (#3157)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 28s
CI / changes (pull_request) Successful in 33s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 6m25s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 28s
CI / changes (pull_request) Successful in 33s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 6m25s
Синхронная пересылка алерта была ограничена таймаутом одного HTTP-запроса (8 с), а попыток после ретранслятора (#3471) больше одной: relay, прямой путь, пауза 2 с, повтор. 16.09.2026 16:42 UTC отказ шёл медленно, GlitchTip (aiohttp ClientTimeout total=10) оборвал запрос на 10-й секунде, Caddy записал status=0, а хендлер досчитал 200 уже разорванному клиенту: uvicorn такой ответ выбрасывает вместе со строкой access-log. Исход у отправителя и приёмника расходился, 502 и фоновая доставка не срабатывали. Вся синхронная попытка теперь под asyncio.timeout(7 с). TimeoutError идёт тем же путём, что TelegramError: logger.exception, 502, фоновая доставка. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
34642e1dd5
commit
bc0ef9536e
2 changed files with 52 additions and 14 deletions
|
|
@ -43,6 +43,7 @@ regex/prefix-веток в ``rbac_guard``). Сам путь — не секре
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
|
|
@ -74,6 +75,14 @@ _TRUNCATE_SUFFIX = "\n… (обрезано)"
|
|||
# timeout=10s), поэтому наш путь не может тянуть воркерные 5 ретраев/минуты.
|
||||
_INTERACTIVE_SEND_TIMEOUT_S = 8.0
|
||||
_INTERACTIVE_SEND_MAX_RETRIES = 1
|
||||
# Потолок на ВСЮ синхронную попытку (#3157). Таймаут выше — на один HTTP-запрос,
|
||||
# а попыток больше одной: ретранслятор, при его отказе прямой путь, пауза, повтор
|
||||
# (#3471). 16.09.2026 16:42 UTC отказ шёл медленно (8 с relay + 5 с напрямую +
|
||||
# 2 с паузы), GlitchTip на 10-й секунде оборвал соединение (Caddy: status=0),
|
||||
# а хендлер досчитал себе 200 уже в пустоту — uvicorn ответ разорванному
|
||||
# клиенту молча выбрасывает вместе со строкой access-log. Отправитель и
|
||||
# приёмник расходились в исходе. Запас 3 с от 10 с GlitchTip — на Caddy и тело.
|
||||
_INTERACTIVE_SEND_DEADLINE_S = 7.0
|
||||
|
||||
|
||||
class GlitchTipAttachment(BaseModel):
|
||||
|
|
@ -234,19 +243,21 @@ async def glitchtip_webhook(
|
|||
# до api.telegram.org перед каждой отправкой. Живёт в lifespan.
|
||||
client = get_telegram_client()
|
||||
try:
|
||||
await client.send_message(
|
||||
chat_id=settings.telegram_alerts_chat_id,
|
||||
text=text,
|
||||
message_thread_id=settings.telegram_alerts_topic_id or None,
|
||||
# review H1-style бюджет (см. support.py) — синхронный HTTP-путь не
|
||||
# может легально висеть воркерные минуты ретраев.
|
||||
timeout=_INTERACTIVE_SEND_TIMEOUT_S,
|
||||
max_retries=_INTERACTIVE_SEND_MAX_RETRIES,
|
||||
)
|
||||
except TelegramError:
|
||||
async with asyncio.timeout(_INTERACTIVE_SEND_DEADLINE_S):
|
||||
await client.send_message(
|
||||
chat_id=settings.telegram_alerts_chat_id,
|
||||
text=text,
|
||||
message_thread_id=settings.telegram_alerts_topic_id or None,
|
||||
# review H1-style бюджет (см. support.py) — синхронный HTTP-путь не
|
||||
# может легально висеть воркерные минуты ретраев.
|
||||
timeout=_INTERACTIVE_SEND_TIMEOUT_S,
|
||||
max_retries=_INTERACTIVE_SEND_MAX_RETRIES,
|
||||
)
|
||||
except (TelegramError, TimeoutError):
|
||||
# Ловим общий предок, а не `TelegramApiError`: недоступность Telegram —
|
||||
# тоже «переслать не смогли», и отвечать на неё надо задуманным 502, а не
|
||||
# 500 из необработанного исключения (#3456). 502 ОСТАЁТСЯ — это честный
|
||||
# 500 из необработанного исключения (#3456). `TimeoutError` — наш потолок
|
||||
# `_INTERACTIVE_SEND_DEADLINE_S`, путь тот же (#3157). 502 ОСТАЁТСЯ — это честный
|
||||
# сигнал отправителю. Но GlitchTip вебхуки не ретраит (#3157) — без этого
|
||||
# текст алерта пропал бы бесследно, поэтому доставку ставим в фон
|
||||
# (#3471, см. app.tasks.glitchtip_alert_retry).
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import os
|
|||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import pytest
|
||||
|
|
@ -46,12 +47,13 @@ class _FakeTelegramClient:
|
|||
"""Подменяет `TelegramClient` внутри модуля `glitchtip` — никакого httpx/сети.
|
||||
|
||||
`responses` — очередь: каждый вызов `send_message` берёт следующий элемент
|
||||
(dict = успех, Exception = отказ), позволяя смоделировать «первая попытка не
|
||||
удалась, повторная фоном прошла».
|
||||
(dict = успех, Exception = отказ, float = зависнуть на столько секунд, потом
|
||||
успех), позволяя смоделировать «первая попытка не удалась, повторная фоном
|
||||
прошла».
|
||||
"""
|
||||
|
||||
calls: ClassVar[list[dict[str, Any]]] = []
|
||||
responses: ClassVar[list[dict[str, Any] | Exception]] = []
|
||||
responses: ClassVar[list[dict[str, Any] | Exception | float]] = []
|
||||
|
||||
def __init__(self, _token: str = "fake-token") -> None:
|
||||
pass
|
||||
|
|
@ -61,6 +63,9 @@ class _FakeTelegramClient:
|
|||
outcome = _FakeTelegramClient.responses.pop(0)
|
||||
if isinstance(outcome, Exception):
|
||||
raise outcome
|
||||
if isinstance(outcome, float):
|
||||
await asyncio.sleep(outcome)
|
||||
return {"message_id": 1}
|
||||
return outcome
|
||||
|
||||
|
||||
|
|
@ -109,6 +114,28 @@ def test_sync_failure_queues_background_retry_and_still_returns_502(
|
|||
assert _fake_telegram_client.calls[0]["text"] == _fake_telegram_client.calls[1]["text"]
|
||||
|
||||
|
||||
def test_slow_telegram_answers_502_before_sender_gives_up(
|
||||
client: TestClient, _fake_telegram_client: Any, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""#3157, прод 16.09.2026 16:42 UTC: отказ Telegram был медленным (relay 8 с +
|
||||
напрямую 5 с + пауза 2 с), GlitchTip оборвал запрос на 10-й секунде, 502 и
|
||||
фоновой доставки не было. Медленный отказ обязан идти тем же путём, что быстрый."""
|
||||
# GlitchTip 6.1.6 apps/alerts/webhooks.py send_webhook: ClientTimeout(total=10).
|
||||
assert glitchtip_module._INTERACTIVE_SEND_DEADLINE_S <= 8.0
|
||||
monkeypatch.setattr(glitchtip_module, "_INTERACTIVE_SEND_DEADLINE_S", 0.2)
|
||||
_fake_telegram_client.responses = [5.0, {"message_id": 2}]
|
||||
|
||||
started = time.monotonic()
|
||||
r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD)
|
||||
elapsed = time.monotonic() - started
|
||||
|
||||
assert r.status_code == 502
|
||||
assert elapsed < 2.0
|
||||
# Фон отработал: зависшая синхронная попытка + доставка фоном тем же текстом.
|
||||
assert len(_fake_telegram_client.calls) == 2
|
||||
assert _fake_telegram_client.calls[0]["text"] == _fake_telegram_client.calls[1]["text"]
|
||||
|
||||
|
||||
def test_sync_success_does_not_queue_background_retry(
|
||||
client: TestClient, _fake_telegram_client: Any
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue