Files
av 37413bb551 change razbor-metrik-v-obekty заархивирован
Дельты влиты в openspec/specs (parsing, storage), задача убрана из беклога,
план отражает сделанную часть шага 3.

Не закрыт один пункт: живая доставка с телефона не разобрана — поток молчит
с 17:13, пауза началась до перезапуска сервиса.
2026-08-01 19:03:46 +03:00

14 KiB
Raw Permalink Blame History

1. Фикстуры и схема

  • 1.1 Скрипт tmp/research/fixtures.py: собирает фикстуры из data/raw, вычищая измеренные значения и сохраняя порядок ключей, точность чисел, неразрывные пробелы и форматы времени
  • 1.2 Набор internal/hae/testdata: минутная доставка, посекундная, часовая, смешанная (перенастройка автоматизации), обе схемы sleep_analysis, точка с heartbeatSeries, доставка без плотных метрик с заголовком Default
  • 1.3a Фикстура с эпизодами сна: три точки с одной меткой date и разными парами start/end, включая эпизод, пересекающий границу часа (в архиве такие есть — вычистить значения, интервалы сохранить)
  • 1.3 Рукотворная фикстура с выдуманным полем точки — скрипт из архива её не породит, а без неё требование «незнакомое поле сохраняется» останется без теста
  • 1.4 Миграция internal/store/migrations/00003_bucket.sql: таблица bucket (metric, layer, hour_utc, units, payload BLOB, content_hash, points, first_ts, last_ts, first_delivery_id, sealed, created_at, updated_at), уникальность по (metric, layer, hour_utc)
  • 1.5 docs/database.md — ER-схема с delivery и bucket (шаг гейта er-schema требует её при изменении миграций)

2. Канонизация — общий дом

  • 2.1 Пакет internal/canon: каноническая форма (числа читаются литералом через json.Number, округление до 12 значащих цифр — именованная константа со ссылкой на находку 30), полнота точки, хеш
  • 2.2 Сортировку ключей не писать — её делает encoding/json
  • 2.3 Тест канонизации на настоящих парах чисел из находки 30: все три схлопываются
  • 2.4 Тест: значение с 1.0, целым больше 2^53 и невалидным UTF-8 переживает хранение дословно

3. Разбор — пакет internal/hae

  • 3.1 Декодирование конверта в структуру с Data []json.RawMessage; точка декодируется по одной. Тест удержания кучи на теле в десятки МиБ
  • 3.2 Разбор метки 2006-01-02 15:04:05 -0700 в UTC с сохранением офсета; неразобранная метка — пропуск точки со счётчиком, не паника
  • 3.3 Эпоха внутри heartbeatSeries не разбирается: элементы проходят исходными байтами
  • 3.4 Вывод слоя: плотная метрика (≥10 точек) сама, редкая наследует преобладающий, доставка без плотных — наследует последний слой автоматизации по automation-id
  • 3.5 Наследовать нечего и заголовок Default — точки не сохраняются, WARN и счётчик
  • 3.6 WARN о расхождении слоя — только против Minutes/Hours; Default в сравнении не участвует
  • 3.7 Разделение sleep_analysis на поэпизодную и sleep_analysis_summary со слоем day; выполняется до вывода слоя, точки с назначенным слоем в голосовании не участвуют
  • 3.8 recover внутри hae.Parse — превращает панику разбора в ошибку пакета; паника из store наверх не перехватывается
  • 3.9 Контракт: error ненулевая только когда точек нет вовсе; частичные исходы — счётчиками в результате
  • 3.10 FuzzParse и таблица враждебных входов: усечённое тело, null вместо объекта, массив вместо data, число вместо строки даты
  • 3.11 Тесты разбора на фикстурах 1.2–1.3, включая смешанную доставку и обе схемы сна

4. Хранение — часовые объекты

  • 4.1 Модель объекта в internal/store; содержимое — исходные байты точек, gzip-BLOB, точки упорядочены по времени
  • 4.2 Слияние: координатный ключ, победа более полной точки, при равной полноте — детерминированный исход по порядку канонических форм
  • 4.3 Столкновение с различием канонической формы — WARN без значений и счётчик перезаписей
  • 4.4 Ключ точки — метрика + слой + начало + конец одной формы для всех точек: начало из start, иначе из date; конец из end, иначе равен началу. Час объекта — по началу. Ветвления по «классу метрики» быть не должно
  • 4.4a Тест на фикстуре 1.3a: три записи с одной меткой и разными интервалами дают три точки, а не одну; повтор той же тройки следующей доставкой не задваивает; точка без end кладётся вырожденным интервалом
  • 4.5 Хеш объекта как детектор изменений: совпал — записи нет
  • 4.6 _txlock=immediate в DSN; повтор оборачивает всю доставку целиком. Путь «хеш совпал» под TxOptions{ReadOnly: true} не сделан — вынесен блокером otvet-i-svyortka вместе с остальной ценой синхронной свёртки (измерено: 52 мс на неизменившийся плотный час под write-lock)
  • 4.7 Распознавание занятости — errors.As на *sqlite.Error, коды 5 и 517, обёрнуто в store
  • 4.8 Тест конкурентной записи: N горутин × M слияний в один hour_utc, проверка суммы точек, под -race
  • 4.9 Тест идемпотентности: повторное слияние того же набора не меняет ни содержимое, ни хеш
  • 4.10 Тесты сценариев слияния: «бедная точка не стирает поля богатой», «смена source не создаёт вторую точку»

5. Сшивка с приёмом

  • 5.1 Свёртка вызывается по идентификатору доставки, тело читается из архива — один код с будущей пересборкой
  • 5.2 Работа после записи в архив — на context.WithoutCancel с собственным дедлайном: обрыв соединения не рвёт запись объектов
  • 5.3 Отказ разбора не меняет код ответа: 200, parse_status=failed, запись ERROR без значений
  • 5.4 Единственный логирующий чекпоинт на границе: счётчики метрик, точек, объектов, пропусков, перезаписей; идентификатор доставки; ни значений, ни имён устройств
  • 5.5 Тест приёма: битый JSON — 400, непонятое содержимое — 200 с parse_status=failed

6. Сходимость на реальных данных

  • 6.1 Проверка сходимости — не отдельным скриптом, а task verify:archive поверх internal/fold/replay_test.go: одна реализация вместо двух, и она гоняет ровно продакшн-путь
  • 6.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет
  • 6.3 task gate зелёный; task restart поднимает сервис
  • 6.3a Не закрыто: живая доставка с телефона не разобрана — поток молчит с 17:13 (пауза началась ЗА ЧАС до перезапуска, то есть не из-за него). Сервис поднят и здоров, миграции на живой базе применились, схема проверена чтением. Пункт закрывается первой же пришедшей доставкой

7. Приёмочные критерии (рубрика ревью дизайна)

Порождена проходом healthlog-review-rubric до чтения предложения. Каждый пункт проверяем: понятно, каким тестом его провалить.

  • 7.1 Отсутствие паники на произвольном входе — усечённый JSON, null вместо объекта, массив вместо объекта, число вместо строки даты
  • 7.2 Незнакомое поле точки переживает round-trip дословно
  • 7.3 Идемпотентность повторной записи, включая переставленный порядок ключей и точек
  • 7.4 Различие не теряется молча: перезапись и изменение запечатанного часа оставляют след
  • 7.5 Конкурентное слияние того же часа не теряет точки — под -race, с проверкой суммы
  • 7.6 Отмена посреди слияния не оставляет половинчатого состояния: объект либо прежний, либо полный
  • 7.7 Граница размера входа явная; вход в сотни мегабайт не кладёт процесс по памяти
  • 7.8 Ошибки различимы по типу, а не по тексту (errors.Is/errors.As)
  • 7.9 Частично непонятный пакет имеет явную судьбу: что сохранено, что отброшено — видно в счётчиках
  • 7.10 Время нормализовано без потери зоны
  • 7.11 Слой выводится из данных, а не из заголовка; неопределимый слой имеет явную судьбу
  • 7.12 Парсер детерминирован и чист: ни time.Now, ни генерации id; два вызова на одном входе равны
  • 7.13 (добавлено после снятия блокера) Точка-интервал не схлопывается по метке: прогон архива даёт 174 координаты сна, а не 170

8. Отработка ревью кода (профиль deep)

Триаж свёл 62 сырые находки к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки.

  • 8.1 Схема точки сна определяется по самой точке, а не по индексу в исходном массиве (подтверждено шестью проходами)
  • 8.2 Доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (8 прогонов — 7 состояний)
  • 8.3 Граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо лимита
  • 8.4 Столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта
  • 8.5 encodePayload без HTML-экранирования: &, <, > хранятся дословно
  • 8.6 Доставка из одних суточных сводок больше не отвергается целиком
  • 8.7 Выравнивание считается по местной метке: получасовые зоны уводили часовую выгрузку в minute
  • 8.8 Нечитаемый end пропускает точку со счётчиком, а не вырождает интервал
  • 8.9 Единицы не переписываются молча: сохранённое побеждает, расхождение — счётчик и WARN
  • 8.10 Счётчик считает сохранённые точки, а не присланные
  • 8.11 Все выходы Fold с ошибкой логируются; исход пишется на контексте, переживающем отмену; слой не затирается
  • 8.12 Признаки — атрибутами всегда, уровень выбирается отдельно; WARN на отброшенных всех точках
  • 8.13 Регрессионные тесты на каждое исправление; тест «значения точек не попадают в лог» с перехватывающим handler
  • 8.14 Развилки вынесены блокерами: otvet-i-svyortka, pravilo-sliyaniya-tochek, edinicy-metriki-v-razreze, nerazobrannye-sekcii-dostavki