Files
healthlog/openspec/changes/archive/2026-08-02-otvet-i-svyortka/proposal.md
T
av d79189be18 docs: документация переведена на канон av-dev-pm
- беклог и план переехали в docs/tasks (38 задач, 11 целей), слаги
  переименованы с транслита на английские, 85 ссылок поправлены
- conventions.md разобран в docs/conventions/, local-research.md — в
  docs/research/, review-journal.md — в docs/review.md с разделом настройки
  конвейера; заведены security.md, adr/ и .pm.json
- шаг docs.py check добавлен в task gate; поведение в architecture.md помечено
  девятью маркерами долга, database.md получил настройки с числовым значением
2026-08-03 17:14:53 +03:00

87 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Why
Свёртка выполняется **внутри обработчика запроса**, поэтому время ответа равно
времени свёртки: 16 тысяч точек — 11 секунд. `WriteTimeout` в Go ставится в
`readRequest`, то есть до вызова обработчика, и его 30 секунд — общий бюджет на
всё: дочитать тело по мобильной сети, записать архив, вставить строку, свернуть.
Когда бюджет выходит, сервер считает, что отдал `200` (ошибки записи
обработчику не видно, ответ ушёл в буфер), клиент получает обрыв, а `accessLog`
пишет `status_code=200` — единственный канал наблюдаемости в этом сценарии врёт.
Бьёт это по **широким проходам** (`Today`, `Previous 7 Days`, ручной экспорт) —
ровно по тем, ради которых заведён инвариант «дыры закрываются сами».
## What Changes
- Приём отвечает `200` **после архивации тела и вставки строки `delivery`**.
Свёртка из обработчика уходит: время ответа перестаёт зависеть от ширины
доставки.
- Свёртку ведёт **фоновый воркер** — одна горутина, обработка в порядке журнала
(`received_at`, `id`) среди доставок, видимых ему на момент выборки.
- **Очередью служит сама таблица**, а не список идентификаторов в памяти:
доставка ждёт свёртки в статусе `pending`, канал несёт только сигнал «есть
работа». Отсюда три следствия: переполнять нечего (доставка и так `pending`,
это не отказ), падение процесса очередь не теряет, а «подбор `pending` при
старте» перестаёт быть отдельным кодом — это обычный проход воркера.
- Классификация исхода свёртки (`folded` / слой не выведен / содержимое не
разобрано / прочее / частичный разбор) становится **общей с пересборкой**:
один проигрыватель в `internal/replay`, а не второй рядом.
- **Исход свёртки начинает отражать доставку, а не обстоятельства.** Сегодня
`failed` пишется на любой ошибке, включая занятость базы; с воркером это
означало бы, что доставка выбывает из очереди навсегда — а конкуренция за базу
как раз становится штатной. Отмена и занятость статус больше не меняют,
доставка остаётся `pending`; всё прочее по-прежнему `failed` и возвращается
только пересборкой.
- Остановка сервиса формулируется **инвариантом**, а не обещанием досчитать:
приём прекращается раньше воркера, и после остановки нет доставки, которая
числится разобранной, а записана наполовину. Свёртка идёт на контексте,
отвязанном от остановки.
- Наблюдаемость воркера: `WARN`, когда доставка ждала свёртки дольше периода
быстрого прохода, и одна строка `INFO` о размере задолженности при старте.
Метка считается на выборке прохода, а сам проход будит не только сигнал, но и
тик — иначе «работа есть, прогресса нет» неотличимо от пустого потока.
Счётчики в `/stats` — задача `stats-endpoint`, здесь только метки в логе.
- Убирается второй, оставшийся источник молчаливого обрыва: общий `write_timeout`
(30 с) меньше `read_timeout` (5 мин), а он покрывает и чтение тела — то есть
медленная загрузка 64 МиБ обрывается независимо от свёртки. Длинный бюджет
даётся **маршруту приёма** собственным дедлайном ответа; общий таймаут и
конфиг не меняются, и прочие маршруты защиту не теряют.
- Миграция: частичный индекс по неразобранным доставкам — воркер спрашивает их
чаще, чем раз в минуту, а таблица растёт на ~300 строк в сутки.
## Capabilities
### New Capabilities
- `ingest`: приём доставки как самостоятельное поведение — что делает ответ
`200` заслуженным, когда он отдаётся, кто и в каком порядке сворачивает
принятое, что происходит с несвёрнутым при остановке и рестарте.
### Modified Capabilities
- `parsing`: требование «Разбор не влияет на код ответа приёма» уточняется —
разбор идёт **после** ответа, поэтому исход становится виден не в ответе и не
сразу, а асинхронно, в `parse_status` и в логе.
- `storage`: требование «Учёт частично разобранной доставки» уточняется — отказ
обстоятельств (отмена, занятость базы) статуса не меняет вовсе, а прежняя
формулировка «ошибка ⇒ `failed`» этого не допускала.
- `reindex`: требование «Отчёт, оракул и исход команды» получает четвёртый класс
отказа — «работа отложена по обстоятельствам»: классы у пересборки и у фоновой
свёртки общие.
## Impact
- `internal/ingest` — теряет зависимость от `internal/fold`: `Accept` кладёт
тело, учитывает доставку и будит воркер.
- `internal/replay` — общий проигрыватель (свернуть доставку, классифицировать
исход) и фоновый воркер поверх него.
- `internal/store` — выборка неразобранных доставок в порядке журнала с
курсором, сентинел занятости базы; миграция `00006` с частичным индексом.
- `internal/fold` — отмена и занятость базы больше не переводят доставку в
`failed`.
- `cmd/healthlog/serve.go` — жизненный цикл воркера и согласованная остановка.
- `internal/httpapi` — сборка `ingest.Service` без свёртки; собственный дедлайн
ответа на маршруте приёма. `config.example.toml` — комментарий к
`write_timeout`.
- `docs/architecture.md`, `docs/database.md` — путь приёма и новый индекс.