- три пропущенных прохода дозапущены, триаж оставил семь причин - обе развилки разобраны по уже записанным решениям проекта: второй разряд Covers как у Relate, мягкое чтение полей как у Duration - уточнена формулировка в architecture.md про цену покрытия секции
126 lines
11 KiB
Markdown
126 lines
11 KiB
Markdown
# Дозакрыть находки ревью по слиянию сущностей
|
||
|
||
**Приоритет:** высокий
|
||
|
||
Задача «Тренировки и секции с собственными 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` отбирает по обрезаемому на 32
|
||
`uncovered_sections`; для неё дефект пустой — HAE шлёт одну секцию за
|
||
доставку, — но следующая покрытая секция унаследует слепую зону);
|
||
- длина очереди `pending` в `/stats` **и без WARN**: после миграции, переводящей
|
||
доставки в `pending`, отставание по конструкции не WARN-ится (`startupDone`),
|
||
и бэклог идёт молча при зелёном `/healthz` — строка уходит в
|
||
[наблюдаемость](stats-nablyudaemost.md).
|
||
|
||
## Кандидаты в конвенции
|
||
|
||
- текст ошибки разбора не содержит значений из тела — только тип токена и
|
||
смещение;
|
||
- тест перестановок правила слияния обязан включать версию с содержимым, равным
|
||
одной из уже присланных: тест трёх версий с разными хешами ветку равенства не
|
||
посещает ни разу.
|
||
|
||
Готово, когда все семь оракулов зелены, живут обычными тестами пакетов, а
|
||
`task verify:archive` сходится.
|
||
|
||
Связано: `internal/canon`, `internal/store/entity.go`, `internal/hae/entity.go`,
|
||
`docs/review-journal.md` (пропуск проходов на чекпоинте — отклонение процесса,
|
||
ему там место).
|