store: при равной полноте точек побеждает пришедшая доставка

- байтовый порядок канонических форм остался тай-брейком только внутри одной
  доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил
  меньшее значение, из-за чего step_count терял род и verify:archive был красным
- правило перестало быть коммутативным осознанно, поэтому порядок свёртки
  приведён к журнальному: проход воркера прекращается на отложенной доставке,
  а свёртка вне порядка журнала пишет WARN
- заведены счётчики PointsHeld и PointsErased — удержание полнотой и
  единственное направление, в котором правило теряет содержание
This commit is contained in:
av
2026-08-04 11:16:24 +03:00
parent ae607f1ceb
commit b278501a6e
44 changed files with 3780 additions and 172 deletions
+55 -23
View File
@@ -33,10 +33,11 @@ healthlog принимает выгрузки Apple Health из приложен
(`метрика + слой + начало + конец`; у точки-измерения конец равен началу);
`source` в ключ не входит, он
нестабилен. Хеш канонизированного содержимого остаётся детектором изменений,
чтобы не писать зря. При столкновении выигрывает более полная точка, а не
последняя пришедшая: бедная доставка не должна стирать поля у богатой.
Полнота — **множество** ключей с непустым значением, а не их число (см.
«Разрешение столкновений»).
чтобы не писать зря. При столкновении выигрывает более полная точка, а при
равной полноте — стоящая **позже в журнале**: бедная доставка не должна
стирать поля у богатой, но и устаревшее значение не должно пережить свой
досчёт. Полнота — **множество** ключей с непустым значением, а не их число
(см. «Разрешение столкновений»).
- **Дыры закрываются сами.** Данные приходят несколькими проходами разной
глубины, поэтому пропущенная доставка не оставляет постоянного пробела —
см. «Модель синхронизации».
@@ -203,10 +204,11 @@ HRV); у накопительных — только `date`. Поэтому то
доставку. Вместо этого редкий широкий проход **только по ручным секциям**: их
единицы записей, и месячное окно там почти ничего не стоит.
Правило слияния одинаково для всех проходов, и порядок прихода значения не
имеет. Но «последние данные всегда актуализируют картину» — неверно и никогда
не было верным: при столкновении выигрывает более полная точка, а не последняя
пришедшая (см. «Разрешение столкновений»).
Правило слияния одинаково для всех проходов. Порядок прихода при этом значение
**имеет**: полнота решает первой, а при равной полноте побеждает пришедшая
позже по журналу. «Последние данные всегда актуализируют картину» остаётся
неверным ровно в одном разряде — более полная точка бедную не пропускает
(см. «Разрешение столкновений»).
Автоматизации различимы по заголовку `automation-id`; имена стоит задать,
иначе `automation-name` приходит пустым (находка 12).
@@ -417,10 +419,14 @@ capability**, и здесь стоит ссылка, а не пересказ т
пересобрать что угодно.
**Свёртка обязана быть детерминированной.** Проигрывание должно давать то же
состояние, что и приём в реальном времени. Слияние «выигрывает более полная
точка» коммутативно и порядка не требует; но когда две одинаково полные точки
несут разные значения, исход решает порядок — поэтому воспроизведение идёт
строго по `received_at`, а не по порядку файлов в каталоге.
состояние, что и приём в реальном времени. Разряд полноты коммутативен и
порядка не требует, а разряд равной полноты — **нет**: побеждает пришедшая, то
есть исход есть функция порядка свёртки. Отсюда два следствия. Воспроизведение
идёт строго по `(received_at, id)`, а не по порядку файлов в каталоге. И живая
свёртка обязана идти тем же порядком: проход воркера прекращается на первой
отложенной доставке, а свёртка, всё-таки пошедшая вне порядка (конкурентный
приём делает строку учёта видимой позже метки), пишет `WARN` — закрыть это окно
можно только на приёме.
**`reindex` и `import` — одна операция, а не две.** Восстановление это импорт
снапшота плюс проигрывание хвоста; отдельной «пересборки из архива» не
@@ -928,24 +934,50 @@ hour метки выровнены на час heart_rate 00:00:00
проигрывает `{date, qty:10}` по жребию. Несравнимость на втором разряде исходом
не является: лишние ключи там заведомо пусты, объединять в них нечего.
**Победитель — функция множества точек, а не порядка их поступления.** Попарная
свёртка этого не даёт: полнота — частичный порядок, тай-брейк — тотальный, и
вместе они образуют нетранзитивное отношение победы, то есть цикл. При цикле
повторная свёртка одной и той же доставки меняет содержимое объекта, и витрина
перестаёт быть свёрткой журнала. Поэтому кандидаты координаты собираются
**Победитель — функция множества кандидатов вместе с их происхождением, а не
порядка элементов на проводе.** Попарная свёртка этого не даёт: полнота —
частичный порядок, тай-брейк — тотальный, и вместе они образуют нетранзитивное
отношение победы, то есть цикл. При цикле повторная свёртка одной и той же
доставки меняет содержимое объекта. Поэтому кандидаты координаты собираются
вместе: отбрасываются превзойдённые по полноте, среди оставшихся берётся
минимум по каноническому порядку. Обе операции зависят только от состава
множества.
минимум тотального порядка — сперва происхождение (пришедшая раньше
сохранённой), затем каноническая форма. Антицикловое свойство от этого не
страдает; зависимость от **порядка журнала** появляется намеренно и оплачена
отдельно (см. ниже).
**Несравнимые множества не сливаются, а считаются.** Объединение полей — самая
дорогая часть правила — на живом потоке не потребовалось ни разу (0 из 2 897),
поэтому вместо реализации стоит счётчик и `WARN` с координатами объекта. Если
событие наступит, оно будет видно, а не додумано заранее.
**Тай-брейк при равной полноте не выбран.** Сегодня это порядок канонических
форм, и он измеримо смещён: в 96% случаев берёт меньшее значение. Правильный
выбор зависит от рода метрики, а род измеряется сверкой слоёв между собой —
значит он и станет известен точно, вместо того чтобы быть угаданным.
**Тай-брейк при равной полноте — пришедшая доставка.** Порядок канонических
форм отвергнут замером: он берёт меньшее значение в 96% случаев (находка 49) и
стоил `step_count` его рода. Значение точки в правило не входит («брать
бо́льшее» неверно для мгновенных метрик), род метрики — тоже: род есть функция
витрины, а правило, читающее собственную выдачу, перестаёт быть функцией
префикса журнала. Байтовый порядок остался тай-брейком **внутри одной
доставки**, где провенанс общий.
Цена названа вслух: правило перестало быть функцией множества и стало явной
функцией порядка журнала. Витрина остаётся свёрткой журнала ровно потому, что
порядок свёртки приведён к порядку журнала (см. «Свёртка обязана быть
детерминированной»).
**Два правила равной полноты и когда какое.** У точки и у сущности развилка
одна, а механизмы разные — вот критерий, чтобы третья единица хранения не
открывала спор заново:
| | точка | сущность (`workout`, `record`) |
| --- | --- | --- |
| разряд полноты | множества ключей с непустым значением | покрытие содержания |
| тай-брейк равной полноты | происхождение кандидата: пришедшая побеждает | хранимая позиция журнала `(received_at, id)` |
| внутри одной доставки | порядок канонических форм | он же |
| гарантия | верна, пока порядок свёртки равен порядку журнала | верна всегда |
| в остаточном окне конкурентного приёма | расходится, пишет `WARN`, лечится `reindex` | не расходится |
| почему так | провенанса у точки нет, и заводить его дорого: колонка на точку меняет формат содержимого объекта | колонка провенанса уже есть |
Правило выбора для будущего: есть где хранить позицию журнала — храним её;
негде и завести дорого — берём происхождение и обеспечиваем порядок свёртки.
### Измерение рода агрегации