store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
This commit is contained in:
@@ -60,16 +60,28 @@ type Worker struct {
|
||||
// ждала не воркера, а его появления, и сотня одинаковых WARN при первом же
|
||||
// запуске обесценила бы уровень.
|
||||
startupDone bool
|
||||
|
||||
// fold — свёртка одной доставки. Полем, а не прямым вызовом, ради ОДНОГО
|
||||
// шва: исход `Deferred` наступает только от занятости базы, а удержать её
|
||||
// по-настоящему стоит двадцать пять секунд (`busy_timeout` 5 с × 5 повторов
|
||||
// транзакции) — столько же, сколько стоит `task verify:busy`, который в
|
||||
// гейт намеренно не входит. Барьер журнального порядка при этом и есть
|
||||
// механизм, на котором держится равенство «пересборка = приём», и оставить
|
||||
// его без дешёвой проверки значило бы проверять его только тем прогоном,
|
||||
// который никто не гоняет по расписанию.
|
||||
fold func(ctx context.Context, deliveryID string) Outcome
|
||||
}
|
||||
|
||||
// NewWorker собирает воркер над рабочей базой.
|
||||
func NewWorker(st *store.Store, f *fold.Service, log *slog.Logger) *Worker {
|
||||
return &Worker{
|
||||
w := &Worker{
|
||||
store: st,
|
||||
player: Player{Fold: f},
|
||||
log: log.With("capability", "fold-worker"),
|
||||
wake: make(chan struct{}, 1),
|
||||
}
|
||||
w.fold = w.foldOne
|
||||
return w
|
||||
}
|
||||
|
||||
// Notify будит воркер. Вызывается приёмом после того, как доставка учтена.
|
||||
@@ -169,7 +181,45 @@ func (w *Worker) Pass(ctx context.Context) (Outcome, error) {
|
||||
return total, nil
|
||||
}
|
||||
lag.add(d)
|
||||
total.Add(w.foldOne(ctx, d.ID))
|
||||
// Порядок спрашивается ДО свёртки, а говорится ПОСЛЕ и только если
|
||||
// свёртка состоялась: отложенная доставка витрину не трогала, и
|
||||
// запись о свёртке вне порядка утверждала бы событие, которого не
|
||||
// было, — да ещё повторялась бы каждым проходом, пока голова очереди
|
||||
// занята. Спросить надо всё же до: после свёртки предикат уже видит
|
||||
// саму эту доставку разобранной.
|
||||
outOfOrder := w.outOfOrder(ctx, d)
|
||||
out := w.fold(ctx, d.ID)
|
||||
total.Add(out)
|
||||
if outOfOrder && out.Deferred == 0 {
|
||||
w.warnOutOfOrder(ctx, d)
|
||||
}
|
||||
if out.Deferred > 0 {
|
||||
// Отложенная доставка ДЕРЖИТ очередь: перешагнув её, проход
|
||||
// свернул бы её преемниц раньше неё, а тай-брейк равной полноты
|
||||
// разрешается в пользу пришедшей доставки — то есть исход
|
||||
// слияния стал бы функцией порядка свёртки, отличного от
|
||||
// порядка журнала, и живая витрина разошлась бы с пересборкой
|
||||
// молча, в значениях точек.
|
||||
//
|
||||
// Очередь от этого не встаёт: отложенным считается ТОЛЬКО
|
||||
// занятость базы и отмена снаружи (store.Transient), а
|
||||
// собственный дедлайн свёртки в него намеренно не входит —
|
||||
// доставка, не уложившаяся в бюджет, получает `failed` и
|
||||
// очередь освобождает. Занятость же блокирует запись всем
|
||||
// одинаково: проход, перешагнувший занятую доставку, упёрся бы
|
||||
// в ту же занятость на следующей.
|
||||
//
|
||||
// Метка отставания пишется ЗДЕСЬ, а не только на пустой
|
||||
// выборке. Иначе барьер отменял бы её ровно в том состоянии,
|
||||
// ради которого она заведена: занятая голова очереди не
|
||||
// пропускает проход до пустой выборки никогда, и «очередь стоит
|
||||
// два часа» стало бы неотличимо от «споткнулась один раз».
|
||||
//
|
||||
// Флаг первого прохода при этом НЕ взводится: проход до конца
|
||||
// очереди не дошёл, и объявлять задолженность разобранной рано.
|
||||
w.warnLag(ctx, lag)
|
||||
return total, nil
|
||||
}
|
||||
cursor = d
|
||||
}
|
||||
}
|
||||
@@ -216,6 +266,38 @@ func (w *Worker) foldOne(ctx context.Context, deliveryID string) Outcome {
|
||||
return out
|
||||
}
|
||||
|
||||
// warnOutOfOrder называет свёртку, идущую вне порядка журнала.
|
||||
//
|
||||
// Остаточное окно закрыть здесь нечем: метка `received_at` фиксируется при
|
||||
// выпуске ULID, а строка учёта становится видимой только после записи тела
|
||||
// (измерено 184 мс на 62 МиБ), поэтому при конкурентном приёме доставка с более
|
||||
// ранней меткой появляется после того, как её преемница уже свёрнута. Барьер
|
||||
// выше этого не ловит — отставшей доставки в момент прохода не существует.
|
||||
//
|
||||
// Молчать об этом нельзя: правило слияния точек стало функцией порядка свёртки,
|
||||
// и перестановка оставляет в витрине версию не той доставки, что стоит в
|
||||
// журнале последней. Лечится `healthlog reindex`; без этой строки узнать о
|
||||
// необходимости неоткуда.
|
||||
//
|
||||
// Одна запись на доставку: событие редкое и адресное, шквала здесь не бывает.
|
||||
// Отказ запроса свёртку НЕ прекращает — наблюдение не может стоить доставки.
|
||||
func (w *Worker) outOfOrder(ctx context.Context, d store.PendingDelivery) bool {
|
||||
later, err := w.store.ParsedAfter(ctx, d.ReceivedAt, d.ID)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
w.log.ErrorContext(ctx, "journal order check failed", "error", err, "delivery_id", d.ID)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return later
|
||||
}
|
||||
|
||||
func (w *Worker) warnOutOfOrder(ctx context.Context, d store.PendingDelivery) {
|
||||
w.log.WarnContext(ctx, "delivery folded out of journal order",
|
||||
"delivery_id", d.ID,
|
||||
"waited_sec", int64(store.Now().Sub(d.ReceivedAt).Seconds()))
|
||||
}
|
||||
|
||||
// warnLag называет отставание одной строкой на проход.
|
||||
//
|
||||
// Одной, а не по строке на доставку: задолженность в сотню тел давала бы сотню
|
||||
|
||||
Reference in New Issue
Block a user