store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Тай-брейк точек — порядок журнала, а не хранимая метка
|
||||
|
||||
- **Дата:** 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` расходится с новым правилом до пересборки:
|
||||
подмена файла базы — необратимое действие человека и этим изменением не
|
||||
выполняется.
|
||||
@@ -33,6 +33,10 @@
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
|
||||
- [ADR-2026-08-04-tie-break-po-poryadku-zhurnala](ADR-2026-08-04-tie-break-po-poryadku-zhurnala.md)
|
||||
— тай-брейк точек при равной полноте: побеждает пришедшая, то есть правило
|
||||
становится явной функцией порядка журнала; хранимая метка провенанса
|
||||
(LWW-Register) отвергнута по цене формата и миграции.
|
||||
- [ADR-2026-08-03-kod-ryadom-so-strokoj-reestrom](ADR-2026-08-03-kod-ryadom-so-strokoj-reestrom.md)
|
||||
— код HealthKit кладётся реестром рядом со строкой, а не полем внутри точки;
|
||||
словарь живёт в бинаре, выведенный код в отпечаток витрины не входит.
|
||||
|
||||
Reference in New Issue
Block a user