Алерты GlitchTip: медленный отказ Telegram отвечает 502 и уходит в фон, а не обрывается на 10-й секунде #3555
2 changed files with 52 additions and 14 deletions
|
|
@ -43,6 +43,7 @@ regex/prefix-веток в ``rbac_guard``). Сам путь — не секре
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
|
@ -74,6 +75,14 @@ _TRUNCATE_SUFFIX = "\n… (обрезано)"
|
||||||
# timeout=10s), поэтому наш путь не может тянуть воркерные 5 ретраев/минуты.
|
# timeout=10s), поэтому наш путь не может тянуть воркерные 5 ретраев/минуты.
|
||||||
_INTERACTIVE_SEND_TIMEOUT_S = 8.0
|
_INTERACTIVE_SEND_TIMEOUT_S = 8.0
|
||||||
_INTERACTIVE_SEND_MAX_RETRIES = 1
|
_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):
|
class GlitchTipAttachment(BaseModel):
|
||||||
|
|
@ -234,6 +243,7 @@ async def glitchtip_webhook(
|
||||||
# до api.telegram.org перед каждой отправкой. Живёт в lifespan.
|
# до api.telegram.org перед каждой отправкой. Живёт в lifespan.
|
||||||
client = get_telegram_client()
|
client = get_telegram_client()
|
||||||
try:
|
try:
|
||||||
|
async with asyncio.timeout(_INTERACTIVE_SEND_DEADLINE_S):
|
||||||
await client.send_message(
|
await client.send_message(
|
||||||
chat_id=settings.telegram_alerts_chat_id,
|
chat_id=settings.telegram_alerts_chat_id,
|
||||||
text=text,
|
text=text,
|
||||||
|
|
@ -243,10 +253,11 @@ async def glitchtip_webhook(
|
||||||
timeout=_INTERACTIVE_SEND_TIMEOUT_S,
|
timeout=_INTERACTIVE_SEND_TIMEOUT_S,
|
||||||
max_retries=_INTERACTIVE_SEND_MAX_RETRIES,
|
max_retries=_INTERACTIVE_SEND_MAX_RETRIES,
|
||||||
)
|
)
|
||||||
except TelegramError:
|
except (TelegramError, TimeoutError):
|
||||||
# Ловим общий предок, а не `TelegramApiError`: недоступность Telegram —
|
# Ловим общий предок, а не `TelegramApiError`: недоступность Telegram —
|
||||||
# тоже «переслать не смогли», и отвечать на неё надо задуманным 502, а не
|
# тоже «переслать не смогли», и отвечать на неё надо задуманным 502, а не
|
||||||
# 500 из необработанного исключения (#3456). 502 ОСТАЁТСЯ — это честный
|
# 500 из необработанного исключения (#3456). `TimeoutError` — наш потолок
|
||||||
|
# `_INTERACTIVE_SEND_DEADLINE_S`, путь тот же (#3157). 502 ОСТАЁТСЯ — это честный
|
||||||
# сигнал отправителю. Но GlitchTip вебхуки не ретраит (#3157) — без этого
|
# сигнал отправителю. Но GlitchTip вебхуки не ретраит (#3157) — без этого
|
||||||
# текст алерта пропал бы бесследно, поэтому доставку ставим в фон
|
# текст алерта пропал бы бесследно, поэтому доставку ставим в фон
|
||||||
# (#3471, см. app.tasks.glitchtip_alert_retry).
|
# (#3471, см. app.tasks.glitchtip_alert_retry).
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import os
|
||||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -46,12 +47,13 @@ class _FakeTelegramClient:
|
||||||
"""Подменяет `TelegramClient` внутри модуля `glitchtip` — никакого httpx/сети.
|
"""Подменяет `TelegramClient` внутри модуля `glitchtip` — никакого httpx/сети.
|
||||||
|
|
||||||
`responses` — очередь: каждый вызов `send_message` берёт следующий элемент
|
`responses` — очередь: каждый вызов `send_message` берёт следующий элемент
|
||||||
(dict = успех, Exception = отказ), позволяя смоделировать «первая попытка не
|
(dict = успех, Exception = отказ, float = зависнуть на столько секунд, потом
|
||||||
удалась, повторная фоном прошла».
|
успех), позволяя смоделировать «первая попытка не удалась, повторная фоном
|
||||||
|
прошла».
|
||||||
"""
|
"""
|
||||||
|
|
||||||
calls: ClassVar[list[dict[str, Any]]] = []
|
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:
|
def __init__(self, _token: str = "fake-token") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
@ -61,6 +63,9 @@ class _FakeTelegramClient:
|
||||||
outcome = _FakeTelegramClient.responses.pop(0)
|
outcome = _FakeTelegramClient.responses.pop(0)
|
||||||
if isinstance(outcome, Exception):
|
if isinstance(outcome, Exception):
|
||||||
raise outcome
|
raise outcome
|
||||||
|
if isinstance(outcome, float):
|
||||||
|
await asyncio.sleep(outcome)
|
||||||
|
return {"message_id": 1}
|
||||||
return outcome
|
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"]
|
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(
|
def test_sync_success_does_not_queue_background_retry(
|
||||||
client: TestClient, _fake_telegram_client: Any
|
client: TestClient, _fake_telegram_client: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue