Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user