Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
+100
-45
@@ -12,7 +12,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/archive"
|
||||
"git.vakhrushev.me/av/healthlog/internal/fold"
|
||||
"git.vakhrushev.me/av/healthlog/internal/ingest"
|
||||
"git.vakhrushev.me/av/healthlog/internal/store"
|
||||
)
|
||||
@@ -123,32 +122,9 @@ func TestAcceptRejectsMalformed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Разбор не влияет на исход приёма: сохранили — значит приняли. Непонятое
|
||||
// содержимое даёт принятую доставку с parse_status=failed, а не отказ.
|
||||
func TestAcceptНепонятоеСодержимоеПринимается(t *testing.T) {
|
||||
svc, _, st := newService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Метрика есть, но слой определить нечем: плотных метрик нет, заголовок
|
||||
// ничего не означает, наследовать не от чего.
|
||||
body := []byte(`{"data":{"metrics":[{"name":"m","units":"u","data":[` +
|
||||
`{"date":"2026-07-31 12:00:00 +0300","qty":1}]}]}}`)
|
||||
|
||||
if _, err := svc.Accept(ctx, body, ingest.Meta{Aggregation: "Default"}); err != nil {
|
||||
t.Fatalf("Accept отверг доставку из-за разбора: %v", err)
|
||||
}
|
||||
|
||||
d, err := st.LastDelivery(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("LastDelivery: %v", err)
|
||||
}
|
||||
if d.ParseStatus != store.ParseFailed {
|
||||
t.Errorf("parse_status = %q, ожидался %q", d.ParseStatus, store.ParseFailed)
|
||||
}
|
||||
}
|
||||
|
||||
// Разобранная доставка отмечается разобранной, и точки доезжают до объектов.
|
||||
func TestAcceptРазобраннаяДоставкаОтмечена(t *testing.T) {
|
||||
// Ответ отдаётся ДО свёртки: принятая доставка ждёт разбора в очереди, а не
|
||||
// приезжает разобранной. Это смена контракта, и она проверяется явно.
|
||||
func TestAcceptОставляетДоставкуВОчереди(t *testing.T) {
|
||||
svc, _, st := newService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -165,32 +141,106 @@ func TestAcceptРазобраннаяДоставкаОтмечена(t *testing
|
||||
if err != nil {
|
||||
t.Fatalf("LastDelivery: %v", err)
|
||||
}
|
||||
if d.ParseStatus != store.ParseDone {
|
||||
t.Fatalf("parse_status = %q, ожидался %q", d.ParseStatus, store.ParseDone)
|
||||
}
|
||||
if d.Points == 0 {
|
||||
t.Error("точек 0: разбор не дошёл до учёта")
|
||||
if d.ParseStatus != store.ParsePending {
|
||||
t.Errorf("parse_status = %q, ожидался %q", d.ParseStatus, store.ParsePending)
|
||||
}
|
||||
|
||||
n, err := st.CountBuckets(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("CountBuckets: %v", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Error("объектов 0: точки не доехали до хранилища")
|
||||
if n != 0 {
|
||||
t.Errorf("объектов %d: свёртка произошла внутри приёма", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Свёртка идёт на контексте, отвязанном от запроса (context.WithoutCancel в
|
||||
// Accept), чтобы обрыв соединения не оставил часть объектов записанной.
|
||||
// Автотестом это не покрыто: отмену надо подать РОВНО между учётом доставки и
|
||||
// свёрткой, а такого шва снаружи нет, и заводить его ради теста дороже, чем
|
||||
// проверять глазами. Атомарность самой записи проверена в store
|
||||
// (TestMergePointsОтменаНеОставляетПоловины).
|
||||
|
||||
func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store) {
|
||||
t.Helper()
|
||||
// Сигнал уходит после того, как доставка учтена: воркер, разбуженный раньше,
|
||||
// потратил бы проход впустую.
|
||||
func TestAcceptБудитСвёрткуПослеУчёта(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, arch := newDeps(t, dir)
|
||||
|
||||
var seen int64
|
||||
notify := func() {
|
||||
n, err := st.CountDeliveries(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("CountDeliveries: %v", err)
|
||||
}
|
||||
seen = n
|
||||
}
|
||||
svc := ingest.New(arch, st, notify, slog.New(slog.DiscardHandler))
|
||||
|
||||
if _, err := svc.Accept(context.Background(), []byte(`{"data":{"metrics":[]}}`), ingest.Meta{}); err != nil {
|
||||
t.Fatalf("Accept: %v", err)
|
||||
}
|
||||
if seen != 1 {
|
||||
t.Errorf("на момент сигнала доставок в учёте %d, ожидалась 1", seen)
|
||||
}
|
||||
}
|
||||
|
||||
// Нулевой сигнал — законный вход (свёрткой заведует вызывающий), и приём от
|
||||
// него не падает. Паника здесь наступила бы ПОСЛЕ записи тела и учёта, то есть
|
||||
// отправитель получил бы отказ по сохранённой доставке.
|
||||
func TestAcceptБезСигналаНеПадает(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, arch := newDeps(t, dir)
|
||||
svc := ingest.New(arch, st, nil, slog.New(slog.DiscardHandler))
|
||||
|
||||
if _, err := svc.Accept(context.Background(), []byte(`{"data":{"metrics":[]}}`), ingest.Meta{}); err != nil {
|
||||
t.Fatalf("Accept: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Обрыв соединения после записи тела не должен оставлять тело без учёта:
|
||||
// доставка, не попавшая в журнал, восстанавливается только пересборкой с
|
||||
// ручной подменой базы.
|
||||
func TestAcceptУчитываетДоставкуПослеОбрываСоединения(t *testing.T) {
|
||||
svc, _, st := newService(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := svc.Accept(ctx, []byte(`{"data":{"metrics":[]}}`), ingest.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("Accept на отменённом контексте: %v", err)
|
||||
}
|
||||
|
||||
status, err := st.DeliveryStatus(context.Background(), res.DeliveryID)
|
||||
if err != nil {
|
||||
t.Fatalf("DeliveryStatus: %v", err)
|
||||
}
|
||||
if status != store.ParsePending {
|
||||
t.Errorf("parse_status = %q, ожидался %q", status, store.ParsePending)
|
||||
}
|
||||
}
|
||||
|
||||
// Учёта нет, а тело есть: приём кладёт тело на диск раньше строки в базе, и
|
||||
// отказ на вставке оставляет тело в архиве. Такое тело подберёт пересборка.
|
||||
func TestAcceptПриОтказеУчётаОставляетТелоВАрхиве(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, arch := newDeps(t, dir)
|
||||
svc := ingest.New(arch, st, nil, slog.New(slog.DiscardHandler))
|
||||
|
||||
// База закрыта — учесть доставку нечем.
|
||||
if err := st.Close(); err != nil {
|
||||
t.Fatalf("закрытие базы: %v", err)
|
||||
}
|
||||
|
||||
if _, err := svc.Accept(context.Background(), []byte(`{"data":{"metrics":[]}}`), ingest.Meta{}); err == nil {
|
||||
t.Fatal("приём не заметил, что доставка не учтена")
|
||||
}
|
||||
|
||||
entries, err := filepath.Glob(filepath.Join(arch.Root(), "*", "*", "*", "*.json.gz"))
|
||||
if err != nil {
|
||||
t.Fatalf("обход архива: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Errorf("тел в архиве %d, ожидалось 1: тело потеряно вместе с учётом", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func newDeps(t *testing.T, dir string) (*store.Store, *archive.Archive) {
|
||||
t.Helper()
|
||||
|
||||
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
|
||||
if err != nil {
|
||||
@@ -202,7 +252,12 @@ func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store)
|
||||
if err != nil {
|
||||
t.Fatalf("archive.New: %v", err)
|
||||
}
|
||||
return st, arch
|
||||
}
|
||||
|
||||
log := slog.New(slog.DiscardHandler)
|
||||
return ingest.New(arch, st, fold.New(arch, st, 0, log), log), arch, st
|
||||
func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store) {
|
||||
t.Helper()
|
||||
|
||||
st, arch := newDeps(t, t.TempDir())
|
||||
return ingest.New(arch, st, nil, slog.New(slog.DiscardHandler)), arch, st
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user