Files
healthlog/docs/adr/ADR-2026-08-04-tie-break-po-poryadku-zhurnala.md
T
av b278501a6e store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной
  доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил
  меньшее значение, из-за чего step_count терял род и verify:archive был красным
- правило перестало быть коммутативным осознанно, поэтому порядок свёртки
  приведён к журнальному: проход воркера прекращается на отложенной доставке,
  а свёртка вне порядка журнала пишет WARN
- заведены счётчики PointsHeld и PointsErased — удержание полнотой и
  единственное направление, в котором правило теряет содержание
2026-08-04 11:16:24 +03:00

7.6 KiB
Raw Blame History

Тай-брейк точек — порядок журнала, а не хранимая метка

  • Дата: 2026-08-04
  • Источник: openspec/changes/archive/2026-08-04-tie-break-equal-completeness/design.md

Решение

При равной полноте побеждает точка, пришедшая разбираемой доставкой. Правило слияния точек тем самым перестаёт быть функцией множества и становится явной функцией порядка журнала; за это платится приведением порядка живой свёртки к журнальному. Хранимая метка провенанса у точки — очевидный ответ на тот же вопрос — отвергнута по цене.

Почему

Байтовый порядок канонических форм, стоявший тай-брейком прежде, оказался не крайним разрядом правила, а главным: перемер на живом корпусе дал 80 129 спорных координат, из которых полнота отбрасывает кого-то лишь в 981 (1,2%), а 79 148 (98,8%) решает тай-брейк. И решает измеримо неверно — берёт меньшее значение в 1 847 случаях из 1 912, то есть системно хранит версию, которую источник уже пересчитал. Ценой этого час 2026-08-03T07:00Z метрики step_count остался недосчитанным, сверка слоёв объявила метрику мгновенной против 23 согласных часов, и род ушёл в unknown.

Готовое решение известно и рассмотрено первым. Цитата из источника:

Регистр «последняя запись побеждает» (LWW-Register, Shapiro et al., «A comprehensive study of Convergent and Commutative Replicated Data Types», INRIA RR-7506) сходится только потому, что метка времени хранится вместе со значением: слияние сравнивает две метки, а не «кто пришёл вторым». Без хранимой метки то же правило вырождается в last-writer-wins по порядку применения — а он у реплик разный, и сходимости нет. Ровно это и означает «не полурешётка».

Взять готовое целиком нельзя: хранимая метка — это колонка провенанса на точку, то есть смена формата payload и миграция, которые постановка запрещает. Отвергнуто с названной причиной, и причина не «нам не подходит», а «цена выше разрешённой рамки».

Что взято вместо метки — вывод той же литературы о плате за отказ от неё:

Если состояние не решётка, сходимость обеспечивается единственным детерминированным порядком применения операций — это уже не CRDT, а конвейер репликации с журналом (state machine replication: Schneider, «Implementing fault-tolerant services using the state machine approach», и то же в Raft/Kafka log-compaction). Требование там одно и оно жёсткое: все потребители применяют журнал в одном порядке.

Внутренний прецедент сильнее внешнего и решён иначе: слияние сущностей ту же развилку прошло и выбрало хранимую позицию журнала (received_at, id), прямо отвергнув «побеждает приехавшая». Разница не в намерении, а в том, что у сущности колонка провенанса есть, а у точки нет. Критерий выбора между двумя механизмами записан в docs/architecture.md, раздел «Разрешение столкновений».

Значение точки и род метрики в правило не входят намеренно: «брать бо́льшее» неверно для мгновенных метрик, которые источник досчитывает вниз, а род есть функция витрины — правило, читающее собственную выдачу, перестаёт быть функцией префикса журнала (тот же дефект уже ловили на наследовании слоя «из будущего»).

Последствия

  • + step_count вернул род (cumulative, ноль противоречащих часов), заодно вернулся headphone_audio_exposure (instant); общий станок task verify:archive из красного стал зелёным.
  • + Систематический недосчёт на 75 494 координатах прекращён (95% из них — basal_energy_burned слоя raw).
  • Правило больше не коммутативно: содержимое витрины стало функцией порядка свёртки. Живой порядок приведён к журнальному барьером — проход воркера прекращается на первой отложенной занятостью доставке, — но голова очереди теперь блокирует хвост.
  • Остаточное окно конкурентного приёма (строка учёта видна позже метки) закрыть без изменения приёма нельзя; оно сделано наблюдаемым (WARN) и оставлено вопросом владельца в docs/tasks/items/journal-order-on-ingest.md.
  • Появилось направление, в котором правило теряет содержание: разряд полноты гаснет при разошедшихся значениях общих ключей, и пришедшая точка может унести ключ сохранённой. Замерено — 2 координаты из 80 129 спорных; вместо запрета заведён счётчик и WARN, тем же решением и по той же причине, по какой отложено объединение полей.
  • Восстановление коммутативности «для чистоты» молча откатит починку. Поэтому запрет записан нормативно в спеке хранения, а формулировки во всех документах приведены к «функция множества и позиции в журнале».
  • Живая витрина в ./data расходится с новым правилом до пересборки: подмена файла базы — необратимое действие человека и этим изменением не выполняется.