Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки. Все закрыты регрессионными тестами. - схема точки сна определяется по самой точке, а не по индексу в исходном массиве: одна пропущенная точка меняла эпизод и сводку местами - доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (восемь прогонов — семь состояний) - граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо max_body_mb - столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта; payload без HTML-экранирования - выравнивание по местной метке: получасовые зоны уводили часовую выгрузку в minute - доставка из одних суточных сводок больше не отвергается целиком - единицы не переписываются молча; счётчик считает сохранённые точки - все выходы Fold логируются, исход пишется на переживающем отмену контексте Четыре развилки вынесены блокерами в беклог.
14 KiB
14 KiB
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,payloadBLOB,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 Скрипт
tmp/research/verify_buckets.py: прогоняет архив через разбор и сверяет суммы по часовому слою с проверкой из разведки - 6.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет
- 6.3
task gateзелёный;task restartподнимает сервис, новая доставка с телефона разбирается
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