- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
141 lines
8.3 KiB
Go
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
|
|
}
|