Files
healthlog/docs/adr/ADR-2026-08-04-sloy-vybiraetsya-po-ohvatu-tochek.md
av 29ca8d415c httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
  объявляет слой, измеренный род, его применимость к отданному ряду и границу
  окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
  точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
  под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
  хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
  записи: дословность содержимого точки иначе не удерживается, а оборванное
  тело уходило под видом успешного `200`
2026-08-04 18:46:45 +03:00

81 lines
6.5 KiB
Markdown
Raw Permalink 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.
# Слой ответа выбирается по охвату точек внутри периода
- **Дата:** 2026-08-04
- **Источник:** openspec/changes/archive/2026-08-04-tochki-metriki-za-period/design.md
## Решение
Слой, из которого собирается ряд, выбирается так:
> Охват слоя — длина пересечения отрезка `[первая метка слоя, последняя метка
> слоя]` с запрошенным периодом. Слой с пустым пересечением выбывает. Среди
> оставшихся берётся слой с наибольшим охватом, при равенстве — самый мелкий
> (`sample` → `raw` → `minute` → `hour` → `day`).
Это **пересмотр** прежнего правила, записанного в `docs/architecture.md`: «самый
мелкий слой, покрывающий весь запрошенный диапазон».
## Почему
**Прежняя формулировка неопределена на входе, который тот же документ объявляет
законным.** Границы слоя — границы **данных**, а не обещание покрытия: внутри
диапазона законно есть дыры, и слоя, покрывающего диапазон целиком, может не
существовать вовсе. Правило, не определённое на законном входе, реализатор
доопределяет молча.
**Мера — охват, а не число точек.** `body_mass` в нижнем слое за три плотных дня
даёт больше объектов, чем часовой слой за год с еженедельным взвешиванием: по
числу точек «вес за год» вернул бы три дня, не сказав об этом ни словом.
**Охват меряется метками точек, а не часами объектов**, и это не придирка.
Объекты адресуются часом, поэтому выборка обязана быть шире запроса (точка
`10:59` живёт в объекте `10:00`), а ряд отбирается точной меткой. Путь построен
проходом ревью на предложении:
> `from = 10:30`, `to = 10:45`. Слой `hour` имеет объект `10:00` с единственной
> точкой в `10:00`, слой `minute` — объект `10:00` с точками `10:31…10:44`. По
> часам объектов охваты равны, побеждает `hour` — и после точного отбора ответ
> уходит пустым при непустых минутных данных.
Класс общий: **предикат выбора источника и предикат отбора данных обязаны
использовать одну границу**.
**Цена меры измерена, и она не нулевая.** Индекс `bucket_catalog` идёт
`(metric, layer, hour_utc, …)`, и без предиката по слою SQLite не сужает поиск по
`hour_utc` — он просматривает все строки метрики за всю историю, а план при этом
выглядит успешным (`SEARCH … USING COVERING INDEX`). Замер эксплуатационного
прохода на копии схемы: 2.06 мс при 52 560 строках метрики против 13.9 мс при
350 400, то есть цена росла бы вместе с возрастом сервиса при любой ширине
запроса. С явным перечислением слоёв — 0.026 мс. Отсюда же следствие: **словарь
слоёв один** (`hae.Layers`), из него выводятся и порядок, и перечень выборки, и
проверка параметра запроса, и текст отказа клиенту.
## Последствия
- `+` Правило определено на любом входе, включая тот, где ни один слой периода
не покрывает.
- `+` Смены слоя внутри одного ответа не бывает: ряд, склеенный из двух слоёв,
поехал бы незаметно для клиента, а вместе с ним поехала бы и будущая свёртка.
- `` Правило **максимизирует** размер ответа: при равном охвате берётся самый
мелкий слой, то есть «пульс за неделю» без параметров это сотни тысяч точек.
Предел ответа — соседняя задача; цена измерена и названа (см. ниже).
- `` Краевой объект, у которого есть точки и до, и после периода, но ни одной
внутри, свой слой из выбора не выведет. Остаток узкий и честный: слой в ответе
назван, а `points` пуст.
## Открыто, решает владелец
**Инвертировать ли умолчание при равном охвате.** Сегодня берётся самый мелкий —
это правило `architecture.md` до пересмотра, и оно максимизирует размер ответа.
Измерено на этом маршруте: неделя нижнего слоя — 604 800 точек, 1.75 с и
1375 МиБ суммарных выделений на доменном слое; под HTTP вместе с сериализацией —
2.89 с, 279.7 МиБ тела, 1335 МиБ живой кучи; четыре одновременных запроса дают
4322 МиБ.
- **(а)** оставить как есть, предел вводит `read-api-response-limit`;
- **(б)** при равном охвате брать самый **крупный** слой, мелкий — только по
явному `layer`.
**Рекомендация:** (а). Решение сцеплено с формой предела, и принимать его
мимоходом на первой ручке — то же, от чего отказались на каталоге.