Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
+167
-21
@@ -486,9 +486,16 @@ task up
|
||||
|
||||
Что пересборка **не** переносит: признак `sealed` (правила его выставления ещё
|
||||
нет, переносить нечего) и производные от разбора поля учёта — `parse_status`,
|
||||
`points`, `derived_layer`, `uncovered_sections`. Последнее не косметика:
|
||||
доставка, чей повторный разбор отказал, отдала бы в наследование слой прежнего
|
||||
разбора, и витрина снова стала бы функцией предыдущего прогона, а не журнала.
|
||||
`points`, `derived_layer`, `uncovered_sections`, `skipped_entities`. Перечень
|
||||
пополняется **тем же изменением**, которое заводит новое поле: он единственное
|
||||
место, где сказано, чему нельзя пережить пересборку.
|
||||
|
||||
Это не косметика. Доставка, чей повторный разбор отказал, отдала бы в
|
||||
наследование слой прежнего разбора, и витрина снова стала бы функцией
|
||||
предыдущего прогона, а не журнала. У числа пропущенных сущностей цена та же и
|
||||
хуже: пустота у него означает «не измерялось», и перенесённое число выдавало бы
|
||||
измерение прежнего разбора за измерение текущего — а по нему принимается
|
||||
необратимое решение об удалении тела.
|
||||
|
||||
#### Что не восстанавливается, и это сказано вслух
|
||||
|
||||
@@ -555,7 +562,7 @@ HAE. Значит для него доставки не хвост журнал
|
||||
```
|
||||
delivery(id, received_at, automation_name, automation_id, aggregation,
|
||||
period, session_id, bytes, sha256, raw_path, parse_status, points,
|
||||
headers, derived_layer)
|
||||
headers, derived_layer, uncovered_sections, skipped_entities NULL)
|
||||
|
||||
bucket(metric, layer, hour_utc, units, payload BLOB, content_hash, points,
|
||||
first_ts, last_ts, first_delivery_id, sealed, created_at, updated_at)
|
||||
@@ -858,6 +865,29 @@ data-миграции, переводящей уже принятые `partial`-
|
||||
объекта тренировки. Это ожидаемо, они лежат в разных таблицах и не
|
||||
смешиваются.
|
||||
|
||||
**Значение заголовка не того типа стоит одного поля, а не сущности.** Пять полей
|
||||
(`id`, `name`, `date`, `start`, `end`) читаются мягко: нестроковое значение
|
||||
считается неприсланным. Иначе `name`, приехавшее числом, уносит тренировку
|
||||
вместе с маршрутом, а доставка при этом числится разобранной. Мягкость сделана
|
||||
через `json.Unmarshaler`, а не через разбор ошибки типа постфактум: библиотека
|
||||
дозаполняет поля «как может», но не обязуется дозаполнить те, что стоят **после**
|
||||
проблемного, — то есть исход перестал бы быть функцией тела.
|
||||
|
||||
Исключений два, и оба названы. `id`: без строкового идентификатора сущность не
|
||||
адресуема, а приведение чужого значения к строке было бы выдумыванием
|
||||
идентичности за источник. `start`: непонятое значение не откатывается на `date` —
|
||||
подстановка другого поля дала бы метку **другого момента времени**, неотличимую
|
||||
от настоящей и ничем не считаемую.
|
||||
|
||||
**Граница правила: оно закрывает смену типа, но не смену формата строки.** А
|
||||
наблюдался именно дрейф формата дат. Тренировка с датой в незнакомом формате
|
||||
по-прежнему теряется целиком; закрыть это может только хранение сущности с
|
||||
неразобранной меткой, и это отдельная задача. Пропуск при этом перестал быть
|
||||
невидимым: число пропущенных сущностей лежит в учётной записи доставки, и
|
||||
ретеншен, решающий «что потеряется, если тело удалить», больше не получает
|
||||
ложное «терять нечего». Отсутствие значения в этой колонке означает «не
|
||||
измерялось» и нулю не равно.
|
||||
|
||||
#### Замена версии сущности
|
||||
|
||||
«Перезаписывается» уточнено измерением. Тренировка приезжает повторно каждой
|
||||
@@ -882,16 +912,67 @@ data-миграции, переводящей уже принятые `partial`-
|
||||
счётчик + WARN
|
||||
```
|
||||
|
||||
**Содержание сравнивается множеством ключей с непустым значением и длиной
|
||||
верхнеуровневых массивов — но не значениями.** Правило полноты, принятое для
|
||||
точек, здесь неприменимо, и это проверено выполненной командой: оно гасит
|
||||
отношение включения до «равенства», когда значения общих ключей разошлись, — а
|
||||
у сущности они расходятся всегда. Обеднённая версия получила бы «равенство» и
|
||||
заместила бы сохранённую вместе с маршрутом, причём тест на фикстуре с
|
||||
неизменёнными значениями остался бы зелёным. Длина массивов добавлена потому,
|
||||
что усечённый маршрут (три точки вместо 593) ключа не теряет, а теряет 95% веса
|
||||
тренировки. Предел правила назван вслух: сокращение **внутри** элемента ряда не
|
||||
ловится ничем, кроме сверки с телом в архиве.
|
||||
**Содержание сравнивается множествами ключей и формой их значений — но не
|
||||
значениями.** Правило полноты, принятое для точек, здесь неприменимо, и это
|
||||
проверено выполненной командой: оно гасит отношение включения до «равенства»,
|
||||
когда значения общих ключей разошлись, — а у сущности они расходятся всегда.
|
||||
Обеднённая версия получила бы «равенство» и заместила бы сохранённую вместе с
|
||||
маршрутом, причём тест на фикстуре с неизменёнными значениями остался бы
|
||||
зелёным.
|
||||
|
||||
Условий покрытия четыре, все по **верхнему уровню**:
|
||||
|
||||
```
|
||||
1. каждый содержательный ключ сохранённой есть у приехавшей и содержателен
|
||||
2. каждый ключ сохранённой, даже пустой, есть у приехавшей
|
||||
3. форма не вырождается: объект остаётся объектом, массив — массивом
|
||||
4. верхнеуровневый массив не теряет ни длины, ни содержательных элементов
|
||||
```
|
||||
|
||||
Условие 2 — тот же второй разряд, что у точек, и с тем же **условием**: оно
|
||||
включается только при равенстве множеств содержательных ключей. Иначе ключ с
|
||||
пустым значением исчезает по жребию тай-брейка — но и обратная крайность
|
||||
проверена оракулом и отвергнута: безусловный второй разряд запирал законный
|
||||
досчёт навсегда. Версия с `totalEnergy: null` и без маршрута оказывалась
|
||||
несравнимой с версией, у которой маршрут приехал, а этого ключа нет, — и
|
||||
маршрут не доезжал **никогда**, причём пересборка проигрывала то же поражение.
|
||||
Второй разряд разрешает спор равных, а не отменяет первый.
|
||||
|
||||
Условие 3 закрывает «скелет»: тело, где каждый вложенный объект заменён числом,
|
||||
а каждый массив — массивом той же длины из `null`, проходило все прежние
|
||||
проверки и по тай-брейку журнала замещало настоящую тренировку целиком.
|
||||
|
||||
Условие 4 добавлено потому, что усечённый маршрут (три точки вместо 593) ключа
|
||||
не теряет, а маршрут из `[null,null,null]` не теряет и длины — притом что
|
||||
маршрут это 95% веса тренировки. Содержательность элемента — **та же пустота**,
|
||||
что у поля точки; второй словарь пустоты дал бы два ответа на один вопрос. Цена
|
||||
названа вслух: ряд настоящих нулей (`[0,0,0]`) считается лишённым содержания, и
|
||||
версия с ним сохранённую не заместит. Ошибка направлена в безопасную сторону —
|
||||
правило удерживает, а не затирает, — и видна счётчиком.
|
||||
|
||||
Условия 3 и 4 применяются к ключам, содержательным у сохранённой: у пустоты
|
||||
формы нет, и требовать её сохранения значило бы отличать `[]` от `0` там, где ни
|
||||
то, ни другое ничего не несёт.
|
||||
|
||||
Поле `source` в множества не входит — ни у точки, ни у сущности. Для точки
|
||||
причина измерена (оно нестабильно и переписывается задним числом, находка 36);
|
||||
для сущности она наследуется, и это сказано вслух, потому что список исключений
|
||||
живёт в общем разборе: правка ради точек молча изменит правило удержания
|
||||
сущностей. Верхнеуровневого `source` ни у тренировки, ни у `stateOfMind` живьём
|
||||
не наблюдалось.
|
||||
|
||||
**Предел правила назван вслух и не закрывается: сокращение внутри элемента ряда
|
||||
(точка маршрута без `altitude` при непустом элементе и той же длине) не ловится
|
||||
ничем, кроме сверки с телом в архиве.** Поэлементная сверка содержимого
|
||||
отвергнута ценой: она разворачивала бы каждый элемент маршрута в дерево
|
||||
значений на каждое сравнение, а тело 40 МиБ уже даёт 768 МиБ пика.
|
||||
|
||||
**Проверить это правило отпечатком нельзя.** Живой приём и пересборка пользуются
|
||||
одним правилом и одинаково сойдутся на одинаково удержанной версии — то есть
|
||||
слишком строгое правило, замораживающее тренировку на старой версии, выглядело
|
||||
бы идеальной сходимостью. Поэтому число удержаний идёт в отчёт пересборки и
|
||||
печатается всегда, включая ноль: здесь ноль это утверждение, а не отсутствие
|
||||
новостей.
|
||||
|
||||
**Тай-брейк при равном содержании — позиция доставки в журнале
|
||||
`(received_at, id)`, а не порядок свёртки.** Напрашивавшееся «побеждает
|
||||
@@ -904,9 +985,32 @@ data-миграции, переводящей уже принятые `partial`-
|
||||
точек) отвергнут по другой причине: он заморозил бы тренировку на произвольной
|
||||
из версий навсегда, вместе с недосчитанной энергией.
|
||||
|
||||
Две версии одного ключа **внутри одной доставки** позициями не различаются и
|
||||
разрешаются минимумом канонической формы: порядок элементов в JSON-массиве
|
||||
нестабилен.
|
||||
Провенанс поднимается **и при совпавшем хеше**. Совпал хеш — содержимое то же,
|
||||
писать нечего; но сохранённая позиция журнала участвует в тай-брейке пункта 4, и
|
||||
если в ней осталась первая свёрнутая копия вместо победителя журнала, отложенная
|
||||
доставка вернёт витрину к прежнему содержимому — то есть живая витрина
|
||||
разойдётся с пересборкой молча. Обновляется только провенанс: метка изменения
|
||||
содержимого не двигается, иначе она становится меткой касания строки и дребезжит
|
||||
двадцать шесть раз на неизменившейся тренировке, а запрос «что изменилось с
|
||||
момента X» получает шум, неотличимый от настоящего досчёта.
|
||||
|
||||
Слово «провенанс» у сущности и у часового объекта значит **разное**, и это
|
||||
сказано вслух: у объекта хранится доставка, **создавшая** его, и она не
|
||||
поднимается никогда; у сущности — доставка, **чья версия лежит сейчас**, и она
|
||||
поднимается до максимума по журналу среди версий с этим содержимым. У объекта
|
||||
нет замещения версии целиком, у сущности только оно и есть.
|
||||
|
||||
Версии одного ключа **внутри одной доставки** позициями не различаются, и
|
||||
победитель среди них — **функция множества**, а не порядка элементов массива:
|
||||
отбрасываются строго покрытые (покрыта другой и сама её не покрывает —
|
||||
покрытие предпорядок, и наивное «выбросить всё покрытое» опустошило бы
|
||||
множество), среди оставшихся берётся минимум канонической формы, а при равных
|
||||
формах — минимум исходных байтов. Последний разряд не украшение: у сущностей
|
||||
версии с равной формой не схлопываются, а порядок ключей в JSON от HAE
|
||||
нестабилен — без него в витрину легли бы разные байты при одинаковом содержимом.
|
||||
Механизм тот же, что у точек, и живёт он одним помощником на обе единицы
|
||||
хранения: попарная свёртка здесь уже давала нетранзитивную победу, при которой
|
||||
`[A,B,C]` и `[B,C,A]` выбирали разных победителей.
|
||||
|
||||
Отвергнут и **голый upsert по `id`** (так делает сервер HealthyApps поверх
|
||||
MongoDB, и так просилось из слова «перезаписывается»): единственный наблюдённый
|
||||
@@ -914,10 +1018,19 @@ MongoDB, и так просилось из слова «перезаписыва
|
||||
восстановление требует пересборки всего журнала. Условие пункта 3 стоит одного
|
||||
сравнения множеств и делает событие наблюдаемым вместо необратимого.
|
||||
|
||||
Остаточный предел назван вслух: слияние попарное, поэтому при несравнимых
|
||||
наборах (пункт 5) исход зависит от порядка проигрывания. Тот же предел есть у
|
||||
часового объекта — в нём лежит победитель прошлых слияний, а не все кандидаты
|
||||
истории.
|
||||
Остаточный предел назван вслух: сравнение сохранённой с приехавшей попарно —
|
||||
в витрине лежит победитель прошлых слияний, а не все кандидаты истории, —
|
||||
поэтому при несравнимых наборах (пункт 5) исход зависит от порядка
|
||||
проигрывания. Тот же предел есть у часового объекта. **Это единственная точка,
|
||||
где витрина не является функцией множества доставок**, и потому утверждение
|
||||
«перестановка порядка свёртки даёт один отпечаток» верно ровно при нулевом
|
||||
счётчике несравнимых версий; при ненулевом расхождение законно и обязано идти
|
||||
вместе с этим счётчиком.
|
||||
|
||||
Второй разряд условия покрытия делает пункт 5 чаще, чем он был: версия, принёсшая
|
||||
новые содержательные ключи и потерявшая пустой, теперь несравнима вместо
|
||||
«полнее». Плата принята сознательно — она направлена в сторону удержания, а не
|
||||
затирания, — и её величину показывает счётчик удержаний в отчёте пересборки.
|
||||
|
||||
#### Отпечаток и отчёт пересборки идут за витриной
|
||||
|
||||
@@ -1116,6 +1229,39 @@ VPS **rivendell** (Timeweb), доступен всегда. Перед серв
|
||||
Тома: каталог сырого архива и файл SQLite — на постоянном хранении, конфиг
|
||||
(с токенами) — отдельно, `0600`.
|
||||
|
||||
**Откат бинаря поверх новой схемы отказывает на старте.** Версия схемы базы выше
|
||||
версии, вшитой в бинарь, — отказ, а не повод мигрировать; в контейнере это
|
||||
выглядит циклом перезапуска, и лечится возвратом бинаря вперёд. Версию читает
|
||||
сам goose (`Provider.GetVersions`), а не собственный запрос: имя таблицы учёта и
|
||||
правило «максимум = текущая версия» принадлежат ему, и рукописная копия
|
||||
разошлась бы при обновлении зависимости — причём не отказом, а тем, что страж
|
||||
перестал бы ловить.
|
||||
|
||||
Цена названа вслух, потому что она реальна: пока сервис не поднят, приём не
|
||||
работает, а доставка, не попавшая в архив, в журнал не попадает вовсе — телефон
|
||||
её не перешлёт. Выбор сделан так потому, что откат это действие оператора,
|
||||
который в этот момент рядом и видит отказ немедленно, а дыры плотных метрик за
|
||||
время простоя закроют широкий и глубокий проходы синхронизации. Не закроют
|
||||
`stateOfMind`: у него доставки HAE единственный источник — это и есть цена
|
||||
решения. Она меньше цены молчания: старый бинарь поверх новой схемы стартовал
|
||||
бы успешно, незнакомые секции игнорировал и доставки за всё окно отката помечал
|
||||
разобранными, а узнать об этом было бы неоткуда.
|
||||
|
||||
Открытие базы **только на чтение** (`reindex`, утилиты учёта) остаётся строгим:
|
||||
там отказ даёт любое расхождение версий, включая базу старее бинаря — читать
|
||||
колонки, которых ещё нет, нечем. База без журнала миграций отвергается сразу и
|
||||
структурным вопросом к `sqlite_master`, а не через сам goose: тот при отсутствии
|
||||
таблицы идёт её создавать, и на соединении «только чтение» это три секунды
|
||||
повторов и ответ про права на файл вместо ответа про версию. Асимметрия только у
|
||||
открытия с накатом.
|
||||
|
||||
**Понижение схемы не поддерживается: откат — только вперёд.** Подкоманды
|
||||
миграции у бинаря нет, `goose` CLI в образ не кладётся, `-- +goose Down` в
|
||||
миграциях существует для локальной разработки и на рабочей базе не исполнялся ни
|
||||
разу. Значит после наката новой схемы возврат прежнего бинаря приёма не чинит —
|
||||
чинит только выкатка вперёд. Это цена стража, названная целиком; чем её
|
||||
смягчать, решает отдельная задача беклога.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Механизм доставки образа и запуска на rivendell (compose руками / плейбук).
|
||||
|
||||
Reference in New Issue
Block a user