Files
healthlog/openspec/changes/archive/2026-08-02-cena-chitayushchego-marshruta/proposal.md
T
av 8db2ec7ff4 Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал
  пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не
  только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под
  удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а
  при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как
  «разобрано целиком»
- каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка
  собрана из всего, от чего зависит ответ: версии витрины (`data_version` с
  закреплённого соединения плюс поколение — значение локально для соединения и
  не переживает переоткрытия), горизонта измерения и области действия ресурса.
  Версия снимается до и после сборки: снятая после пометила бы устаревший снимок
  свежим номером
- предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной
  ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший
  значение точки в сыром буфере записи лога
2026-08-02 20:42:22 +03:00

67 lines
5.6 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
Первый читающий маршрут (`GET /api/v1/metrics`) обошёлся дороже, чем выглядел:
два прохода ревью измерили 693 мс и +153 МиБ живой кучи на враждебном запросе,
а непрерывная запись вместе с четырьмя читающими транзакциями внахлёст дала
рост `-wal` около 7 МБ/с без верхней границы (40 МБ за пять секунд). Приём
живёт в том же процессе, и обе цены платит он: OOM убивает приём, а доставка,
не попавшая в архив, телефоном не переприсылается. Третье проявление той же
причины — повтор: спека каталога уже требует побайтового совпадения двух
ответов на неизменившейся витрине, то есть ресурс по построению пригоден для
условного запроса, а `ETag` не выставляется вовсе.
Задача берётся **перед** Read API точек намеренно: тот строится поверх этой же
машинерии, и решать один вопрос трижды (каталог, точки, MCP) нельзя.
## What Changes
- **Периодический чекпойнт WAL.** Рядом с воркером свёртки живёт горутина,
которая раз в минуту выполняет `PRAGMA wal_checkpoint(PASSIVE)` и
останавливается дренированием, как воркер. Автоматический чекпойнт SQLite
срабатывает только по концу записи, поэтому WAL, раздутый всплеском, остаётся
неразобранным до следующей доставки — а ночью телефон молчит часами.
- **Наблюдаемость непродвинувшегося чекпойнта.** Пассивный чекпойнт не идёт
дальше снимка самого старого активного читателя и **ошибки при этом не
возвращает**: измерено — `busy=0`, `log=6256`, `checkpointed=5`. Значит
единственный различимый признак — «страниц в журнале много, перенесено
меньше», и именно он идёт в `WARN` владельцу.
- **Названный предел файла журнала.** `journal_size_limit` в строке
подключения: пассивный чекпойнт возвращает страницы в базу, но файл оставляет
на пике (измерено: 51 МБ до и после успешного чекпойнта на 12502 страницы).
Роста это не ограничивает — усечение делает первая запись после полного
чекпойнта, — и так и сказано в спеке.
- **Версия витрины и условный запрос.** Хранилище отдаёт версию витрины по
`PRAGMA data_version`, каталог выставляет `ETag`, а на `If-None-Match` с
непротухшей версией отвечает `304` **не открывая снимок вовсе**.
- **Не делается** (отложено): предел размера ответа и собственный дедлайн
маршрута — их проектирует Read API точек; потоковое измерение по метрике;
кеш ответа; `HEAD` на маршруте каталога.
## Capabilities
### New Capabilities
Новых нет: обе части ложатся на существующие домены.
### Modified Capabilities
- `storage`: добавляется **версия витрины** (признак изменения «в базу никто не
коммитил»; монотонной она не является) и **обслуживание WAL** (чекпойнт по
таймеру, признак непродвижения, остановка дренированием).
- `catalog`: добавляется **условный запрос**`ETag` на ответе каталога и
`304` на `If-None-Match`, связанный с уже существующим требованием
побайтового совпадения двух ответов на неизменившейся витрине.
## Impact
- `internal/store` — закреплённое соединение-щуп для `data_version`, метод
чекпойнта WAL, `journal_size_limit` в DSN.
- `internal/catalog` — снимок каталога уезжает вместе с версией витрины.
- `internal/httpapi` — общий помощник условного запроса (им же будут
пользоваться точки и MCP), `ETag`/`304` на маршруте каталога.
- `cmd/healthlog/serve.go` — горутина чекпойнта и её дренирование в общем
бюджете остановки.
- Схема базы **не меняется**: миграции нет.
- Контракт приёма не меняется. Контракт чтения расширяется совместимо: клиент,
не присылающий `If-None-Match`, получает ровно то же, что и сегодня.