- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
67 lines
5.6 KiB
Markdown
67 lines
5.6 KiB
Markdown
## 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`, получает ровно то же, что и сегодня.
|