- Создан docs/adr/ADR-002-offline-pattern.md (Accepted, дата 2026-03-20) с секцией Open Questions: #1001, охват 78.75%, ACTION:/конвенция #1049 - ADR-004: добавлен "exponential backoff согласно решению #1046" к строке 429/retry_after - ARCHITECTURE.md: добавлена вводная фраза "ADR-файлы хранятся в docs/adr/" и строка таблицы для ADR-002 (Accepted) - tests/test_arch_004.py: удалены 4 теста на отсутствие ADR-002, устаревшие после создания нового ADR-002 (BATON-ARCH-014 supersedes) - tests/test_arch_014.py: 14 новых тестов для критериев приёмки - Все 216 тестов: passed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.9 KiB
ADR-004: Стратегия отправки в Telegram (прямой vs агрегатор)
Дата: 2026-03-20 Статус: Accepted Автор: Architect Agent (Kin pipeline, BATON-003) Решения: #1009, #1010, #1011, #1014, #1017, #1020
Контекст
При нажатии кнопки SOS бэкенд должен доставить уведомление в Telegram-группу через Bot API. Вопрос: отправлять каждый сигнал отдельным sendMessage (direct) или буферизовать и отправлять пачками (aggregator)?
Rate limits Telegram Bot API
| Ограничение | Значение |
|---|---|
| Один чат (любой тип) | ~1 msg/сек |
| Группа | 20 msg/минуту |
| Глобально | ~30 msg/сек |
При превышении: HTTP 429 + parameters.retry_after. При последовательных 429 рекомендуется exponential backoff согласно решению #1046.
Оценка пиковой нагрузки (#1017)
Параметры:
- Зарегистрированных пользователей: 300–400
- Частота использования: экстренная кнопка — событие редкое
- Типичная частота: ~1 нажатие/неделю по всей базе
Сценарии:
| Сценарий | Сигналов/мин | Telegram msg/мин | Статус |
|---|---|---|---|
| Нормальный (1 пользователь) | 1 | 1 | Лимит: 20/мин ✅ |
| Групповая тревога (5 человек за минуту) | 5 | 5 | Лимит: 20/мин ✅ |
| Массовая тревога (20 за минуту) | 20 | 20 | На грани лимита ⚠️ |
| Абсурдный worst case (все 400) | 400 | 400 | Далеко за лимитом ❌ |
Вывод: реалистичный пик — 5–10 одновременных сигналов. «Все 400 нажали» — нереалистичный сценарий (разные страны, разные часовые пояса, разные ситуации). Лимит 20 msg/мин покрывает 99.9% случаев.
Решение #1017: оценки нагрузки в ADR могут быть «drastically wrong» → архитектура должна позволять включить агрегатор без переписывания. Код агрегатора УЖЕ реализован в telegram.py:51-121 — он просто не активирован.
Варианты
Вариант A: Прямой sendMessage (1 сигнал = 1 сообщение)
[PWA] POST /api/signal
→ [Backend] save_signal() в SQLite
→ [Backend] POST api.telegram.org/sendMessage
→ [Telegram Group]
Плюсы:
- Минимальная задержка: сигнал → сообщение < 1 секунды
- Простейшая логика: нет буфера, нет фоновых задач, нет lock
- Отладка тривиальна: 1 HTTP вызов, 1 ответ
- Прозрачность: каждый сигнал виден отдельно в Telegram
Минусы:
- При 20+ сигналов/мин → 429 от Telegram → потеря или задержка сообщений
- Каждый сигнал = отдельное уведомление в группе (шум при массовом событии)
Вариант B: Агрегатор (буфер 10 секунд → batch message)
[PWA] POST /api/signal
→ [Backend] add_signal() в in-memory buffer
→ [Background task: 10 sec flush loop]
→ [Backend] POST sendMessage (пакетное сообщение)
Плюсы:
- Гарантированное соблюдение rate limits: 6 msg/мин max (1 каждые 10 сек)
- Компактные сообщения: "🚨 3 сигнала [12:05:00–12:05:08]" вместо 3 отдельных
- Масштабируется до 1000+ сигналов/мин
Минусы:
- Задержка: до 10 секунд между сигналом и сообщением
- Сложность: asyncio.Lock, background task, graceful shutdown, buffer management
- In-memory буфер: при crash uvicorn — сигналы в буфере потеряны (SQLite сохраняет, но Telegram не получит)
- Один uvicorn worker обязателен (буфер в памяти, не shared)
Вариант C: Внешняя очередь (Redis/RabbitMQ)
Плюсы:
- Масштабируется горизонтально
- Буфер переживёт перезапуск процесса
Минусы:
- Дополнительная зависимость (Redis) для 1 нажатия/неделю
- Deployment complexity растёт кратно
- Абсурдный overkill для 300-400 пользователей
Решение
Выбран Вариант A: Прямой sendMessage для v1
Агрегатор (Вариант B) оставлен в коде (telegram.py:SignalAggregator) но НЕ активирован. При росте нагрузки — включается изменением 2 строк в main.py.
Обоснование
-
#1020: агрегатор нужен только при 20+ одновременных сигналов/мин. Для 300–400 пользователей это нереалистичный сценарий. Агрегатор — premature optimization.
-
Задержка неприемлема для экстренного приложения. 10-секундный буфер = 10 секунд ожидания, пока спасатель увидит сигнал. При прямой отправке — < 1 секунды.
-
Код агрегатора уже написан.
telegram.py:51-121содержит полныйSignalAggregatorс lock, flush, rate limiting. Включение в v2:# main.py lifespan: раскомментировать 2 строки aggregator = SignalAggregator(interval=10) asyncio.create_task(aggregator.run()) -
#1017 учтён: если оценка нагрузки окажется «drastically wrong» — переключение на агрегатор не требует переписывания архитектуры.
Безопасность Telegram-интеграции
Webhook secret validation (#1010)
Каждый входящий запрос на POST /api/webhook/telegram ОБЯЗАН содержать заголовок:
X-Telegram-Bot-Api-Secret-Token: <WEBHOOK_SECRET>
Реализовано: middleware.py:verify_webhook_secret() — FastAPI dependency, 403 при несовпадении.
HTTPS обязателен (#1011)
- Telegram принимает webhook ТОЛЬКО на HTTPS URL
- Поддерживаемые порты: 443, 80, 88, 8443
- TLS 1.2 минимум
- CA-signed сертификат (Let's Encrypt достаточен)
setWebhook vs getUpdates (#1009)
Telegram Bot API: setWebhook и getUpdates взаимоисключающи. Baton использует ТОЛЬКО setWebhook. Вызов getUpdates автоматически удалит webhook.
Формат сообщения в Telegram (v1)
Прямая отправка (1 сигнал = 1 сообщение)
🚨 SOS от Алиса
📍 55.7558, 37.6173 (±15м)
🕐 2026-03-20 14:05:23 UTC
Без геолокации:
🚨 SOS от Алиса
📍 Геолокация недоступна
🕐 2026-03-20 14:05:23 UTC
Если имя неизвестно (UUID не зарегистрирован):
🚨 SOS от 550e8400
📍 Геолокация недоступна
🕐 2026-03-20 14:05:23 UTC
Последствия
-
Один uvicorn worker достаточен для v1. Агрегатор не активен → нет shared state → но для консистентности рекомендуется 1 worker.
-
При 429 от Telegram: текущий код (
telegram.py:22-25) обрабатывает retry_after корректно. Сигнал не теряется — он уже в SQLite. Telegram-сообщение задержится наretry_afterсекунд. -
Переход на v2 (агрегатор): изменение 3 строк в
main.py+ изменение signal endpoint (вместо прямого sendMessage →aggregator.add_signal()). Оценка: 30 минут работы dev-агента. -
Мониторинг: логировать 429 ответы от Telegram. Если частота > 1/день — триггер для включения агрегатора.