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
+81 -11
View File
@@ -120,19 +120,76 @@
порядке журнала — `(received_at, id)`, как он определён capability пересборки.
Распараллеливать свёртку MUST NOT.
Порядок здесь — не удобство отладки, а условие правильности содержимого:
тай-брейк при равной полноте точек разрешается в пользу пришедшей доставки, то
есть исход слияния есть функция порядка свёртки. Свёрнутая не в порядке журнала
доставка возвращает координату к версии, которую источник уже пересчитал, и
живая витрина расходится с тем, что даёт пересборка.
Проход воркера SHALL прекращаться на первой доставке, чей исход свёртки
**классифицирован как отложенный** (занятость базы, отмена снаружи), а не
перешагивать её. Перешагнув, проход свернул бы её преемниц раньше неё.
Предикат остановки — именно класс исхода, а не статус доставки. Доставка,
оставшаяся `pending` из-за отказа записи самого исхода, курсор двигает и проход
не останавливает: иначе проход выбирал бы её бесконечно, свёртка встала бы
целиком, а приём продолжал бы отвечать `200`.
Очередь при этом не встаёт: собственный дедлайн свёртки обстоятельством
MUST NOT считаться — доставка, не уложившаяся в бюджет, получает `failed` и
очередь освобождает, а занятость базы блокирует запись всем одинаково. Проход
возобновляется сигналом приёма или периодическим пробуждением.
Стоящая голова очереди молчать MUST NOT: у неё нет верхнего предела ожидания, и
снаружи она неотличима от здорового потока, потому что приём продолжает отвечать
`200`. Метка отставания (см. ниже) SHALL писаться и на выходе прохода по
барьеру, а не только на пустой выборке: занятая голова очереди до пустой
выборки не пропускает проход НИКОГДА, и метка, привязанная к ней, молчала бы
ровно в том состоянии, ради которого заведена. Флаг «первый проход завершён»
при этом взводиться MUST NOT — проход до конца очереди не дошёл.
Достижимая гарантия называется точно: в порядке `(received_at, id)`
сворачиваются все доставки, **видимые воркеру** на момент выборки. Доставка,
ставшая видимой позже курсора прохода, подбирается следующим проходом;
абсолютного порядка при конкурентных приёмах система не обещает.
абсолютного порядка при конкурентных приёмах система не обещает, потому что
строка учёта становится видимой только после записи тела (измерено 184 мс на
62 МиБ).
Последствие этого предела называется вслух: доставка без плотных метрик,
свёрнутая раньше своей предшественницы, слоя не выведет и получит `failed` — то
есть её точки в витрину не попадут до пересборки. Живое состояние в этом случае
расходится с тем, что даёт `healthlog reindex`. Окно узкое (обе доставки должны
приниматься одновременно, и только у автоматизации без плотных метрик), и
изменение его сужает, а не открывает: прежде свёртка шла в порядке завершения
обработчиков. Устранение предела — отдельный вопрос, оно требует удерживать
порядок на самом приёме.
Остаточное окно молчать MUST NOT. Перед свёрткой воркер SHALL спрашивать
журнал, есть ли доставка **позже** этой по `(received_at, id)`, уже записавшая
исход разбора в витрину — то есть в статусе `parsed` или `partial`. Есть —
пишется одна запись `WARN` на доставку, с её идентификатором, ожиданием в
секундах и без значений точек.
Спрашивать журнал система SHALL **до** свёртки, а писать запись — **после** и
только если свёртка состоялась: после свёртки предикат уже видит саму эту
доставку разобранной, а отложенная доставка витрину не трогала, и запись о
свёртке вне порядка утверждала бы событие, которого не было, — да ещё
повторялась бы каждым проходом, пока голова очереди занята. Это единственное наблюдение, по которому расхождение живой витрины с
пересборкой вообще обнаружимо до сверки отпечатков; лечится оно
`healthlog reindex`.
Статус `failed` в предикат входить MUST NOT: такая доставка в витрину ничего не
записала, и перестановка относительно неё содержимого не разводит. А
`failed` — штатный исход (невыводимый слой), и его учёт превратил бы `WARN` в
шум, на который перестают смотреть.
Пересборка эту проверку выполнять MUST NOT: она идёт в порядке журнала по
построению, и её тишина здесь содержательна.
Проверка эта — **страж окна, а не постоянная часть свёртки**: закрыв порядок на
самом приёме, её SHALL снять вместе с окном. Сказано здесь потому, что иначе
страж переживёт стерегомое и станет тем, что следующий читатель удалит без
объяснения.
Последствие предела называется вслух: доставка без плотных метрик, свёрнутая
раньше своей предшественницы, слоя не выведет и получит `failed` — то есть её
точки в витрину не попадут до пересборки. Та же перестановка при равной полноте
точек оставляет в витрине версию не той доставки, что стоит в журнале последней.
Окно узкое (обе доставки должны приниматься одновременно), и изменение его
сужает, а не открывает: прежде проход перешагивал отложенную доставку.
Устранение предела — отдельный вопрос, оно требует удерживать порядок на самом
приёме.
Воркер SHALL продвигаться по неразобранным доставкам строго возрастающим
курсором в пределах одного прохода. Курсор обязателен для завершимости:
@@ -159,6 +216,21 @@
- **AND** доставка без плотных метрик наследует слой предшествующей ей по этому
порядку доставки той же автоматизации, а не соседа по времени вставки
#### Scenario: Отложенная доставка держит очередь
- **GIVEN** в очереди несколько доставок, и свёртка первой из них отложена
занятостью базы
- **WHEN** воркер делает проход
- **THEN** её преемницы в этом проходе не сворачиваются
- **AND** следующий проход снова начинает с отложенной доставки
#### Scenario: Свёртка вне порядка журнала не молчит
- **GIVEN** доставка стала видимой после того, как её преемница по журналу уже
вышла из очереди
- **WHEN** воркер сворачивает её
- **THEN** система пишет `WARN` с идентификатором доставки и без значений точек
#### Scenario: Доставка, не записавшая исход, не зацикливает проход
- **WHEN** свёртка доставки не смогла записать исход разбора и оставила её
@@ -170,7 +242,6 @@
- **GIVEN** доставка осталась `pending`, и новых доставок не приезжает
- **WHEN** наступает очередное периодическое пробуждение
- **THEN** воркер пробует свернуть её снова
### Requirement: Подбор неразобранного при старте — та же операция
При старте система SHALL сворачивать доставки, числящиеся неразобранными, тем
@@ -320,4 +391,3 @@ NOT: факт уходит в `DEBUG`, приём продолжается.
- **THEN** отказ логируется на уровне `ERROR` вместе с путём тела в архиве
- **AND** запись отличает занятость базы от прочих причин отказа
- **AND** тот же отказ по другой причине этого признака не несёт