telegram: сервис поднимается без бота и работает одним входом
- Клиент бота собирается один раз и достаётся отправителю и транспорту; разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание при сборке ограничено сроком — иначе молчащий Telegram вешал подъём. - Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой, уровень по причине — WARN для неподнятого входа, ERROR для неназванного адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count. - Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше обращения к клиенту, то есть мимо чистки на его границе.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# Недоступность Telegram подъёму сервиса не мешает
|
||||
|
||||
- **Дата:** 2026-08-13
|
||||
- **Источник:** openspec/changes/archive/2026-08-13-start-without-telegram-token/design.md
|
||||
|
||||
## Решение
|
||||
|
||||
Старт роняет только один исход сборки клиента бота — ответ Telegram «такого бота
|
||||
нет». Всё прочее, включая недоступность Telegram и истёкший срок ожидания, даёт
|
||||
подъём без Telegram: сервис работает по HTTP и говорит о неподнятом входе
|
||||
записью журнала и метрикой.
|
||||
|
||||
## Почему
|
||||
|
||||
Очевидный подход был обратный, и он же стоял в первой редакции дизайна: любой
|
||||
отказ сборки бота роняет старт, потому что «сервис, молча потерявший бота после
|
||||
опечатки в токене, перестаёт отвечать своим отправителям, и узнать об этом было
|
||||
бы неоткуда».
|
||||
|
||||
Ревью кода показало цену этого подхода. Цитата из источника:
|
||||
|
||||
> при `api.telegram.org`, отвечающем молчанием, процесс висит в `getMe` без
|
||||
> ограничения времени: HTTP-вход не открыт, панель не открыта, `/health` не
|
||||
> отвечает вовсе, воркеры не запущены, в журнале — ни строки.
|
||||
|
||||
То есть перезапуск в минуту чужой аварии оставлял без работы приём по HTTP,
|
||||
панель и конвейер, которому Telegram не нужен вовсе. Паспорт при этом называет
|
||||
основным входом приложение, а бот и HTTP API — дополняющими его.
|
||||
|
||||
Тем же ревью снят довод, на котором держалась прежняя редакция. Она утверждала,
|
||||
что «Telegram не признал бота» и «до Telegram не дошли» различать нечем. Цитата
|
||||
из источника:
|
||||
|
||||
> Различать есть чем: ответ Bot API приезжает своим типом с кодом, транспортный
|
||||
> отказ — нашим после чистки, и одно от другого отделяется проверкой типа.
|
||||
> Утверждение держалось на незнании библиотеки, а не на её устройстве.
|
||||
|
||||
Решение владельца: недоступность Telegram на старт приложения не влияет.
|
||||
|
||||
Из него следует второе, без которого оно невыполнимо: ожидание при сборке
|
||||
ограничено сроком. Пока срока не было, недоступность не отличалась от подъёма.
|
||||
Срок стоит только на сборке — длинный опрос им не ограничен, иначе он рвался бы
|
||||
на каждом круге.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` авария Telegram не роняет основной вход, панель и конвейер: сервис
|
||||
поднимается и обрабатывает уже принятое;
|
||||
- `+` опечатка в токене по-прежнему заметна: Telegram отвечает отказом, и старт
|
||||
не проходит;
|
||||
- `+` молчащий Telegram больше не вешает подъём бессрочно;
|
||||
- `−` долгая недоступность Telegram даёт сервис, работающий без бота, а
|
||||
отправители в это время не получают ответов. Замена «узнать неоткуда» —
|
||||
запись журнала при старте и признак поднятости входа метрикой;
|
||||
- `−` токен, не разбирающийся как часть адреса (перенос строки из шаблона
|
||||
выкладки), Telegram не отвергает — его отвергает разбор адреса, и такой случай
|
||||
попадает в недоступность, а не в ошибку настройки. Заметен он записью журнала,
|
||||
а не отказом старта.
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-13 | [Недоступность Telegram подъёму сервиса не мешает](ADR-2026-08-13-telegram-outage-does-not-block-startup.md) | |
|
||||
| 2026-08-12 | [Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла](ADR-2026-08-12-protected-file-behind-session.md) | |
|
||||
| 2026-08-12 | [Сессия живёт семь суток и не продлевает саму себя](ADR-2026-08-12-session-without-refresh.md) | |
|
||||
| 2026-08-12 | [Кого пускать в сервис, решает правило провайдера, а не сервис](ADR-2026-08-12-access-delegated-to-provider.md) | |
|
||||
|
||||
+20
-10
@@ -14,16 +14,20 @@
|
||||
от 2026-08-13 его нормы живут в самих шагах, их проверках и
|
||||
[conventions/go-linters.md](conventions/go-linters.md).
|
||||
|
||||
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: приём и
|
||||
опрос за сессией, имя отправителя не доходит ни до хранилища, ни до журнала,
|
||||
метка метрики несёт только известное расширение. Задачи
|
||||
- [intake](../openspec/specs/intake/spec.md) — **приём по HTTP плюс наличие
|
||||
входов**: приём и опрос за сессией, имя отправителя не доходит ни до
|
||||
хранилища, ни до журнала, метка метрики несёт только известное расширение, а
|
||||
незаданный вход Telegram не мешает подъёму. Задачи
|
||||
`http-handler-tests-never-green` и `no-user-filename-in-log` 2026-08-11,
|
||||
`pocketbase-storage` и `oidc-login` 2026-08-12. Приём из Telegram здесь не
|
||||
описан;
|
||||
`pocketbase-storage` и `oidc-login` 2026-08-12,
|
||||
`local-run-without-telegram-token` 2026-08-13. Приём из Telegram по существу —
|
||||
кто допущен и как забирается запись — здесь по-прежнему не описан;
|
||||
- [pipeline](../openspec/specs/pipeline/spec.md) — пустой прогон воркера, захват
|
||||
задачи и срок его протухания, число попыток, состояние «мертва» и пауза перед
|
||||
повтором: задачи `errors-as-instead-of-typecast` 2026-08-11 и
|
||||
`pocketbase-storage` 2026-08-12. Переходы состояний и отмена контекста посреди шага остаются
|
||||
задачи и срок его протухания, число попыток, состояние «мертва», пауза перед
|
||||
повтором и недоставленный ответ отправителю: задачи
|
||||
`errors-as-instead-of-typecast` 2026-08-11, `pocketbase-storage` 2026-08-12 и
|
||||
`local-run-without-telegram-token` 2026-08-13. Переходы состояний и отмена
|
||||
контекста посреди шага остаются
|
||||
долгом; что именно не описано, перечисляет раздел `Purpose` самой спеки;
|
||||
- [storage](../openspec/specs/storage/spec.md) — где живут запись, её метаданные
|
||||
и её файл, как файл отдаётся и что видит владелец: задача `pocketbase-storage`
|
||||
@@ -110,15 +114,21 @@
|
||||
- **Где работает, что рядом, кто перезапускает:** один контейнер на личном
|
||||
сервере, разворачивает и перезапускает Ansible из `pet-project-server`. Рядом —
|
||||
обратный прокси, который публикует HTTP-порт наружу.
|
||||
- **Пустой токен бота нельзя разворачивать раньше образа, который его понимает.**
|
||||
Пустое значение стало объявленным режимом 2026-08-13; версии до неё роняли на
|
||||
нём старт с кодом 1 **до** открытия порта. Значит, откат образа при уже
|
||||
применённом пустом токене останавливает не бот, а весь сервис — вместе с HTTP
|
||||
и панелью. Порядок: сперва образ, потом конфиг; при откате — наоборот.
|
||||
Воспроизведено ревью кода на прежней версии.
|
||||
- **Внешние зависимости поимённо и чем каждая отказывает.** Столбец «отвечает
|
||||
медленно» читается вместе с тем, что таймаута нет ни у одного обращения
|
||||
наружу — [database.md](database.md), «Настройки с числовым значением»:
|
||||
|
||||
<!-- канон: поведение → openspec/specs/conversion, recognition -->
|
||||
<!-- канон: поведение → openspec/specs/intake, pipeline -->
|
||||
|
||||
| Зависимость | Падает | Отвечает медленно | Молчит | Отдаёт мусор |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Telegram Bot API | Бот не стартует, приложение продолжает работу без него | Скачивание файла висит бесконечно | Длинный опрос пуст, новые задачи не заводятся | Файл скачался битым, отказ вылезет на конвертации |
|
||||
| Telegram Bot API | Сервис поднимается без Telegram и работает по HTTP; старт роняет только ответ «такого бота нет». Норму держит [intake](../openspec/specs/intake/spec.md), «Недоступный или незаданный вход Telegram не мешает подъёму» | На старте — ждём не дольше срока, дальше поднимаемся без Telegram. У поднятого сервиса скачивание файла висит бесконечно: там срока нет | То же, что «отвечает медленно»: на старте — подъём без Telegram по истечении срока, у поднятого — длинный опрос пуст и новые задачи не заводятся | Файл скачался битым, отказ вылезет на конвертации |
|
||||
| Yandex SpeechKit | Шаг возвращает ошибку, задача остаётся на повтор | Захват держится час, задача не двигается | Операция вечно `in progress`, повтор каждые 5 секунд | Пустой текст — задача завершается заглушкой «на записи нет текста» |
|
||||
| ↳ *остановка сервиса* | Принятие операции от отмены защищено своим пределом в 10 секунд: операцию там могли принять и начать считать деньги, а потерянный идентификатор заставил бы повтор оплатить ту же запись второй раз. Заливка в Object Storage отменяется штатно — её повтор бесплатен, объект ложится под тем же ключом | — | — | — |
|
||||
| Yandex Object Storage | Заливка падает, задача остаётся в `converted` | То же, что падение: висит до конца захвата | — | SpeechKit не прочитает объект и вернёт отказ операции |
|
||||
|
||||
@@ -116,10 +116,14 @@ Ansible из `pet-project-server`). Приложение просто читае
|
||||
- ключи внешних сервисов не пусты.
|
||||
|
||||
*Расхождение:* `LoadConfig` проверяет только существование файла и разбирает
|
||||
TOML. Пустой токен бота ловится в `NewTelegramController` уже после старта, и
|
||||
приложение продолжает работу без бота; пустые ключи Yandex ловятся в
|
||||
конструкторе распознавателя, и вот там процесс уже выходит с кодом 1. Единого
|
||||
места проверки нет.
|
||||
TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс
|
||||
выходит с кодом 1. Единого места проверки нет.
|
||||
|
||||
Токен бота под это расхождение больше не подпадает: он судится при сборке
|
||||
клиента, до подъёма сервера, и разрез у него объявленный — пустое значение
|
||||
означает отказ от входа и даёт подъём без Telegram, непустое негодное роняет
|
||||
старт как ошибка настройки. Нормирует это `openspec/specs/intake`, «Недоступный
|
||||
или незаданный вход Telegram не мешает подъёму».
|
||||
|
||||
Секция `[auth]` — первая, у которой проверка своя и стоит на старте:
|
||||
`AuthConfig.Validate()` зовётся из `main.go` сразу после загрузки и роняет
|
||||
|
||||
@@ -65,9 +65,14 @@ transcriber — **приложение, а не библиотека**: внеш
|
||||
вызывающему нужны **данные** ошибки. Достаём `errors.As`. Не плодим типы там,
|
||||
где хватает sentinel.
|
||||
|
||||
Сегодня в проекте три типизированные ошибки, и данные несёт только одна:
|
||||
`contract.JobNotFoundError` (состояние и сообщение), `contract.NoopJobError`
|
||||
(состояние), `tg.EmptyBotTokenError` (без полей — уместнее sentinel).
|
||||
Сегодня в проекте две типизированные ошибки, и обе несут данные:
|
||||
`contract.JobNotFoundError` (состояние и сообщение) и `contract.NoopJobError`
|
||||
(состояние). Третья, `tg.EmptyBotTokenError`, была ровно тем случаем, против
|
||||
которого написано правило — тип без полей, — и снята задачей
|
||||
`local-run-without-telegram-token` 2026-08-13; её место занял sentinel
|
||||
`telegram.ErrEmptyToken`. Рядом с ним живёт `contract.ErrDeliveryChannelDown` —
|
||||
тоже sentinel и по той же причине: заглушка отправителя не знает ни задачи, ни
|
||||
чата, и нести ей нечего.
|
||||
|
||||
## Граница и трансляция: приватный и публичный канал
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ capability, и третий смысл развёл бы одно слово п
|
||||
| Таймаут мягкой остановки | 5 секунд | конфиг, `[server] shutdown_timeout` | — |
|
||||
| Таймаут жёсткой остановки | 20 секунд | конфиг, `[server] force_shutdown_timeout` | — |
|
||||
| Таймаут обновлений Telegram | 10 секунд | конфиг, `[telegram] update_timeout` | — |
|
||||
| Срок ожидания Telegram при сборке клиента | 10 секунд | `adapter/telegram.ProbeTimeout` | решение, не замер: одно обращение за `getMe` укладывается в доли секунды, дольше Telegram считается недоступным и сервис поднимается без него. Длинный опрос этим сроком не ограничен — клиент подменяется сразу после сборки |
|
||||
| Качество кодирования vorbis | `-q:a 4` | `adapter/converter/ffmpeg/ffmpeg.go` | — |
|
||||
| Жизнь приглашения завести владельца панели | 30 минут | умолчание PocketBase | — |
|
||||
| Потолок размера одной записи | 8 ГиБ | `entity.MaxRecordSize` | расчётный потолок в шесть часов с запасом на видео |
|
||||
|
||||
+12
-6
@@ -226,12 +226,18 @@ API и имя не откатываются обратной правкой по
|
||||
длительность от подставного источника. Своего теста у
|
||||
`adapter/metaviewer/ffmpeg` нет; решение и его цена — в
|
||||
[adr/ADR-2026-08-11-stub-adapters-in-tests.md](adr/ADR-2026-08-11-stub-adapters-in-tests.md);
|
||||
- **всё, что требует поднять сервис целиком.** Локальный запуск роняет адаптер
|
||||
Telegram: он проверяет токен обращением к Telegram, а боевым токеном
|
||||
запускаться запрещено. Значит поведенческая верификация живым прогоном
|
||||
недоступна ни одной задаче, и заменяют её проверки поверх настоящего роутера
|
||||
хранилища. Замечено 2026-08-12 задачей `oidc-login`; своей задачи на это пока
|
||||
нет.
|
||||
- **работа сервиса с настоящими внешними собеседниками.** Сам сервис поднять
|
||||
теперь можно: с пустым `telegram.bot_token` он встаёт и работает одним входом
|
||||
(`openspec/specs/intake`, «Недоступный или незаданный вход Telegram не мешает
|
||||
подъёму»). Живой прогон — осмотр HTTP, панели, журнала и остановки — доступен
|
||||
теперь любой задаче. Прежняя формулировка «всё, что требует поднять сервис целиком»
|
||||
снята задачей `local-run-without-telegram-token` 2026-08-13.
|
||||
|
||||
**Остаток**: за настоящий Telegram, SpeechKit и Object Storage живой прогон
|
||||
по-прежнему не отвечает — боевым токеном запускаться запрещено, ключи Yandex в
|
||||
прогоне выдуманные, а распознавание подменяют в коде. Проверить живьём можно
|
||||
подъём, отказ старта, маршруты и остановку; нельзя — приём из Telegram,
|
||||
расшифровку и заливку.
|
||||
|
||||
## Журнал дефектов
|
||||
|
||||
|
||||
@@ -281,6 +281,13 @@ Telegram отправителю.
|
||||
отдают готовым. Правило — [conventions/logging.md](conventions/logging.md),
|
||||
случай — [review.md](review.md), оракул — `internal/adapter/telegram/bot_test.go`.
|
||||
|
||||
Ещё один путь закрыт задачей `local-run-without-telegram-token` 2026-08-13, и до
|
||||
неё он был открыт: токен, не разбирающийся как часть адреса (перенос строки из
|
||||
шаблона выкладки, невычищенная `%`-последовательность), роняет сборку клиента
|
||||
**раньше** обращения к нему — то есть мимо чистки на границе клиента. Отказ
|
||||
конструктора теперь чистится отдельно. Нашло это ревью кода тремя проходами
|
||||
независимо; оракул — там же, в `bot_test.go`.
|
||||
|
||||
## Что вне модели
|
||||
|
||||
Перечислить явно.
|
||||
|
||||
Reference in New Issue
Block a user