Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
@@ -19,9 +19,9 @@
|
||||
|
||||
## блокеры
|
||||
- [Порядок журнала при конкурентных приёмах](poryadok-zhurnala-na-priyome.md) — доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда — живое состояние расходится с reindex
|
||||
- [Чем откатывать релиз после наката миграции](otkat-reliza-posle-migracii.md) — Страж версии схемы делает возврат бинаря отказом, а понизить схему нечем — аварийный путь придётся изобретать при остановленном приёме
|
||||
|
||||
## высокий
|
||||
- [Дозакрыть находки ревью по слиянию сущностей](dozakryt-nahodki-sushchnostej.md) — Скелет из null затирает маршрут необратимо, а откат бинаря поверх новой схемы проходит молча: семь находок с прогнанными оракулами
|
||||
- [Измеренный род агрегации и каталог разрезов](rod-agregacii-i-katalog.md) — Без рода метрики свёртка в ответе неотличима от угадывания — а суммировать нижний слой значит завысить втрое
|
||||
- [Read API: точки, выбор слоя, свёртка по сетке](read-api-tochki.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
||||
- [OpenAPI-спека и Swagger UI](openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
|
||||
@@ -45,6 +45,8 @@
|
||||
- [Заголовки доставки в архиве рядом с телом](zagolovki-dostavki-v-arhive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
|
||||
- [Предел на размер и число заголовков доставки](predel-na-zagolovki-dostavki.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
|
||||
- [Сверка живой витрины с пересборкой](sverka-vitriny-s-peresborkoj.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
|
||||
- [Сущность с id, но неразобранной меткой](hranenie-sushchnosti-bez-metki.md) — Тренировка с меткой в неизвестном формате пропадает целиком — а её id и содержимое разобраны
|
||||
- [Пределы на размер сущности и потоковый расчёт формы](predely-razmera-sushchnosti.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
|
||||
|
||||
## низкий
|
||||
- [Устаревание нижнего слоя после экспорта](ustarevanie-nizhnego-sloya.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
|
||||
@@ -58,4 +60,5 @@
|
||||
- [[idea] Выгрузка в parquet отдельной командой](vygruzka-v-parquet.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
|
||||
- [[idea] NDJSON-поток для больших выборок Read API](ndjson-potok.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
|
||||
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](razvorachivanie-marshrutov.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
|
||||
- [Data-миграции не отбирают строки по обрезаемым спискам](otbor-strok-data-migraciyami.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
|
||||
|
||||
|
||||
@@ -15,3 +15,15 @@
|
||||
Приоритет низкий, пока сервис на рабочей машине и я вижу его каждый день.
|
||||
После деплоя на rivendell поднимется.
|
||||
|
||||
|
||||
## Источник алерта не может жить внутри `serve`
|
||||
|
||||
Отказ стража версии схемы (база новее бинаря) останавливает процесс, а
|
||||
`restart: unless-stopped` даёт цикл перезапуска. Значит алерт «данных нет N
|
||||
часов», живущий внутри сервиса, на эту причину остановки не сработает **по
|
||||
построению** — он не поднимется вместе с ним. Обоснование стража («откат делает
|
||||
оператор, он в этот момент рядом») верно для ручного отката и не покрывает
|
||||
перезапуск хоста или откат деплоя.
|
||||
|
||||
Пришло из задачи «Дозакрыть находки ревью по слиянию сущностей» (проход `ops`,
|
||||
`negative`).
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Дозакрыть находки ревью по слиянию сущностей
|
||||
|
||||
**Приоритет:** высокий
|
||||
|
||||
Задача «Тренировки и секции с собственными 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` (пропуск проходов на чекпоинте — отклонение процесса,
|
||||
ему там место).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Сущность с id, но неразобранной меткой
|
||||
|
||||
**Приоритет:** средний
|
||||
|
||||
Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change
|
||||
`dozakryt-nahodki-sushchnostej`). Та задача сделала мягким чтение заголовка:
|
||||
поле не той формы стоит одного поля, а не сущности. Но метка исключение —
|
||||
разбор кладёт сущность в `ts_utc`/`start_utc`, колонки `NOT NULL`, и сущность
|
||||
с неразбираемой меткой по-прежнему пропускается целиком.
|
||||
|
||||
## Что известно
|
||||
|
||||
- Оракул: `internal/hae/entity_test.go`, случаи «метка в ином формате», «метка
|
||||
Unix-эпохой», «метки нет вовсе» — сущность в результат разбора не попадает,
|
||||
счётчик `SkippedEntityNoTime` растёт.
|
||||
- После той задачи пропуск виден в базе: у доставки есть `skipped_entities`,
|
||||
и ретеншен получает честный ответ «терять есть что». То есть событие больше
|
||||
не молчит — но содержимое всё ещё не хранится.
|
||||
- Достижимость из реального потока: замер на 118 доставках дал **ноль**
|
||||
пропусков всех трёх классов. Дрейф формата дат у HAE при этом
|
||||
задокументирован (`docs/local-research.md`), то есть вход не выдуман.
|
||||
|
||||
## Что решить
|
||||
|
||||
Хранить ли сущность с разобранным `id` и неразобранной меткой. Цена:
|
||||
|
||||
1. **Хранить с NULL-меткой** — правка схемы (`start_utc`/`ts_utc` становятся
|
||||
NULLABLE) плюс правила чтения витрины: выборка «за период» обязана сказать,
|
||||
что делает с такими строками, иначе они молча исчезнут из любого ответа.
|
||||
Зато содержимое (маршрут!) сохраняется, а метку восстановит пересборка,
|
||||
когда разбор научится читать формат.
|
||||
2. **Не хранить** — как сейчас. Тело живёт в архиве до ретеншена, доставку
|
||||
вернёт `reindex`. После включения ретеншена окно становится необратимым.
|
||||
3. **Хранить, подставив метку доставки** — отвергается сразу: это выдуманное
|
||||
измерение в колонке, по которой идёт выборка.
|
||||
|
||||
Рекомендация — (1), но не раньше, чем появится Read API по сущностям: правило
|
||||
чтения без читателя проектируется вслепую.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Data-миграции не отбирают строки по обрезаемым спискам
|
||||
|
||||
**Приоритет:** низкий
|
||||
|
||||
Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change
|
||||
`dozakryt-nahodki-sushchnostej`).
|
||||
|
||||
## Оракул: механизм доказан, дефект пока пустой
|
||||
|
||||
Миграция `00007` переводит в `pending` доставки, у которых имя ставшей покрытой
|
||||
секции стоит в `uncovered_sections`:
|
||||
|
||||
```sql
|
||||
WHERE EXISTS (SELECT 1 FROM json_each(delivery.uncovered_sections)
|
||||
WHERE json_each.value IN ('workouts', 'stateOfMind'))
|
||||
```
|
||||
|
||||
Список `uncovered_sections` обрезается на 32 имени **в порядке встречи**
|
||||
(`hae.maxUncovered`, счётчик `UncoveredDropped`). Секция, стоящая в теле после
|
||||
тридцати двух незнакомых ключей, в список не попадает — и отбор миграции её не
|
||||
найдёт. Оракул жил в `tmp/adv/uncovered_test.go`: тело с 32 ключами `junk` и
|
||||
секцией `ecg` за ними даёт список без `ecg`.
|
||||
|
||||
Для `00007` дефект **пустой**: HAE шлёт одну секцию за доставку
|
||||
(`docs/local-research.md`, находка 50), секций восемь, тела с 32 незнакомыми
|
||||
ключами в архиве не существует. Но следующая покрытая секция унаследует ту же
|
||||
слепую зону, а к тому времени причину никто не вспомнит.
|
||||
|
||||
## Что делать
|
||||
|
||||
Записать принцип и выбрать форму отбора:
|
||||
|
||||
- **Принцип:** data-миграция не отбирает строки по списку, который где-то
|
||||
обрезается. Отбирать надо по признаку, который обрезке не подлежит, —
|
||||
например «эту доставку смотрел разбор старше версии N».
|
||||
- Практическое следствие для существующего кода: `UncoveredDropped > 0` обязан
|
||||
означать безусловное пересворачивание — доставка, у которой список обрезан,
|
||||
про своё покрытие ничего достоверного не говорит.
|
||||
- Кандидат в `docs/conventions.md` (раздел про миграции), если форма отбора
|
||||
окажется общей.
|
||||
|
||||
## Связано
|
||||
|
||||
- [Проверка секций, которых поток ещё не приносил](proverka-novyh-sekcij.md) —
|
||||
именно она следующей сделает секцию покрытой и напишет такую миграцию.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Чем откатывать релиз после наката миграции
|
||||
|
||||
**Приоритет:** блокеры
|
||||
|
||||
Вынуто ревью кода задачи «Дозакрыть находки ревью по слиянию сущностей»
|
||||
(проходы `ops` и `negative`, профиль `deep`).
|
||||
|
||||
## Что именно решить
|
||||
|
||||
Та задача перенесла в `store.Open` стража версии схемы: база новее бинаря —
|
||||
отказ на старте. Решение принято владельцем и здесь не пересматривается. Но у
|
||||
него есть следствие, которое до сих пор нигде не было записано:
|
||||
|
||||
**после того как новый бинарь накатил миграцию, возврат старого бинаря приёма
|
||||
не чинит.** Он теперь отказывается стартовать, а понизить схему нечем:
|
||||
|
||||
- подкоманды миграции у бинаря нет (`serve`, `reindex`, `healthcheck`);
|
||||
- `goose` CLI в образ не кладётся;
|
||||
- блоки `-- +goose Down` в миграциях написаны, но ни один тест их не исполняет,
|
||||
и на рабочей базе они не выполнялись ни разу (`DROP COLUMN` в SQLite через
|
||||
`modernc.org/sqlite` не проверялся вовсе);
|
||||
- `restart: unless-stopped` превращает отказ в цикл перезапуска, а телефон всё
|
||||
это время шлёт в закрытый порт и **не перешлёт** потом.
|
||||
|
||||
То есть аварийный путь придётся изобретать в момент аварии, при остановленном
|
||||
приёме. Цена простоя для метрик закрывается широким и глубоким проходами
|
||||
синхронизации; для `stateOfMind` не закрывается ничем — у него доставки HAE
|
||||
единственный источник.
|
||||
|
||||
## Варианты и цена
|
||||
|
||||
1. **Подкоманда `healthlog migrate --down-to N`.** Цена: новая поверхность CLI
|
||||
плюс тест на `Down` каждой миграции (сейчас их нет, и `DROP COLUMN` в SQLite
|
||||
ведёт себя не так, как в постгресе). Зато откат становится операцией, а не
|
||||
импровизацией.
|
||||
2. **Копия файла базы перед накатом** — entrypoint контейнера делает `cp` рядом,
|
||||
откат = подмена файла. Цена: место (база растёт), плюс правило «сколько копий
|
||||
держим». Зато не требует ни кода, ни доверия к `Down`, а база производна от
|
||||
архива — потеря копии не смертельна.
|
||||
3. **`goose` CLI в образ.** Цена: образ перестаёт быть одним статическим
|
||||
бинарём, появляется вторая точка, знающая про схему.
|
||||
4. **Ничего, но записать вслух**: «понижение схемы не поддерживается, лечение —
|
||||
только выкатка вперёд». Цена: в аварии выбора нет.
|
||||
|
||||
## Рекомендация
|
||||
|
||||
(2) плюс уже сделанная запись из (4). Копия файла — единственный вариант,
|
||||
который не требует доверять непроверенному коду ровно в тот момент, когда
|
||||
проверять некогда; а `Down`-блоки при этом честно называются декорацией для
|
||||
локальной разработки.
|
||||
|
||||
## Что стоит, пока решения нет
|
||||
|
||||
Ничего: страж работает, и это правильно. Стоит только аварийный сценарий —
|
||||
он существует ровно в том виде, в каком описан выше. Строка «понижение схемы не
|
||||
поддерживается» уже записана в `docs/architecture.md` (раздел «Деплой»).
|
||||
@@ -0,0 +1,70 @@
|
||||
# Пределы на размер сущности и потоковый расчёт формы
|
||||
|
||||
**Приоритет:** средний
|
||||
|
||||
Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change
|
||||
`dozakryt-nahodki-sushchnostej`). Та задача убрала канонизацию приехавшей
|
||||
сущности из транзакции и перестала считать каноническую форму дважды. Осталось
|
||||
структурное: **предела на размер одной сущности нет вовсе**, а форма и хеш
|
||||
считаются материализацией значения целиком.
|
||||
|
||||
## Оракул: измерено
|
||||
|
||||
Оракулы жили в `tmp/adv/mem_test.go` и `tmp/adv/lock_test.go`; числа снимались
|
||||
на теле в пределах приёма (64 МиБ):
|
||||
|
||||
```
|
||||
тело 40 МиБ → пик HeapAlloc 768.3 МиБ
|
||||
тело 63 МиБ → повторная доставка держит блокировку 5.019 с при busy_timeout 5000
|
||||
```
|
||||
|
||||
При `_txlock=immediate` конкурентный `CreateDelivery` получает `SQLITE_BUSY`,
|
||||
`inTx` повторяет до пяти раз и на исчерпании отдаёт `store.ErrBusy` — приём
|
||||
отвечает 500 по доставке, тело которой уже в архиве. Осиротевшее тело подберёт
|
||||
`reindex`, но узнать о нём можно только из лога.
|
||||
|
||||
## Что делать
|
||||
|
||||
1. Предел на размер **одной сущности** и на суммарный размер секции, отдельно
|
||||
от предела тела (64 МиБ). Сегодня одна тренировка законно может занять всё
|
||||
тело целиком. Вход, превышающий предел, обязан отклоняться **до**
|
||||
канонизации, а не после.
|
||||
2. Потоковый расчёт канонической формы и хеша: `canon.Form` разворачивает
|
||||
значение в дерево `any`, из-за чего пик кучи кратен размеру входа (замер даёт
|
||||
множитель около 19×). Хеш считается по потоку; форма нужна целиком только для
|
||||
сравнения, и только когда хеш разошёлся.
|
||||
3. Разбор **сохранённой** версии всё ещё идёт внутри транзакции: её содержимое
|
||||
читается оттуда же. Убрать это можно оптимистичным чтением до транзакции —
|
||||
но только с перепроверкой хеша и провенанса **внутри** транзакции, иначе две
|
||||
конкурентные свёртки одного `id` дадут потерянное обновление и исход снова
|
||||
станет функцией порядка коммитов, а не журнала.
|
||||
|
||||
## Условия, пришедшие из закрывающей задачи
|
||||
|
||||
1. Мягкое чтение заголовка сущности увеличило долю тел, доходящих до
|
||||
канонизации: сущность, которая раньше отсекалась на `json.Unmarshal`
|
||||
заголовка почти бесплатно, теперь разбирается и канонизируется целиком. То
|
||||
есть худший случай по памяти стал достижим на входах, которые до него не
|
||||
доходили, — предел из пункта 1 после этого **обязателен**, а не желателен.
|
||||
|
||||
2. Каноническая форма и множества ключей всех версий доставки теперь
|
||||
**удерживаются** до конца транзакции слияния (раньше считались лениво и на
|
||||
одной доставке из сорока четырёх). Расход стал пропорционален размеру
|
||||
ДОСТАВКИ, а не самой большой её сущности; предел обязан считать суммарный
|
||||
размер секции, а не только одной сущности.
|
||||
|
||||
3. **Потолок на число версий одного ключа в одной доставке.** Выбор победителя
|
||||
квадратичен по числу кандидатов; версии с совпавшей канонической формой
|
||||
схлопываются, но различных тело вмещает сколько угодно. Отмена цикл
|
||||
прерывает (дедлайн свёртки снова работает), но доставка при этом уходит в
|
||||
`failed` — то есть отравленное тело стоит полного дедлайна воркера. Тот же
|
||||
вопрос открыт для точек на одной координате: `cena-sliyaniya-na-shirokoj-dostavke.md`,
|
||||
пункт 4.
|
||||
|
||||
## Связано
|
||||
|
||||
- [Цена слияния на широкой доставке](cena-sliyaniya-na-shirokoj-dostavke.md) —
|
||||
та же плата со стороны **точек** (`hashPoints` пересчитывает форму всех точек
|
||||
часа). Задачи делать вместе: половина решения общая — `canon`.
|
||||
- Из того же ревью: «хеш без полного прохода по содержимому не посчитать» —
|
||||
отброшено как предел по конструкции, но условием ложится сюда.
|
||||
@@ -61,3 +61,20 @@ Change `2026-08-02-trenirovki-i-zapisi` покрыл `stateOfMind` разбор
|
||||
же изменением переводит `partial`-строки с этим ключом в `pending` (так сделала
|
||||
миграция `00007`). Ретеншену позволено смотреть на `partial` только пока правило
|
||||
соблюдается.
|
||||
|
||||
## Что читать перед удалением тела
|
||||
|
||||
Две колонки учётной записи, и обе обязательны:
|
||||
|
||||
- `uncovered_sections` — непустой список означает, что в теле есть секции,
|
||||
которых разбор не покрывает; удалять нельзя;
|
||||
- `skipped_entities` — число сущностей с собственным `id`, которые разбор не
|
||||
понял. **`NULL` означает «не измерялось» и нулю не равен**: так выглядят
|
||||
доставки, свёрнутые разбором, который пропусков не считал, и те, чей разбор не
|
||||
досчитал. `NULL` — «не удалять». Прочитать его как ноль значит удалить тело
|
||||
тренировки, маршрута которой нет больше нигде: в экспорте Apple его не
|
||||
существует.
|
||||
|
||||
Правило пришло из задачи «Дозакрыть находки ревью по слиянию сущностей»
|
||||
(миграция `00008`), где колонка и заведена — без `DEFAULT` именно ради этого
|
||||
различия.
|
||||
|
||||
@@ -22,5 +22,13 @@
|
||||
Пришло из задачи «Разнести ответ приёма и свёртку доставки»: там числа
|
||||
намеренно не заводились, чтобы не предрешать форму счётчиков этой задачи.
|
||||
|
||||
Длина очереди обязана быть видна **и без `WARN`**. После миграции, переводящей
|
||||
доставки в `pending`, весь исторический бэклог встаёт в очередь перед свежими
|
||||
доставками, а `warnLag` на это время намеренно подавлен (`startupDone`) — то
|
||||
есть отставание по конструкции не WARN-ится ровно тогда, когда оно максимально,
|
||||
и бэклог идёт молча при зелёном `/healthz`. Пришло из дозакрытия находок ревью
|
||||
по слиянию сущностей (проход `ops`, находка O1); оракула нет — он потребовал бы
|
||||
десятков тысяч доставок.
|
||||
|
||||
Активное уведомление — отдельная задача, здесь только факт.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user