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,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-13
|
||||
@@ -0,0 +1,207 @@
|
||||
## Context
|
||||
|
||||
Сегодня старт роняет отсутствующий токен бота: сборка отправителя ответов
|
||||
возвращает «токен не задан», и процесс заканчивается раньше, чем встаёт
|
||||
HTTP-сервер. Соседний путь того же старта — сборка транспорта бота — тот же отказ
|
||||
уже терпит и сервис не роняет. Два пути одного старта решают одно и то же
|
||||
по-разному, и побеждает тот, что стоит выше.
|
||||
|
||||
Ограничение, из-за которого это дорого: боевым токеном запускаться запрещено, а
|
||||
другого действующего токена у разработчика нет. Значит, живой прогон недоступен
|
||||
никому, и всякая задача проверяется одними тестами.
|
||||
|
||||
Отправитель ответов уходит в ядро расшифровки обязательной зависимостью, и ядро
|
||||
зовёт его без проверки. Убрать отправителя, ничего не решив, — значит уронить
|
||||
процесс на первой же задаче из Telegram, лежащей в базе с прошлого запуска.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- сервис поднимается без токена бота и работает оставшимся входом;
|
||||
- отсутствие бота видно в журнале, а не выводится читателем из тишины;
|
||||
- задача из Telegram, которой некому ответить, не роняет процесс и не теряется
|
||||
молча;
|
||||
- ошибка в токене остаётся заметной.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- приём из Telegram по существу — кто допущен, как забирается запись — не
|
||||
нормируется; оговорка спеки `intake` остаётся;
|
||||
- запрет запускаться боевым токеном не снимается и не смягчается;
|
||||
- второй вход не становится необязательным «вообще»: сервис без обоих входов
|
||||
бессмыслен, но проверять это изменение не берётся;
|
||||
- отправка отложенных ответов, когда бот появится позже, не заводится.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Решение 1: пустой токен — отказ от входа, негодный непустой — ошибка настройки
|
||||
|
||||
Разрез проходит по **пустому значению**, а не по отказу сборки бота.
|
||||
|
||||
Что человек увидит иначе: разработчик стирает токен в своём файле настроек и
|
||||
поднимает сервис; владелец сервиса, опечатавшийся в токене при ротации, получает
|
||||
отказ старта вместо сервиса, молча работающего без бота.
|
||||
|
||||
**Правка после ревью кода, решение владельца 2026-08-13.** Разрез перенесён с
|
||||
«пусто / непусто» на «ответил ли Telegram»: недоступность Telegram на подъём
|
||||
сервиса не влияет. Довод — тот же, что у паспорта: основной вход не Telegram, и
|
||||
класть его целиком из-за чужой аварии нельзя. Ровно этого и требовал прежний
|
||||
разрез: перезапуск в минуту аварии Bot API оставил бы без работы приём по HTTP,
|
||||
панель и конвейер, которому Telegram не нужен вовсе.
|
||||
|
||||
Отдельно снят довод, оказавшийся ложным. Дизайн утверждал, что «Telegram не
|
||||
признал бота» и «до Telegram не дошли» различать нечем. Различать есть чем:
|
||||
ответ Bot API приезжает своим типом с кодом, транспортный отказ — нашим после
|
||||
чистки, и одно от другого отделяется проверкой типа. Утверждение держалось на
|
||||
незнании библиотеки, а не на её устройстве.
|
||||
|
||||
Из решения следует второе, без которого оно невыполнимо: **ожидание при сборке
|
||||
ограничивается сроком**. Пока срока не было, недоступность не отличалась от
|
||||
подъёма — молчащий Telegram вешал старт бессрочно, без записи, без порта и без
|
||||
пробы здоровья. Срок стоит только на сборке; длинный опрос им не ограничен, и
|
||||
клиент подменяется сразу после.
|
||||
|
||||
Рассмотрено и отвергнуто:
|
||||
|
||||
- **терпеть любой отказ сборки бота** — отвергнуто: опечатка в боевом токене
|
||||
дала бы работающий сервис без бота, и отправители перестали бы получать
|
||||
ответы. Ответ «такого бота нет» опознаётся точно, ждать по нему нечего, и он
|
||||
остаётся единственным отказом старта;
|
||||
- **ронять старт на любом отказе** — отвергнуто владельцем: авария третьей
|
||||
стороны не должна класть основной вход;
|
||||
- **отдельный ключ настройки «работать без Telegram»** — явное объявление
|
||||
намерения. Отвергнуто: имя ключа конфига объявлено необратимым, а пустое
|
||||
значение уже несёт ровно этот смысл. Второй способ сказать одно и то же
|
||||
разъезжается — останется решить, что делать с пустым токеном при выключенном
|
||||
ключе.
|
||||
|
||||
**Разрез стоит в одном месте, потому что клиент бота собирается один раз.**
|
||||
Сегодня его собирают дважды — под отправителя ответов и под транспорт бота, — и
|
||||
именно поэтому два пути разошлись. Вместо того чтобы согласовывать их вручную,
|
||||
изменение сводит сборку к одной: клиент заводится в сборке при старте и отдаётся
|
||||
обоим. Транспорт уже принимает готового клиента, так что менять надо только
|
||||
отправителя — он перестаёт принимать токен и начинает принимать клиента.
|
||||
|
||||
Что это даёт сверх опрятности: разрез «пусто / непусто» существует ровно один,
|
||||
запись о неподнятом боте по построению одна, обращение к Telegram при старте
|
||||
одно вместо двух, и подмена журнала библиотеки тоже одна. Проверять «согласованы
|
||||
ли два пути» больше не надо — второго пути нет.
|
||||
|
||||
**Цена решения:** сборка бота перестаёт терпеть негодный токен и начинает ронять
|
||||
старт. Наблюдаемо это почти ничего не меняет: сборка отправителя роняет старт на
|
||||
том же токене и сегодня, а стоит она раньше — до терпимости транспорта очередь
|
||||
попросту не доходит.
|
||||
|
||||
### Решение 2: заглушка отвечает «канала нет», а запись делает шаг
|
||||
|
||||
Когда токена нет, ядро получает отправителя-заглушку. Она ничего не отправляет и
|
||||
на всякий ответ возвращает **особое значение отказа — «канал доставки не
|
||||
поднят»**. Шаг конвейера узнаёт это значение, пишет недоставку в журнал с
|
||||
идентификатором задачи и завершается **без отказа**.
|
||||
|
||||
Что человек увидит иначе: владелец сервиса находит в журнале строку «ответ не
|
||||
доставлен» с идентификатором задачи и забирает расшифровку там же, где лежат
|
||||
остальные.
|
||||
|
||||
Почему запись делает шаг, а не сама заглушка: **идентификатора задачи у
|
||||
заглушки нет**. Контракт отправки несёт текст, чат и сообщение для ответа —
|
||||
задачу он не называет, и знать о ней отправителю незачем. Заглушка, пишущая
|
||||
`chat_id` вместо задачи, дала бы владельцу строку, по которой задачу не найти, а
|
||||
расширение контракта ради журнала потянуло бы правку и настоящего отправителя, и
|
||||
всех его вызовов.
|
||||
|
||||
Почему это не заводит в ядре ветки «а есть ли бот»: ядро ветвится не на
|
||||
устройстве сборки, а на **исходе доставки** — ровно так же, как оно уже ветвится
|
||||
на «работы нет» и «захват потерян». Особое значение отказа живёт там же, где эти
|
||||
два, и узнаётся тем же способом. Знания о том, как собран сервис, у ядра не
|
||||
появляется.
|
||||
|
||||
Источник задачи ядро при этом уже различает: ответ отправителю начинается с
|
||||
проверки источника и на задаче, пришедшей по HTTP, кончается раньше обращения к
|
||||
отправителю. Заглушка задач основного входа не увидит, и ложных строк о
|
||||
недоставке в журнале не будет.
|
||||
|
||||
Рассмотрено и отвергнуто:
|
||||
|
||||
- **ронять задачу в `failed`** — отвергнуто: расшифровка к этому моменту уже
|
||||
получена и сохранена, а «не удалось» сообщить всё равно некому. Пометка отказа
|
||||
на удавшейся работе врёт и панели, и метрике;
|
||||
- **оставлять задачу пригодной к повтору** — отвергнуто: смысл повтора в том,
|
||||
чтобы работа однажды удалась, а недоставка сама не пройдёт — бот не появится
|
||||
оттого, что задачу подождали;
|
||||
- **немая заглушка, возвращающая успех** — отвергнуто находкой ревью дизайна:
|
||||
недоставку тогда некому записать, и норма «принятая запись не теряется молча»
|
||||
оказывается нарушена именно тем решением, которое её и обслуживало;
|
||||
- **отпустить отказ заглушки наверх, не разбирая** — отвергнуто: шаг объявил бы
|
||||
отказ там, где работа сделана. Задачу это в повтор не отправит — воркеры
|
||||
опрашивают только незавершённые состояния, — но воркеру засчитается сбой,
|
||||
которого не было, и владельцу уедет запись отказа. Соврала бы и метрика, и
|
||||
журнал.
|
||||
|
||||
### Решение 3: заглушка живёт рядом с настоящим отправителем
|
||||
|
||||
Место — тот же пакет, что и отправитель Telegram: заглушка знает ровно то же,
|
||||
что и он, и подставляется в сборке при старте, как и все прочие адаптеры.
|
||||
Направление зависимостей это не нарушает, и тесты-сканеры остаются зелёными.
|
||||
Само значение отказа живёт среди контрактов — там же, где «работы нет» и «захват
|
||||
потерян»: узнаёт его ядро, а порождает адаптер, и ни один из них не зависит от
|
||||
другого.
|
||||
|
||||
**Форма значения — сентинел, а не тип с полями.** Соседи по ряду несут поле
|
||||
(состояние, идентификатор задачи) и потому объявлены типами; этому нести нечего —
|
||||
заглушка не знает ни задачи, ни чата. Прецедент сентинела в проекте есть: им же
|
||||
объявлено «токен не задан».
|
||||
|
||||
**Заодно убирается третье представление того же факта.** Кроме пустой строки в
|
||||
настройках и значения «токен не задан» в пакете отправителя, в транспорте бота
|
||||
объявлен ещё один тип с тем же смыслом, не употребляемый нигде. Он снимается
|
||||
этой же задачей: объяснять четвёртое представление дороже, чем удалить мёртвое.
|
||||
|
||||
### Решение 4: живой прогон требует заполнить ещё две секции, и это говорится вслух
|
||||
|
||||
Пустого токена мало. Настройки входа проверяются на старте и роняют процесс,
|
||||
называя незаполненные ключи; конструкторы Yandex так же роняют его на пустых
|
||||
регионе, ключах Object Storage, ключе SpeechKit и папке. Ни один из них при
|
||||
старте наружу не ходит, поэтому **выдуманных непустых значений достаточно** —
|
||||
живой прогон получается, а денег не стоит.
|
||||
|
||||
Этой задачей разрез «пустое значение — отказ от возможности» на другие секции не
|
||||
переносится: распознавание без Yandex не работает по существу, и отказ от него —
|
||||
отдельное решение с отдельной ценой. Здесь только называется, что заполнить,
|
||||
чтобы сервис поднялся.
|
||||
|
||||
Отсюда же граница правки документов: строка «живой прогон недоступен» не
|
||||
снимается, а **сужается с остатком** — стал доступен подъём и осмотр, а прогон с
|
||||
по-настоящему пустыми ключами Yandex по-прежнему невозможен.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Сервис молча работает без бота, потому что токен забыли стереть или забыли
|
||||
вписать** → строка журнала при старте называет это прямо, а не оставляет
|
||||
читателю вывод из тишины. Дальше — дело того, кто выкладывает;
|
||||
- **Отказ старта на негодном токене останавливает выкладку, которая прежде
|
||||
проходила** → это и есть цель решения 1; чинится правкой настройки, и отказ
|
||||
называет, какой ключ виноват, не называя значения;
|
||||
- **Сборка бота ходит в Telegram, и без сети старт с непустым токеном упадёт** →
|
||||
поведение не новое и не ухудшается. Сегодня сборка отправителя зовётся первой и
|
||||
роняет старт на любом отказе, так что терпимость соседнего пути на негодном
|
||||
токене всё равно не срабатывает: до неё не доходит очередь. Изменение делает
|
||||
два пути согласованными и **снимает** сеть с законного пути — пустой токен не
|
||||
ходит наружу вовсе. Срока ожидания у обращения к Telegram при этом нет, и
|
||||
задача его не заводит: таймауты у трёх внешних собеседников — известный
|
||||
недостаток проекта и предмет отдельной работы;
|
||||
- **Недоставленный ответ пропадает навсегда** → отложенной доставки нет и не
|
||||
заводится: расшифровка лежит в хранилище и достаётся через панель и HTTP API;
|
||||
- **Остановка идёт по пути, которым прежде не ходили** → останов зеркален
|
||||
сборке: чего не собрали, того не закрывают и не ждут. Проверяется прогоном
|
||||
сигнала остановки на конфиге с пустым токеном.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Схемы хранилища изменение не трогает, миграции нет, откат — обычный откат образа.
|
||||
Выкладка с заполненным токеном ведёт себя ровно как прежде.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет.
|
||||
@@ -0,0 +1,54 @@
|
||||
## Why
|
||||
|
||||
Сервис принимает записи двумя входами — ботом Telegram и HTTP API, — но
|
||||
поднимается только тогда, когда настроены оба: пустой токен бота кончает старт
|
||||
отказом раньше, чем встаёт HTTP-сервер. Боевым токеном запускаться запрещено, и
|
||||
из этого следует, что **поднять сервис и посмотреть на него живьём не может
|
||||
никто**: всякая задача, меняющая поведение, проверяется одними тестами.
|
||||
|
||||
Намерение «работать без Telegram» в сервисе уже есть — отдельное значение «токен
|
||||
не задан» и терпимость к отказу сборки бота при старте, — но один путь его
|
||||
отменяет, и потому оно ничего не значит.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Ненастроенный вход Telegram больше не мешает подъёму: сервис встаёт и работает
|
||||
оставшимся входом — принимает записи по HTTP, расшифровывает их и отдаёт текст
|
||||
туда же. Об отсутствии бота сервис говорит одной строкой журнала при старте, а
|
||||
не молчанием.
|
||||
- Задача, пришедшая из Telegram и дошедшая до ответа тогда, когда бота нет,
|
||||
доводится до конца, а факт недоставки уезжает в журнал владельца. Сегодня такая
|
||||
задача уронила бы процесс.
|
||||
- Запрет запускаться боевым токеном остаётся: рядом с ним появляется способ
|
||||
поднять сервис без токена вовсе.
|
||||
|
||||
Ломки нет: с заданным токеном не меняется ничего.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Новых нет: оба требования ложатся в capability, чей раздел `Purpose` сам
|
||||
называет их своим предметом и приглашает дописать.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `intake`: добавляется требование о подъёме с ненастроенным входом Telegram —
|
||||
сервис работает оставшимся входом. Приём из Telegram по существу (кто допущен,
|
||||
как скачивается запись) остаётся ненормированным, и оговорка спеки об этом
|
||||
сохраняется;
|
||||
- `pipeline`: добавляется требование об ответе отправителю, чей вход не поднят —
|
||||
шаг не роняется, задача доводится до конца, недоставка идёт в журнал.
|
||||
|
||||
## Impact
|
||||
|
||||
- сборка сервиса при старте: отправитель ответов и клиент бота;
|
||||
- ответ отправителю в конвейере расшифровки;
|
||||
- секция `[telegram]` конфига и её образец `config.dist.toml`;
|
||||
- `CLAUDE.md`, раздел «Запреты» — рядом с запретом на боевой токен встаёт способ
|
||||
подняться без него;
|
||||
- `docs/review.md`, подраздел «Недоступно проверке» — строка о недоступности
|
||||
живого прогона сужается;
|
||||
- `docs/architecture.md` — перечень capability и то, что каждая нормирует.
|
||||
|
||||
Внешних зависимостей, схемы хранилища и контракта HTTP API изменение не трогает.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Ревью изменения `start-without-telegram-token` — отчёт триажа
|
||||
|
||||
Прогон 2026-08-13. Отчёт сохранён оркестратором: агент триажа записывать
|
||||
`.md` не вправе.
|
||||
|
||||
## Сводка
|
||||
|
||||
- **Режим:** по графу; изменение не закоммичено, база диффа `origin/master`.
|
||||
- **Метка:** `large` — крупное × знакомое. Повторная разметка после правок
|
||||
дизайна: первая давала `medium`, исходя из того, что ядро не тронуто; правки
|
||||
ревью дизайна это допущение сняли.
|
||||
- **Гейт:** зелёный целиком, 13 шагов, включая `-race`, `golangci-lint`,
|
||||
`govulncheck`. Оракул снят проходом `autotests`, триаж гейт не перезапускал.
|
||||
- **Находок на входе:** 19 (specs 3, code 6, architecture 3, adversary 4,
|
||||
ops 3, autotests 0). **Осталось:** 6 в основном списке, 2 гипотезы,
|
||||
2 кандидата в промоут; срезы названы поимённо.
|
||||
|
||||
### План с исходом по каждой теме
|
||||
|
||||
| тема | дом | глубина | кто закрывает | исход |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| requirements | `openspec/specs` + дельты change | разбор | specs | закрыта, 3 находки |
|
||||
| autotests | `CLAUDE.md`, «Гейт» | — | autotests | закрыта, 0 находок |
|
||||
| conventions | `docs/conventions/` | разбор | code | закрыта, 6 находок |
|
||||
| architecture | `docs/architecture.md` + `passport.md` | доказательство | architecture | закрыта, 3 находки |
|
||||
| security | `docs/security.md` | доказательство | adversary | закрыта, 4 находки |
|
||||
| operations | `docs/architecture.md` «Эксплуатация» + `database.md` | доказательство | ops | закрыта, 3 находки |
|
||||
|
||||
Тем без дома нет, тем без отчёта нет. Своих тем у проекта нет, `basics` не
|
||||
запускался — все темы ядра закрыты именными проходами. Побочное следствие:
|
||||
независимого второго голоса о заниженности метки на прогоне не было.
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
### 1. Токен бота уезжает в журнал целиком при опечатке — critical
|
||||
|
||||
`internal/adapter/telegram/bot.go` возвращал отказ конструктора без чистки.
|
||||
Отказ рождается в `http.NewRequest` на разборе адреса — **до** обращения к
|
||||
клиенту, то есть мимо `safeClient` и `WithoutURL`. Токен с управляющим символом
|
||||
или неверной `%`-последовательностью печатался в журнал целиком.
|
||||
|
||||
Оракул: воспроизведено тремя проходами независимо и триажем отдельно. Нарушены
|
||||
инвариант `CLAUDE.md` «Секрет не покидает конфиг» (critical) и MUST дельта-спеки
|
||||
`intake`.
|
||||
|
||||
**Исход: починено инлайн.** Отказ конструктора пропущен через `WithoutURL`.
|
||||
Заодно закрыта дыра в собственной проверке: прежний тест судил запрет **годным**
|
||||
токеном, то есть случаем, который и так работал. Добавлен тест с токеном,
|
||||
ломающим разбор адреса.
|
||||
|
||||
### 2. Молчащий Telegram вешает старт навсегда — major
|
||||
|
||||
Клиент собран из `&http.Client{}` без срока ожидания, сборка стоит до подъёма
|
||||
сервера. При Telegram, отвечающем молчанием, процесс висит бесконечно: порт не
|
||||
слушается, `/health` не отвечает, воркеры не запущены, в журнале ни строки.
|
||||
|
||||
Поведение предсуществует изменению, но изменение **записывает его нормой**.
|
||||
Довод дизайна «различать нечем» проверяемо неверен: отказ Bot API приезжает
|
||||
типом `*tgbotapi.Error` с кодом, транспортный — нашим после чистки.
|
||||
|
||||
**Исход: развилка владельцу.** Цена дописана в таблицу отказов
|
||||
`docs/architecture.md`; выбор поведения — за владельцем.
|
||||
|
||||
## Стоит исправить сейчас
|
||||
|
||||
### 3. Сервис без Telegram выглядит здоровым — major
|
||||
|
||||
`/health` отдаёт статические `200 ok` и о входах не знает; серий `transcriber_*`
|
||||
на `/metrics` при неподнятом боте ноль; поля «доставлено» в схеме нет. Владелец
|
||||
узнаёт о потерянном входе только из журнала контейнера и только до ротации.
|
||||
|
||||
**Исход: развилка владельцу.** Попутно исправлено фактическое: обоснование нормы
|
||||
называло третьим последствием перезапись служебных полей завершённой задачи —
|
||||
такого не бывает, переход в терминальное состояние снимает захват, и повторная
|
||||
запись натыкается на «захват потерян». Довод сведён к двум последствиям.
|
||||
|
||||
### 4. Задача из Telegram, потерявшая чат, считается успешной — major
|
||||
|
||||
Было `ERROR` и отказ шага, стало `WARN` и успех. Тем самым снят самый громкий
|
||||
детектор класса, который `CLAUDE.md` называет самым коварным: колонка, выпавшая
|
||||
из пары `acquireColumns`/`acquiredRow`, обнуляет чат у задачи, попавшей к
|
||||
воркеру.
|
||||
|
||||
**Исход: развилка владельцу** — развести уровни по причине или оставить.
|
||||
|
||||
### 5. Разрез старта не держался ни одним тестом — minor
|
||||
|
||||
Инвертируй разрез — весь набор оставался зелёным.
|
||||
|
||||
**Исход: починено инлайн.** Решение вынесено из `main` в `telegramFromBot` и
|
||||
накрыто тремя случаями. Обращение к Telegram отделено от решения намеренно:
|
||||
обращение ходит в сеть и в проверке недоступно, а разрез проверять надо.
|
||||
|
||||
### 6. Образец конфига и три документа описывали снятое поведение — minor
|
||||
|
||||
`config.dist.toml` оставлял непустой плейсхолдер, хотя собственный комментарий
|
||||
рядом объявлял пустой токен режимом: копия образца старт роняла.
|
||||
`docs/conventions/config.md` описывал снятый механизм строкой «Расхождение», а
|
||||
она в этом проекте выдаёт индульгенцию будущим ревью. `docs/conventions/errors.md`
|
||||
перечислял удалённый тип. Маркер канона в `docs/architecture.md` ссылался на
|
||||
несуществующие capability.
|
||||
|
||||
**Исход: починено инлайн, все четыре места.**
|
||||
|
||||
## Чем кончились развилки — решения владельца 2026-08-13
|
||||
|
||||
- **Находка 2 (молчащий Telegram).** Выбран вариант сверх предложенных:
|
||||
недоступность Telegram на старт не влияет. Разрез перенесён с «пусто /
|
||||
непусто» на «ответил ли Telegram»: ответ «такого бота нет» роняет старт,
|
||||
недоступность даёт подъём без Telegram с записью `WARN`. Из решения следует
|
||||
срок ожидания при сборке — без него недоступность неотличима от подъёма.
|
||||
- **Находка 3 (наблюдаемость).** Выбран счётчик и признак входов: метрика
|
||||
поднятости по каждому входу и счётчик недоставленных ответов с причиной
|
||||
меткой. Колонку в задаче не заводили — это шаг схемы и необратимое.
|
||||
- **Находка 4 (уровень).** Уровни разведены: неподнятый вход — `WARN`,
|
||||
неназванный адресат — `ERROR`.
|
||||
|
||||
**Отдельно о самом прогоне.** Живой прогон, снятый проходом `adversary`, оставил
|
||||
процесс работающим на том же порту, и он держал его ещё час. Часть моих проверок
|
||||
после переделки мерила этот чужой процесс, а не новую сборку; обнаружено по
|
||||
отсутствию новой метрики, исправлено остановкой процесса и повторным прогоном.
|
||||
Кандидат в правило: прогон, поднимающий сервис, обязан снимать его за собой, а
|
||||
проверяющий — убеждаться, что порт занят его собственной сборкой.
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
- **`{"ok":true,"result":null}` считается успешной доставкой.** Механизм доказан
|
||||
на подставном сервере, вторая половина — что живой Telegram так отвечает — не
|
||||
доказана и по правилам проекта недоказуема. Предсуществует изменению.
|
||||
- **Второй `os.Exit(1)` недостижим сегодня.** Приемлемая страховка, не дефект:
|
||||
конструктор объявляет отказ в сигнатуре, и разобрать его вызывающий обязан.
|
||||
|
||||
## Кандидаты в промоут
|
||||
|
||||
- **Порядок выкладки: конфиг с пустым токеном нельзя выкатывать раньше бинаря.**
|
||||
Воспроизведено на `origin/master` в отдельном worktree: откат бинаря при уже
|
||||
применённом пустом токене останавливает весь сервис. Это правило эксплуатации,
|
||||
которого в проекте нет; дом — `docs/architecture.md`, «Эксплуатация».
|
||||
- **Сверка документов на упоминания удалённых идентификаторов.** Три из четырёх
|
||||
мест находки 6 — прямые ссылки на снесённый код и несуществующие capability.
|
||||
Ловит это `av-dev:doc-healthcheck`, которого зовут руками.
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**Что не проверил ни один проход** (`docs/review.md`, «Недоступно проверке»):
|
||||
поведение SpeechKit и Object Storage под нагрузкой; реальный профиль нагрузки;
|
||||
стойкость `ffmpeg` к вредоносному входу; поведение настоящей Authelia; поведение
|
||||
браузера с куками.
|
||||
|
||||
**Перестали проверять сознательно:** разбор вывода настоящего `ffprobe`; работа
|
||||
сервиса с настоящими внешними собеседниками. Подъём живьём стал доступен как раз
|
||||
этим изменением, но остаток — приём из Telegram, расшифровка, заливка — не
|
||||
проверяет никто.
|
||||
|
||||
**Чего не принесёт ни один прогон:**
|
||||
|
||||
1. Решения проекта не сверялись — `docs/adr/` процессный, прогон его не
|
||||
открывает. Расхождение с записанным решением ловит `av-dev:doc-healthcheck`.
|
||||
2. Записанные наблюдения не использовались — `docs/research/` тоже процессный.
|
||||
Всякое число этого отчёта снято на этом прогоне.
|
||||
3. Поимённой сверки с руководствами по стилю Go не задавал ни один проход.
|
||||
4. Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.
|
||||
|
||||
**Сработавшие потолки.** Потолок триажа: 19 находок → 6. Срезано поимённо:
|
||||
дубли в тестах (близко к вкусовщине; починено попутно, тот же файл правился
|
||||
находкой 1); избыточность представлений факта «Telegram не поднят» — шесть
|
||||
вместо четырёх, одно сократимо, последствие не названо; откат бинаря — уехал в
|
||||
промоут; вырожденный ответ библиотеки — в гипотезы.
|
||||
|
||||
**Отдельная находка о самом прогоне:** проходы отдавали сводки пересказом, и
|
||||
свои блоки «Coverage of this pass» с потолками до триажа дошли не все — узнать,
|
||||
срезал ли `code` или `adversary` что-то у себя, из отчёта нельзя.
|
||||
@@ -0,0 +1,90 @@
|
||||
## Purpose
|
||||
|
||||
Приём записи и опрос готовности задачи расшифровки: что считается принятой
|
||||
записью, что уезжает в ответ и что происходит, когда запись не удалось
|
||||
прочитать. Плюс наличие входов: с каким из них сервис вправе подняться.
|
||||
|
||||
Приём по существу описан пока **только для HTTP** — того, что нормируют
|
||||
проверки. Про вход Telegram нормировано одно: настроен он или нет и что из этого
|
||||
следует для подъёма. Кто допущен к боту и как забирается присланная им запись,
|
||||
требованиями по-прежнему не описано — требование, написанное без проверки, это
|
||||
предположение, а не норма. Первая задача, которая трогает поведение приёма из
|
||||
Telegram, дописывает его сюда.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Недоступный или незаданный вход Telegram не мешает подъёму
|
||||
|
||||
Сервис SHALL подниматься, когда вход Telegram поднять не удалось, и MUST
|
||||
продолжать работу оставшимся входом: приём по HTTP, опрос готовности и конвейер
|
||||
расшифровки работают в полном объёме. Неподнятый вход MUST быть назван в журнале
|
||||
**ровно одной** записью уровня `WARN` при старте — с причиной и без значения
|
||||
токена.
|
||||
|
||||
Исключение одно, и оно проходит по тому, **ответил ли Telegram**. Ответ «такого
|
||||
бота нет» — ошибка настройки: бот по этому токену не появится ни от ожидания, ни
|
||||
от повтора, и старт MUST кончаться отказом. Сервис, молча потерявший бота после
|
||||
опечатки в токене, перестаёт отвечать своим отправителям, и узнать об этом было
|
||||
бы неоткуда.
|
||||
|
||||
Всё прочее — недоступность: сеть, DNS, авария Bot API, истёкший срок ожидания.
|
||||
Она MUST не влиять на подъём. Основной вход сервиса — не Telegram, и класть его
|
||||
целиком из-за чужой аварии нельзя: перезапуск в такую минуту оставил бы без
|
||||
работы и приём по HTTP, и панель, и конвейер, которому Telegram не нужен вовсе.
|
||||
|
||||
Ожидание при сборке MUST быть ограничено сроком. Без него недоступность
|
||||
неотличима от подъёма: обращение к Telegram стоит на пути старта, и молчащий
|
||||
собеседник останавливал бы его бессрочно — без записи, без порта и без пробы
|
||||
здоровья.
|
||||
|
||||
Требование нормирует **наличие входа**, а не приём из него.
|
||||
|
||||
#### Scenario: Токен не задан
|
||||
|
||||
- **GIVEN** в настройках сервиса токен бота пуст
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и принимает записи по HTTP
|
||||
- **AND** конвейер расшифровки работает
|
||||
- **AND** бот не заведён, а в журнале ровно одна запись уровня `WARN` о том, что
|
||||
он не поднят и почему
|
||||
|
||||
#### Scenario: Токен задан и годен
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит токен, по которому Telegram признаёт бота
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и работает обоими входами
|
||||
|
||||
#### Scenario: Telegram не отвечает
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит непустой токен
|
||||
- **AND** Telegram недоступен либо не отвечает дольше отведённого срока
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и принимает записи по HTTP
|
||||
- **AND** бот не заведён, а в журнале запись уровня `WARN` с причиной
|
||||
- **AND** запись не несёт значения токена
|
||||
|
||||
#### Scenario: Telegram ответил, что такого бота нет
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит непустой токен
|
||||
- **AND** Telegram отвечает отказом на этот токен
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** старт кончается отказом
|
||||
- **AND** ни журнал, ни текст отказа не несут значения токена
|
||||
|
||||
### Requirement: Поднятые входы видны наблюдателю
|
||||
|
||||
Сервис SHALL отдавать признак поднятости по каждому входу приёма отдельной
|
||||
метрикой. Признак MUST выставляться при сборке входа и MUST различать поднятый
|
||||
вход и неподнятый.
|
||||
|
||||
Требование стоит на том, что иначе потерянный вход не виден ничем: проба
|
||||
здоровья отвечает «сервис работает» и при неподнятом боте, а запись журнала
|
||||
живёт до ротации и вопрос «работает ли вход сейчас» не отвечает. Метрика —
|
||||
единственный канал наблюдения, который у владельца автоматизирован.
|
||||
|
||||
#### Scenario: Вход Telegram не поднят
|
||||
|
||||
- **GIVEN** сервис поднялся без Telegram
|
||||
- **WHEN** наблюдатель читает метрики
|
||||
- **THEN** признак поднятости входа Telegram равен нулю
|
||||
- **AND** признак поднятости входа HTTP равен единице
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
## Purpose
|
||||
|
||||
Конвейер расшифровки: как задача движется по состояниям, что делает воркер,
|
||||
когда работы нет, что считается отказом шага и что бывает с ответом отправителю,
|
||||
когда доставить его некуда.
|
||||
|
||||
Описаны пустой прогон воркера, неделимость захвата и срок его протухания, число
|
||||
попыток и состояние «мертва», нарастающая пауза перед повтором, условие записи
|
||||
результата держателем захвата и недоставка ответа при неподнятом входе.
|
||||
Сознательно не описаны: цепочка переходов `created → converted → transcribe →
|
||||
done | failed`, отмена контекста посреди шага и освобождение ресурсов внешних
|
||||
клиентов. Это не значит, что такого поведения нет: оно живёт в коде, а
|
||||
требования на него не написаны, потому что требование без проверки —
|
||||
предположение, а не норма. Первая задача, которая трогает любое из
|
||||
перечисленного, дописывает его сюда.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Недоставленный ответ не роняет шаг
|
||||
|
||||
Шаг конвейера SHALL доводить задачу до достигнутого состояния, когда ответ
|
||||
отправителю доставить не удалось, и MUST не считать недоставку отказом шага.
|
||||
Недоставка MUST быть записана в журнал владельца, MUST нести идентификатор
|
||||
задачи, MUST называть причину и MUST считаться отдельной метрикой с причиной
|
||||
меткой.
|
||||
|
||||
Причин у недоставки две, и исход у них общий: **вход отправителя не поднят** —
|
||||
задача заведена прошлым запуском, а сервис поднялся без этого входа; и **адресат
|
||||
у задачи не назван** — источником значится Telegram, а чата в задаче нет.
|
||||
|
||||
Уровень записи MUST различать эти причины. Неподнятый вход — объявленный режим,
|
||||
и его уровень «может стать проблемой». Неназванный адресат — симптом порчи
|
||||
записи: у задачи из Telegram чат есть всегда, и пропасть он может только от
|
||||
дефекта, самый коварный источник которого назван инвариантом проекта про колонки
|
||||
очереди. Один уровень на обе причины утопил бы этот сигнал в потоке штатных
|
||||
записей о ненастроенном боте.
|
||||
|
||||
Общий исход — не упрощение, а следствие момента: ответ уходит **после** того, как
|
||||
достигнутое состояние сохранено. Работа к этой минуте сделана, и объявленный
|
||||
отказ засчитался бы воркеру сбоем и лёг бы владельцу записью отказа — то есть
|
||||
соврал бы про исход дважды. Повтор делу не помогает: ни бот, ни адресат от
|
||||
ожидания не появятся. Поэтому задача остаётся в достигнутом состоянии, в повтор
|
||||
не уходит и в `failed` не переводится, а причина недоставки живёт в записи
|
||||
журнала, а не в состоянии задачи.
|
||||
|
||||
Идентификатор задачи в записи обязателен: без него владелец видит, что ответ не
|
||||
ушёл, но не может найти, чей. Текст расшифровки и сообщение отправителя в эту
|
||||
запись MUST не попадать — приватность содержимого записи требование не
|
||||
ослабляет.
|
||||
|
||||
Отложенной доставки это требование не заводит: ответ, не ушедший сегодня, не
|
||||
уходит и потом. Забрать расшифровку можно там же, где лежат остальные.
|
||||
|
||||
#### Scenario: Вход отправителя не поднят
|
||||
|
||||
- **GIVEN** задача принята входом Telegram прошлым запуском сервиса
|
||||
- **AND** сервис поднялся без этого входа
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** задача остаётся в достигнутом состоянии, в повтор не уходит и в
|
||||
`failed` не переводится
|
||||
- **AND** в журнале есть запись уровня `WARN` о недоставке с идентификатором
|
||||
задачи и причиной
|
||||
- **AND** счётчик недоставленных ответов вырос с этой причиной меткой
|
||||
- **AND** ни текста расшифровки, ни сообщения отправителя в этой записи нет
|
||||
|
||||
#### Scenario: Адресат у задачи не назван
|
||||
|
||||
- **GIVEN** у задачи источником значится Telegram, а чат не назван
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** задача остаётся в достигнутом состоянии
|
||||
- **AND** в журнале есть запись уровня `ERROR` о недоставке с идентификатором
|
||||
задачи и причиной: неназванный адресат — симптом порчи записи
|
||||
|
||||
#### Scenario: Отвечать некуда, потому что запись пришла не из Telegram
|
||||
|
||||
- **GIVEN** задача принята по HTTP
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа и без записи о недоставке
|
||||
@@ -0,0 +1,127 @@
|
||||
## 1. Отправитель, который не отправляет
|
||||
|
||||
- [x] 1.1 Завести среди контрактов значение отказа «канал доставки не поднят» —
|
||||
рядом с «работы нет» и «захват потерян», узнаваемое тем же способом
|
||||
- [x] 1.2 Завести в пакете отправителя Telegram заглушку, реализующую контракт
|
||||
отправки: она ничего не отправляет и на всякий ответ возвращает это
|
||||
значение
|
||||
- [x] 1.3 Проверить тестом, что заглушка возвращает именно его и ничего не пишет
|
||||
сама
|
||||
|
||||
## 2. Ответ отправителю в конвейере
|
||||
|
||||
- [x] 2.1 Научить ответ отправителю узнавать это значение: пишется запись уровня
|
||||
`WARN` с идентификатором задачи и причиной, шаг завершается без отказа
|
||||
- [x] 2.2 Свести к тому же исходу вторую причину недоставки — задачу источника
|
||||
Telegram без названного чата: сегодня она даёт отказ шага на уже
|
||||
завершённой работе, то есть ложный сбой в счётчике воркера и перезапись
|
||||
служебных полей
|
||||
- [x] 2.3 Проверить, что в записи нет ни текста расшифровки, ни сообщения
|
||||
отправителя
|
||||
- [x] 2.4 Тест конвейера: задача источника Telegram доходит до ответа через
|
||||
заглушку — шаг без отказа, состояние задачи не откатывается, в повтор она
|
||||
не уходит и в `failed` не переводится
|
||||
- [x] 2.5 Тест: задача источника Telegram без чата даёт тот же исход
|
||||
- [x] 2.6 Тест: задача, принятая по HTTP, до заглушки не доходит и записи о
|
||||
недоставке не порождает
|
||||
|
||||
## 3. Сборка при старте
|
||||
|
||||
- [x] 3.1 Свести сборку клиента бота к одной: отправитель ответов принимает
|
||||
готового клиента вместо токена, транспорт получает того же
|
||||
- [x] 3.2 Поставить разрез в этом единственном месте: пустой токен даёт заглушку
|
||||
и одну запись уровня `WARN` о неподнятом боте, любой другой отказ сборки
|
||||
роняет старт
|
||||
- [x] 3.3 Тест на непустой токен, с которым бот не заводится: старт роняется.
|
||||
Живой Telegram не нужен — адрес подставляется, как в имеющемся тесте
|
||||
клиента
|
||||
- [x] 3.4 Проверить, что ни запись о неподнятом боте, ни текст отказа старта не
|
||||
несут значения токена
|
||||
- [x] 3.5 Проверить остановку: сигнал остановки на конфиге с пустым токеном
|
||||
завершает процесс тем же кодом и в тот же срок, что и с токеном
|
||||
- [x] 3.6 Удалить неупотребляемый тип отказа «токен пуст» в транспорте бота —
|
||||
третье представление того же факта
|
||||
|
||||
## 4. Настройки и их образец
|
||||
|
||||
- [x] 4.1 Описать в образце конфига, что пустой токен означает подъём без
|
||||
Telegram и что при этом перестаёт работать
|
||||
- [x] 4.2 Назвать там же остальные секции, без которых сервис не поднимется:
|
||||
настройки входа и Yandex требуют непустых значений, при локальном прогоне
|
||||
годятся выдуманные, наружу при старте не ходит ни одна
|
||||
|
||||
## 5. Проверки
|
||||
|
||||
- [x] 5.1 Тест на сборку отправителя с непустым токеном: прежний путь сохранён
|
||||
- [x] 5.2 Живой прогон: конфиг с пустым токеном и заполненными по 4.2 секциями,
|
||||
`GET /health` отвечает `200`, в выводе есть запись о неподнятом боте
|
||||
- [x] 5.3 `task gate` зелёный
|
||||
|
||||
## 7. Развилки ревью кода — решения владельца 2026-08-13
|
||||
|
||||
- [x] 7.1 Недоступность Telegram на старт не влияет: разрез перенесён на «ответил
|
||||
ли Telegram». Ответ «такого бота нет» роняет старт, всё прочее даёт подъём
|
||||
без Telegram с записью `WARN`
|
||||
- [x] 7.2 Ограничить ожидание при сборке клиента сроком — без него недоступность
|
||||
неотличима от подъёма; длинный опрос сроком не ограничен
|
||||
- [x] 7.3 Признак поднятости входов метрикой и счётчик недоставленных ответов с
|
||||
причиной меткой
|
||||
- [x] 7.4 Развести уровни недоставки: неподнятый вход — `WARN`, неназванный
|
||||
адресат — `ERROR` (симптом порчи записи)
|
||||
- [x] 7.5 Проверки на все четыре ветки сборки и на оба уровня недоставки
|
||||
|
||||
## 6. Документы
|
||||
|
||||
- [x] 6.1 `CLAUDE.md`, раздел «Запреты»: рядом с запретом на боевой токен встаёт
|
||||
способ подняться без него
|
||||
- [x] 6.2 `docs/review.md`, подраздел «Недоступно проверке»: строка о живом
|
||||
прогоне сужается **с остатком** — подъём и осмотр стали доступны, прогон с
|
||||
пустыми ключами Yandex по-прежнему нет
|
||||
- [x] 6.3 `docs/architecture.md`: перечень capability отражает, что нормируют
|
||||
`intake` и `pipeline` после этого изменения
|
||||
- [x] 6.4 `docs/architecture.md`, таблица отказов внешних зависимостей: строка
|
||||
про Telegram сегодня обещает дежурному «бот не стартует, приложение
|
||||
продолжает работу без него» — привести к новому разрезу ссылкой на
|
||||
требование, не перенося поведение в обзор
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
Первые три — дословно из записи задачи `local-run-without-telegram-token`.
|
||||
**Четвёртый переписан** решением владельца на чекпоинте 2026-08-13: в прежней
|
||||
редакции он требовал, чтобы задача осталась пригодной к повтору либо перешла в
|
||||
`failed`, а дизайн отверг оба исхода с ценой, и норма `pipeline` требует прямо
|
||||
обратного. Прежняя редакция сделала бы приёмку зелёной на поведении, которое это
|
||||
же изменение запрещает. Запись задачи поправлена тем же решением.
|
||||
|
||||
- Сервис поднимается с пустым токеном бота: HTTP отвечает, воркеры идут, бот не
|
||||
создан. Оракул — запуск с конфигом без токена и запрос `GET /health`: код 200.
|
||||
- Отсутствие бота названо в журнале один раз при старте, а не молчанием. Оракул —
|
||||
тот же запуск: в выводе есть строка о том, что бот не поднят и почему.
|
||||
- Поведение с настоящим токеном не изменилось. Оракул — тест на создание
|
||||
отправителя с непустым токеном: прежний путь сохранён.
|
||||
- Задача из Telegram, дошедшая до ответа при отсутствующем боте, не роняет
|
||||
процесс и не теряется молча: она остаётся в достигнутом состоянии, в повтор не
|
||||
уходит и в `failed` не переводится, а недоставка видна записью журнала с
|
||||
идентификатором задачи. Оракул — тест конвейера с задачей источника Telegram и
|
||||
заглушкой вместо отправителя.
|
||||
- Задача источника Telegram без названного чата даёт тот же исход, а не отказ
|
||||
шага. Оракул — тест конвейера на такой задаче: воркеру сбой не засчитан,
|
||||
служебные поля завершённой задачи не переписаны.
|
||||
|
||||
Сверх записи задачи — из ревью дизайна:
|
||||
|
||||
- Непустой токен, с которым бот не заводится, роняет старт. Оракул — тест с
|
||||
подставным адресом Bot API.
|
||||
- Записей о неподнятом боте ровно одна. Оракул — живой прогон с пустым токеном:
|
||||
отбор по журналу даёт одну строку, а не две.
|
||||
- Клиент бота собирается в одном месте. Оракул — отправитель ответов принимает
|
||||
клиента, а не токен, и `NewBot` зовётся из сборки при старте однажды.
|
||||
|
||||
Сверх ревью кода — решения владельца по трём развилкам:
|
||||
|
||||
- Недоступность Telegram подъёму не мешает, ответ «такого бота нет» роняет старт.
|
||||
Оракул — проверки на четыре ветки сборки.
|
||||
- Поднятость входов видна метрикой. Оракул — живой прогон с пустым токеном:
|
||||
признак входа Telegram равен нулю, признак HTTP — единице.
|
||||
- Неназванный адресат пишется уровнем `ERROR`, неподнятый вход — `WARN`. Оракул
|
||||
— проверки конвейера на обе причины.
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
Приём записи и опрос готовности задачи расшифровки: что считается принятой
|
||||
записью, что уезжает в ответ и что происходит, когда запись не удалось
|
||||
прочитать.
|
||||
прочитать. Плюс наличие входов: с каким из них сервис вправе подняться.
|
||||
|
||||
Приём по существу описан пока **только для HTTP** — того, что нормируют
|
||||
проверки. Про вход Telegram нормировано одно: настроен он или нет и что из этого
|
||||
следует для подъёма. Кто допущен к боту и как забирается присланная им запись,
|
||||
требованиями по-прежнему не описано — требование, написанное без проверки, это
|
||||
предположение, а не норма. Первая задача, которая трогает поведение приёма из
|
||||
Telegram, дописывает его сюда.
|
||||
|
||||
Описан пока **только приём по HTTP** — тот, что нормируют проверки. Приём из
|
||||
Telegram делит с ним общий шаг заведения задачи, но требований на него нет:
|
||||
требование, написанное без проверки, — предположение, а не норма. Первая задача,
|
||||
которая трогает поведение приёма из Telegram, дописывает его сюда.
|
||||
## Requirements
|
||||
### Requirement: Приём записи по HTTP
|
||||
|
||||
@@ -256,3 +259,78 @@ Telegram делит с ним общий шаг заведения задачи,
|
||||
- **WHEN** программа спрашивает состояние по неизвестному идентификатору
|
||||
- **THEN** ответ имеет код `404` и сообщение о ненайденной задаче
|
||||
|
||||
### Requirement: Недоступный или незаданный вход Telegram не мешает подъёму
|
||||
|
||||
Сервис SHALL подниматься, когда вход Telegram поднять не удалось, и MUST
|
||||
продолжать работу оставшимся входом: приём по HTTP, опрос готовности и конвейер
|
||||
расшифровки работают в полном объёме. Неподнятый вход MUST быть назван в журнале
|
||||
**ровно одной** записью уровня `WARN` при старте — с причиной и без значения
|
||||
токена.
|
||||
|
||||
Исключение одно, и оно проходит по тому, **ответил ли Telegram**. Ответ «такого
|
||||
бота нет» — ошибка настройки: бот по этому токену не появится ни от ожидания, ни
|
||||
от повтора, и старт MUST кончаться отказом. Сервис, молча потерявший бота после
|
||||
опечатки в токене, перестаёт отвечать своим отправителям, и узнать об этом было
|
||||
бы неоткуда.
|
||||
|
||||
Всё прочее — недоступность: сеть, DNS, авария Bot API, истёкший срок ожидания.
|
||||
Она MUST не влиять на подъём. Основной вход сервиса — не Telegram, и ронять его
|
||||
целиком из-за чужой аварии нельзя: перезапуск в такую минуту оставил бы без
|
||||
работы и приём по HTTP, и панель, и конвейер, которому Telegram не нужен вовсе.
|
||||
|
||||
Ожидание при сборке MUST быть ограничено сроком. Без него недоступность
|
||||
неотличима от подъёма: обращение к Telegram стоит на пути старта, и молчащий
|
||||
собеседник останавливал бы его бессрочно — без записи, без порта и без пробы
|
||||
здоровья.
|
||||
|
||||
Требование нормирует **наличие входа**, а не приём из него.
|
||||
|
||||
#### Scenario: Токен не задан
|
||||
|
||||
- **GIVEN** в настройках сервиса токен бота пуст
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и принимает записи по HTTP
|
||||
- **AND** конвейер расшифровки работает
|
||||
- **AND** бот не заведён, а в журнале ровно одна запись уровня `WARN` о том, что
|
||||
он не поднят и почему
|
||||
|
||||
#### Scenario: Токен задан и годен
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит токен, по которому Telegram признаёт бота
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и работает обоими входами
|
||||
|
||||
#### Scenario: Telegram не отвечает
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит непустой токен
|
||||
- **AND** Telegram недоступен либо не отвечает дольше отведённого срока
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он поднимается и принимает записи по HTTP
|
||||
- **AND** бот не заведён, а в журнале запись уровня `WARN` с причиной
|
||||
- **AND** запись не несёт значения токена
|
||||
|
||||
#### Scenario: Telegram ответил, что такого бота нет
|
||||
|
||||
- **GIVEN** в настройках сервиса стоит непустой токен
|
||||
- **AND** Telegram отвечает отказом на этот токен
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** старт кончается отказом
|
||||
- **AND** ни журнал, ни текст отказа не несут значения токена
|
||||
|
||||
### Requirement: Поднятые входы видны наблюдателю
|
||||
|
||||
Сервис SHALL отдавать признак поднятости по каждому входу приёма отдельной
|
||||
метрикой. Признак MUST выставляться при сборке входа и MUST различать поднятый
|
||||
вход и неподнятый.
|
||||
|
||||
Требование стоит на том, что иначе потерянный вход не виден ничем: проба
|
||||
здоровья отвечает «сервис работает» и при неподнятом боте, а запись журнала
|
||||
живёт до ротации и вопрос «работает ли вход сейчас» не отвечает. Метрика —
|
||||
единственный канал наблюдения, который у владельца автоматизирован.
|
||||
|
||||
#### Scenario: Вход Telegram не поднят
|
||||
|
||||
- **GIVEN** сервис поднялся без Telegram
|
||||
- **WHEN** наблюдатель читает метрики
|
||||
- **THEN** признак поднятости входа Telegram равен нулю
|
||||
- **AND** признак поднятости входа HTTP равен единице
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
## Purpose
|
||||
|
||||
Конвейер расшифровки: как задача движется по состояниям, что делает воркер,
|
||||
когда работы нет, и что считается отказом шага.
|
||||
когда работы нет, что считается отказом шага и что бывает с ответом отправителю,
|
||||
когда доставить его некуда.
|
||||
|
||||
Описаны пустой прогон воркера, неделимость захвата и срок его протухания, число
|
||||
попыток и состояние «мертва», нарастающая пауза перед повтором и условие записи
|
||||
результата держателем захвата. Сознательно не описаны: цепочка переходов
|
||||
`created → converted → transcribe → done | failed`, отмена контекста посреди
|
||||
шага и освобождение ресурсов внешних клиентов. Это не значит, что такого
|
||||
поведения нет: оно живёт в коде, а требования на него не написаны, потому что
|
||||
требование без проверки — предположение, а не норма. Первая задача, которая
|
||||
трогает любое из перечисленного, дописывает его сюда.
|
||||
попыток и состояние «мертва», нарастающая пауза перед повтором, условие записи
|
||||
результата держателем захвата и недоставка ответа при неподнятом входе.
|
||||
Сознательно не описаны: цепочка переходов `created → converted → transcribe →
|
||||
done | failed`, отмена контекста посреди шага и освобождение ресурсов внешних
|
||||
клиентов. Это не значит, что такого поведения нет: оно живёт в коде, а
|
||||
требования на него не написаны, потому что требование без проверки —
|
||||
предположение, а не норма. Первая задача, которая трогает любое из
|
||||
перечисленного, дописывает его сюда.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Пустой прогон воркера — не отказ
|
||||
|
||||
@@ -260,3 +263,65 @@ MUST расти с числом её попыток до объявленног
|
||||
- **THEN** задержка до следующей проверки каждый раз одна и та же
|
||||
- **AND** число попыток задачи не растёт
|
||||
|
||||
### Requirement: Недоставленный ответ не роняет шаг
|
||||
|
||||
Шаг конвейера SHALL доводить задачу до достигнутого состояния, когда ответ
|
||||
отправителю доставить не удалось, и MUST не считать недоставку отказом шага.
|
||||
Недоставка MUST быть записана в журнал владельца, MUST нести идентификатор
|
||||
задачи, MUST называть причину и MUST считаться отдельной метрикой с причиной
|
||||
меткой.
|
||||
|
||||
Причин у недоставки две, и исход у них общий: **вход отправителя не поднят** —
|
||||
задача заведена прошлым запуском, а сервис поднялся без этого входа; и **адресат
|
||||
у задачи не назван** — источником значится Telegram, а чата в задаче нет.
|
||||
|
||||
Уровень записи MUST различать эти причины. Неподнятый вход — объявленный режим,
|
||||
и его уровень «может стать проблемой». Неназванный адресат — симптом порчи
|
||||
записи: у задачи из Telegram чат есть всегда, и пропасть он может только от
|
||||
дефекта, самый коварный источник которого назван инвариантом проекта про колонки
|
||||
очереди. Один уровень на обе причины утопил бы этот сигнал в потоке штатных
|
||||
записей о ненастроенном боте.
|
||||
|
||||
Общий исход — не упрощение, а следствие момента: ответ уходит **после** того, как
|
||||
достигнутое состояние сохранено. Работа к этой минуте сделана, и объявленный
|
||||
отказ засчитался бы воркеру сбоем и лёг бы владельцу записью отказа — то есть
|
||||
соврал бы про исход дважды. Повтор делу не помогает: ни бот, ни адресат от
|
||||
ожидания не появятся. Поэтому задача остаётся в достигнутом состоянии, в повтор
|
||||
не уходит и в `failed` не переводится, а причина недоставки живёт в записи
|
||||
журнала, а не в состоянии задачи.
|
||||
|
||||
Идентификатор задачи в записи обязателен: без него владелец видит, что ответ не
|
||||
ушёл, но не может найти, чей. Текст расшифровки и сообщение отправителя в эту
|
||||
запись MUST не попадать — приватность содержимого записи требование не
|
||||
ослабляет.
|
||||
|
||||
Отложенной доставки это требование не заводит: ответ, не ушедший сегодня, не
|
||||
уходит и потом. Забрать расшифровку можно там же, где лежат остальные.
|
||||
|
||||
#### Scenario: Вход отправителя не поднят
|
||||
|
||||
- **GIVEN** задача принята входом Telegram прошлым запуском сервиса
|
||||
- **AND** сервис поднялся без этого входа
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** задача остаётся в достигнутом состоянии, в повтор не уходит и в
|
||||
`failed` не переводится
|
||||
- **AND** в журнале есть запись уровня `WARN` о недоставке с идентификатором
|
||||
задачи и причиной
|
||||
- **AND** счётчик недоставленных ответов вырос с этой причиной меткой
|
||||
- **AND** ни текста расшифровки, ни сообщения отправителя в этой записи нет
|
||||
|
||||
#### Scenario: Адресат у задачи не назван
|
||||
|
||||
- **GIVEN** у задачи источником значится Telegram, а чат не назван
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** задача остаётся в достигнутом состоянии
|
||||
- **AND** в журнале есть запись уровня `ERROR` о недоставке с идентификатором
|
||||
задачи и причиной: неназванный адресат — симптом порчи записи
|
||||
|
||||
#### Scenario: Отвечать некуда, потому что запись пришла не из Telegram
|
||||
|
||||
- **GIVEN** задача принята по HTTP
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа и без записи о недоставке
|
||||
|
||||
Reference in New Issue
Block a user