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 } // 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 } // classify раскладывает ошибку свёртки по классам исхода. // // Чистая функция, и это не украшение: она и есть та половина, которую задача // требовала не дублировать между пересборкой и фоновым воркером, — а // проверяется она перебором классов, без базы и без архива. // // Неэкспортируемая намеренно: её результат содержит поля `Partial` и // `Incomparable`, которые дописывает только 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 } return out, err }