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
+32
View File
@@ -236,6 +236,38 @@ func (s *Store) PendingDeliveries(ctx context.Context, after PendingDelivery, li
return out, nil
}
// ParsedAfter отвечает, есть ли в журнале доставка ПОЗЖЕ указанной позиции,
// уже записавшая свой разбор в витрину.
//
// Нужен ровно одному наблюдению: свёртка вне порядка журнала. Правило слияния
// точек разрешает равную полноту в пользу пришедшей доставки, то есть исход
// есть функция порядка свёртки; доставка, свёрнутая после своей преемницы,
// возвращает координату к версии, которую источник уже пересчитал, и живая
// витрина расходится с пересборкой. Барьер воркера этого не ловит: отставшей
// доставки в момент прохода просто не существует — строка учёта становится
// видимой только после записи тела.
//
// `failed` в предикат НЕ входит: такая доставка в витрину ничего не записала,
// перестановка относительно неё содержимого не разводит, а исход этот штатный
// (невыводимый слой), и его учёт превратил бы WARN в шум.
//
// Это страж окна, а не постоянная часть свёртки: закрыв порядок на самом
// приёме, метод и его вызов сносят вместе с окном.
func (s *Store) ParsedAfter(ctx context.Context, at time.Time, id string) (bool, error) {
// Кортежное сравнение, а не через OR: развёрнутая форма даёт SCAN по
// индексу вместо SEARCH — тот же приём, что в PendingDeliveries.
const q = `
SELECT EXISTS(
SELECT 1 FROM delivery
WHERE parse_status IN (?, ?) AND (received_at, id) > (?, ?))`
var found bool
if err := s.db.GetContext(ctx, &found, q, ParseDone, ParsePartial, FormatTime(at), id); err != nil {
return false, fmt.Errorf("select parsed after: %w", err)
}
return found, nil
}
// CountPendingDeliveries возвращает размер задолженности — сколько доставок
// ждут свёртки. Нужен ровно одной строке лога при старте: сколько сервис должен
// разобрать, прежде чем витрина станет полной.