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

141 lines
8.3 KiB
Go

package replay
import (
"context"
"errors"
"git.vakhrushev.me/av/healthlog/internal/fold"
"git.vakhrushev.me/av/healthlog/internal/hae"
"git.vakhrushev.me/av/healthlog/internal/store"
)
// Outcome — исход свёртки: одной доставки или их последовательности.
//
// Классы разведены потому, что читаются по-разному. FailedLayer — штатный исход
// (слой не выводится, таких тел в журнале заведомо есть), FailedMalformed —
// содержимое не разбирается, Deferred — работа не сделана по обстоятельствам,
// и доставка осталась в очереди. Только FailedOther означает, что что-то не так
// с самой свёрткой. Один общий счётчик отправлял бы человека искать дефект там,
// где его нет.
type Outcome struct {
Folded int
FailedLayer int
FailedMalformed int
// Deferred — доставка осталась `pending`: отмена или занятость базы. Не
// отказ доставки, а несделанная работа; её подберёт следующий проход.
Deferred int
FailedOther int
// Partial — доставок, в теле которых остались непокрытые разбором секции.
// Не отклонение, а половина потока; названо потому, что именно эти тела
// ретеншену трогать нельзя.
Partial int
// Incomparable — столкновений с несравнимыми наборами полей. На живом потоке
// они единичны, и на этом стоит отказ от объединения полей.
Incomparable int
// PointsHeld — точек, удержанных правилом полноты против пришедшей
// доставки. Здесь по той же причине, что EntitiesHeld: сходимость
// отпечатка удержание не проверяет ПО ПОСТРОЕНИЮ — живой приём и пересборка
// пользуются одним правилом и одинаково сойдутся на одинаково удержанной
// точке. То есть слишком строгое правило полноты выглядело бы идеальной
// сходимостью.
PointsHeld int
// PointsErased — точек, чьё содержание унесла пришедшая победительница.
// Здесь по той же причине, что PointsHeld, и с большим весом: это
// единственное направление, в котором правило слияния теряет содержание.
PointsErased int
// EntitiesHeld — версий сущностей, удержанных правилом «не теряем
// содержания». Без него правило слияния сущностей проверить нечем:
// сходимость отпечатка его не проверяет ПО ПОСТРОЕНИЮ — живой приём и
// пересборка пользуются одним правилом и одинаково сойдутся на одинаково
// удержанной версии. То есть слишком строгое правило (замораживающее
// тренировку на старой версии) выглядело бы идеальной сходимостью.
EntitiesHeld int
// EntitiesDiverging — версии одного ключа, приехавшие в одном теле с разным
// содержанием. Событие другого рода, чем удержание, и считается отдельно:
// смешанное число не отвечало бы ни на один из двух вопросов.
EntitiesDiverging int
}
// Add накапливает исход одной доставки в общий.
func (o *Outcome) Add(other Outcome) {
o.Folded += other.Folded
o.FailedLayer += other.FailedLayer
o.FailedMalformed += other.FailedMalformed
o.Deferred += other.Deferred
o.FailedOther += other.FailedOther
o.Partial += other.Partial
o.Incomparable += other.Incomparable
o.PointsHeld += other.PointsHeld
o.PointsErased += other.PointsErased
o.EntitiesHeld += other.EntitiesHeld
o.EntitiesDiverging += other.EntitiesDiverging
}
// classify раскладывает ошибку свёртки по классам исхода.
//
// Чистая функция, и это не украшение: она и есть та половина, которую задача
// требовала не дублировать между пересборкой и фоновым воркером, — а
// проверяется она перебором классов, без базы и без архива.
//
// Неэкспортируемая намеренно: её результат содержит поля `Partial` и
// `Incomparable`, `EntitiesHeld` и `EntitiesDiverging`, которые дописывает только Play, — вторая публичная дверь
// молча занижала бы именно тот счётчик, по которому принимается решение о
// судьбе тела в архиве.
func classify(err error) Outcome {
var out Outcome
switch {
case err == nil:
out.Folded++
case store.Transient(err):
// Статус доставки свёртка в этих случаях не трогает: она осталась
// `pending` и будет свёрнута снова. Правило одно на обоих — то, по
// которому свёртка решает не писать исход.
out.Deferred++
case errors.Is(err, hae.ErrLayerUnknown):
out.FailedLayer++
case errors.Is(err, hae.ErrMalformed):
out.FailedMalformed++
default:
out.FailedOther++
}
return out
}
// Player сворачивает доставку по идентификатору и классифицирует исход.
//
// Общий и для пересборки журнала, и для фонового воркера приёма — второй
// классификатор разошёлся бы с первым молча, а по одному из его счётчиков
// (`Partial`) принимается решение о судьбе тела в архиве.
type Player struct {
Fold *fold.Service
}
// Play сворачивает одну доставку и возвращает её исход.
//
// Классифицируется ТОЛЬКО ошибка свёртки: на контекст Play не смотрит, и это
// существенно. У двух вызывающих отменённый контекст означает противоположное —
// у пересборки в свёртку уходит тот же отменяемый контекст («нас остановили»),
// у воркера отвязанный от остановки, с собственным дедлайном («доставка не
// уложилась в бюджет»). Решение «работу прекратили снаружи» принимает цикл,
// каждый по своему контексту.
func (p Player) Play(ctx context.Context, deliveryID string) (Outcome, error) {
st, err := p.Fold.Fold(ctx, deliveryID)
out := classify(err)
if err == nil {
// Счётчики читаются только у успешной свёртки: при ошибке поля Stats
// заполнены частично (Uncovered у отказавшего разбора всегда пуст, хотя
// в базу список записан) — и Partial молча занижался бы. А по нему
// принимается решение о ретеншене тел.
if len(st.Uncovered) > 0 {
out.Partial++
}
out.Incomparable += st.Incomparable
out.PointsHeld += st.PointsHeld
out.PointsErased += st.PointsErased
out.EntitiesHeld += st.EntitiesHeld
out.EntitiesDiverging += st.EntitiesDiverging
}
return out, err
}