- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
126 lines
7.2 KiB
Go
126 lines
7.2 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
|
|
// 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.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.EntitiesHeld += st.EntitiesHeld
|
|
out.EntitiesDiverging += st.EntitiesDiverging
|
|
}
|
|
return out, err
|
|
}
|