- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
78 lines
7.5 KiB
Markdown
78 lines
7.5 KiB
Markdown
# Read API: точки, выбор слоя, свёртка по сетке
|
||
|
||
**Приоритет:** высокий
|
||
|
||
Сейчас данные достаются только `sqlite3` на хосте. Все три сценария —
|
||
агент-медик, трекер тренировок, фитнес-игра — упираются в отсутствие чтения.
|
||
|
||
Формы запроса ровно две, и это один запрос с необязательным параметром:
|
||
`?from&to` — все значения за период (вес, лекарства, симптомы), `?from&to&bucket`
|
||
— с разбивкой (шаги, энергия).
|
||
|
||
Решение по размеру ответа (вариант «б»): разбивка не задана и ответ не влезает —
|
||
сервер сам берёт сетку погрубее и **называет её в ответе**; разбивка задана явно
|
||
и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие
|
||
существенно: иначе агент, попросивший минутную сетку, получит суточные суммы.
|
||
|
||
**Отдача тренировок и записей входит сюда же.** Разбор и хранение сущностей с
|
||
собственным `id` сделаны (change `2026-08-02-trenirovki-i-zapisi`), а эндпоинтов
|
||
нет: тренировка с маршрутом и записи `stateOfMind` лежат в витрине и наружу не
|
||
отдаются. Вводить их раньше конверта ответа значило бы задать контракт
|
||
мимоходом, поэтому `GET /workouts`, `GET /workouts/{id}` и
|
||
`GET /records/{kind}` закрываются этой задачей — вместе с формой конверта и
|
||
правилом размера ответа. Второй сценарий паспорта (трекер) до тех пор не закрыт.
|
||
|
||
Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом
|
||
каждый, тренировка отдаётся одним пакетом вместе с маршрутом, а в ответе всегда
|
||
видно `layer`, `bucket` и `aggregation`.
|
||
|
||
**Порог неполного ведра решается здесь, и вместе с ним — его полярность.**
|
||
Каталог и род агрегации сделаны (change `2026-08-02-katalog-i-rod-agregacii`), и
|
||
измерению порог заполненности не понадобился: у него две конкурирующие гипотезы,
|
||
и неполный час не сходится ни с одной сам собой. Свёртке в ответе он нужен, а
|
||
готовые решения задают его **противоположно**: Graphite `xFilesFactor` — доля
|
||
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере, один
|
||
параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе
|
||
величины выглядят как «0.5», означая разное; полярность придётся назвать вслух в
|
||
`architecture.md`, иначе через полгода два места кода поймут поле по-разному.
|
||
|
||
**Предел размера ответа тоже здесь, и он унаследовал измеренную цену.** У
|
||
каталога предела нет намеренно: правило размера — общее для маршрутов чтения, и
|
||
задавать его мимоходом на первой ручке значило бы решить контракт до того, как
|
||
известна форма тяжёлого ответа. Каталог станет первым его потребителем.
|
||
|
||
Цена измерена на каталоге (задача «цена читающего маршрута», закрыта чекпойнтом
|
||
WAL и условным запросом): 693 мс и +153 МиБ живой кучи на враждебном запросе
|
||
(20 метрик × 8 часов × 5000 точек), при том что приём в том же процессе уже даёт
|
||
пик 768 МиБ на теле 40 МиБ. Условный запрос снял повтор, но первый запрос стоит
|
||
столько же, а множители «метрики × окно × точки × одновременные запросы»
|
||
по-прежнему без потолка. Сюда же уезжают отложенные варианты той задачи:
|
||
собственный дедлайн маршрута и потоковое измерение по метрике (второе — только
|
||
если счётчик заговорит).
|
||
|
||
**Машинерия условного запроса готова, и её надо взять, а не написать заново.**
|
||
`store.VersionedRead` держит правило «версией, снятой после чтения, не
|
||
подписывать»; `httpapi` — разбор `If-None-Match` и `304`. Метка обязана нести
|
||
**область действия**: у точек ответ есть функция параметров запроса, и
|
||
`etag(scope, version)` требует их канонизированную форму — иначе `304` ответит
|
||
на другой набор данных. Детали — `docs/architecture.md`, «Условный запрос».
|
||
|
||
**Форма провода наследуется от каталога, и это надо решить один раз.** Сегодня
|
||
типы `internal/catalog` сами несут json-теги, а транспорт владеет только
|
||
обёрткой: переименование поля в домене меняет публичный контракт без касания
|
||
`httpapi`. Держит это один байтовый тест непустого ответа. Либо объявить в
|
||
`architecture.md`, что типы чтения и есть форма провода для всех транспортов
|
||
(HTTP и MCP отдают её байт в байт), либо завести DTO в транспорте — но выбрать до
|
||
того, как образец скопирует эта задача.
|
||
|
||
**Клиент обязан смотреть на границы окна измерения.** Род метрики измерен по
|
||
48 самым свежим ОБЩИМ часам, а не по последним 48 часам календаря: если минутная
|
||
автоматизация HAE выключена, множество общих часов не пополняется и окно
|
||
замирает. Род при этом продолжает объявляться, и единственный след — `last_hour`
|
||
в ответе. Правило выбора свёртки в Read API обязано это учитывать (или явно
|
||
объявить, что не учитывает).
|
||
|
||
Связано: `docs/architecture.md` → «Read API», «Измерение рода агрегации»,
|
||
план → шаг «Read API».
|
||
|