- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
111 lines
5.8 KiB
Go
111 lines
5.8 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
|
|
}
|
|
|
|
// 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
|
|
}
|