- три пропущенных прохода дозапущены, триаж оставил семь причин - обе развилки разобраны по уже записанным решениям проекта: второй разряд Covers как у Relate, мягкое чтение полей как у Duration - уточнена формулировка в architecture.md про цену покрытия секции
11 KiB
Дозакрыть находки ревью по слиянию сущностей
Приоритет: высокий
Задача «Тренировки и секции с собственными id» (f8200f7) прошла ревью не
полностью: проходы adversary, ops и архитектурный на коде не запускались.
Дозапуск принёс девять причин, триаж оставил семь. Оба заявленных critical
понижены до major с названной причиной — вход недостижим из штатного потока
HAE (корпус в 118 доставок такого не производил), — но остались в работе:
потеря маршрута необратима, а reindex проигрывает то же поражение.
Отчёт триажа с прогнанными оракулами — tmp/triage-late.md, оракулы —
tmp/adv/*.go. Каждый пункт ниже имеет падающий тест; правка считается
сделанной, когда соответствующий оракул зеленеет и переезжает из tmp/ в
обычные тесты пакета.
Что делать
1. Скелет не затирает маршрут. canon.Fields.Covers проверяет только
наличие ключа и длину верхнеуровневого массива, поэтому версия, где каждый
массив заменён массивом той же длины из null, а каждое не-массивное значение
— скаляром, признаётся равной настоящей и по тай-брейку журнала замещает её.
Оракул: -run 'Скелет|Маршрут|ДвеВерсии'.
Решение принято, новое не проектируем: применяем к сущностям тот же
стандарт, что записан для точек — второй разряд сравнения, как у
canon.Relate («иначе ключ с пустым значением исчезает по жребию»). Плюс
запрет вырождения формы: покрывающая версия не может подменить объект или
массив скаляром — проверка по верхнему уровню, стоимость O(ключей).
Поэлементная содержательность массивов отвергнута ценой: пункт 4 измерил
768 МиБ пика на канонизации, полный обход маршрута на каждое сравнение эту цену
умножит. Остающийся предел — порча внутри элемента ряда (точка маршрута без
altitude) — не закрывается ничем, кроме сверки с телом в архиве, и должен
быть записан в architecture.md рядом с описанием Covers так же прямо, как
он записан в комментарии кода.
Отдельно: pickWithinDelivery при равном содержании и разных байтах обязан
считать differs=true — сейчас две версии одного id в одном теле дают
удержано=0 и молчащий счётчик.
2. Одно поле не той формы не уносит сущность. entityHead держит
ID/Name/Date/Start/End типизированными строками, поэтому смена типа
любого из пяти роняет json.Unmarshal целиком, а доставка при этом получает
parsed с пустым списком непокрытого. Достижимо из реального потока: дрейф
формата дат у HAE задокументирован. Оракул: -run ОдноПоле.
Читать пять полей через json.RawMessage и извлекать мягко — это буквально
принцип, уже записанный в коде для Duration («нечисловое значение — это
пропуск ОДНОГО поля, а не сломанная сущность»). Плюс пропуски обязаны быть
видны в учётной записи доставки, а не только в логе: ретеншен решает по
базе, и сегодня он получит ответ «терять нечего». Хранение сущности с
неразобранной меткой (NULL) в эту задачу не входит — см. остаток ниже.
3. Откат бинаря не проходит молча. store.Open мигрирует безусловно и не
сверяет версию схемы, поэтому старый бинарь успешно стартует поверх схемы 7,
молча игнорирует незнакомые секции и помечает доставки разобранными. Оракул
прогнан живьём: -run СтарыйБинарь. Перенести в Open страж из
OpenForRead — прецедент записан там же: «расхождение версий — отказ, а не
повод мигрировать».
4. Канонизация — за транзакцию, по-настоящему. Комментарий
bucket.go:143-146 утверждает, что канонизация вынесена наружу; фактически
analyze() вызывается из compareEntities внутри inTx, который открывает
immediate и повторяет до пяти раз, а кеш analyze() пишется в копию
элемента среза и не переживает даже одной попытки. Измерено: тело 40 МиБ → пик
768.3 МиБ; 63 МиБ → блокировка удерживается 5.019 с при busy_timeout 5000, то
есть конкурентный CreateDelivery исчерпывает повторы и приём отвечает 500 по
доставке, тело которой уже на диске.
В этой задаче: вынести analyze() наружу по-настоящему, кешировать в срезе, а
не в копии, и различать в логе delivery failed занятость базы (store.ErrBusy
уже выделен доменной ошибкой) от прочих причин. Пределы на размер сущности и
потоковый расчёт хеша — остатком.
5. Значения из тела не попадают в текст ошибки. fmt.Errorf("… встречено %v", tok) подставляет токен целиком: тело 8 МиБ даёт текст ошибки 8 МиБ,
который уходит атрибутом error на уровень WARN. Инвариант «тела запросов
только на DEBUG и с обрезкой» нарушен буквально. Называть тип токена и
dec.InputOffset(). Оракул: -run Тело. Дефект в базе диффа, не внесён
разбором сущностей.
6. Победитель внутри доставки — функция множества, а не порядка.
Попарная свёртка частичного порядка с тотальным тай-брейком нетранзитивна:
[A,B,C] даёт C, [B,C,A] даёт A. Стандарт «победитель — функция множества
точек, а не порядка» записан в architecture.md для точек и для сущностей
молча не применён. Собрать версии ключа, отбросить строго покрытые, среди
оставшихся взять минимум канонической формы. Оракул: -run ПорядокВнутри.
7. Провенанс обновляется при равных хешах. Совпал хеш — запись
пропускается вместе с провенансом, и в delivery_id/delivery_received_at
остаётся первая свёрнутая копия, а не победитель по журналу. Провенанс
устаревает на каждой из ~26 повторных присылок гарантированно; расхождение
живой витрины с reindex латентно (требует возврата содержимого к прежнему —
корпус такого не производил), но нарушает записанный инвариант детерминизма.
Сравнивать позиции в журнале и обновлять провенанс. Оракул: -run Порядок.
Что уходит остатком
- хранение сущности с
id, но неразобранной меткой (NULL-метка): требует схемы и правил чтения, а после пункта 2 случай становится редким; - пределы на размер одной сущности и суммарный размер секции, потоковый расчёт канонической формы и хеша — заводится задачей вместе с условием из пункта 4;
- принцип «data-миграции не отбирают строки по спискам, которые где-то
обрезаются» (миграция
00007отбирает по обрезаемому на 32uncovered_sections; для неё дефект пустой — HAE шлёт одну секцию за доставку, — но следующая покрытая секция унаследует слепую зону); - длина очереди
pendingв/statsи без WARN: после миграции, переводящей доставки вpending, отставание по конструкции не WARN-ится (startupDone), и бэклог идёт молча при зелёном/healthz— строка уходит в наблюдаемость.
Кандидаты в конвенции
- текст ошибки разбора не содержит значений из тела — только тип токена и смещение;
- тест перестановок правила слияния обязан включать версию с содержимым, равным одной из уже присланных: тест трёх версий с разными хешами ветку равенства не посещает ни разу.
Готово, когда все семь оракулов зелены, живут обычными тестами пакетов, а
task verify:archive сходится.
Связано: internal/canon, internal/store/entity.go, internal/hae/entity.go,
docs/review-journal.md (пропуск проходов на чекпоинте — отклонение процесса,
ему там место).