Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
## 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`, получает ровно то же, что и сегодня.
|
||||
Reference in New Issue
Block a user