свёртка доставки в объекты и сшивка с приёмом

- internal/fold — свёртка по идентификатору доставки, тело из архива: тот же
  код, каким пойдёт пересборка витрины
- приём зовёт свёртку на context.WithoutCancel с собственным дедлайном;
  исход разбора на код ответа не влияет
- слой доставки хранится в delivery.derived_layer и наследуется от
  ПРЕДШЕСТВУЮЩЕЙ доставки автоматизации: без границы по времени свёртка
  переставала быть функцией от префикса журнала (1737 объектов против 1742)
- task verify:archive — сходимость на живом архиве, 99 доставок из 99
This commit is contained in:
av
2026-08-01 18:03:59 +03:00
parent c9ddf0b4dd
commit 32f21044e4
17 changed files with 898 additions and 36 deletions
+68 -1
View File
@@ -7,10 +7,12 @@ import (
"errors"
"io"
"log/slog"
"os"
"path/filepath"
"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"
)
@@ -121,6 +123,71 @@ 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) {
svc, _, st := newService(t)
ctx := context.Background()
body, err := os.ReadFile(filepath.Join("..", "hae", "testdata", "minute.json"))
if err != nil {
t.Fatalf("фикстура: %v", err)
}
if _, err := svc.Accept(ctx, body, ingest.Meta{Aggregation: "Minutes"}); err != nil {
t.Fatalf("Accept: %v", err)
}
d, err := st.LastDelivery(ctx)
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: разбор не дошёл до учёта")
}
n, err := st.CountBuckets(ctx)
if err != nil {
t.Fatalf("CountBuckets: %v", err)
}
if n == 0 {
t.Error("объектов 0: точки не доехали до хранилища")
}
}
// Свёртка идёт на контексте, отвязанном от запроса (context.WithoutCancel в
// Accept), чтобы обрыв соединения не оставил часть объектов записанной.
// Автотестом это не покрыто: отмену надо подать РОВНО между учётом доставки и
// свёрткой, а такого шва снаружи нет, и заводить его ради теста дороже, чем
// проверять глазами. Атомарность самой записи проверена в store
// (TestMergePointsОтменаНеОставляетПоловины).
func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store) {
t.Helper()
dir := t.TempDir()
@@ -137,5 +204,5 @@ func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store)
}
log := slog.New(slog.DiscardHandler)
return ingest.New(arch, st, log), arch, st
return ingest.New(arch, st, fold.New(arch, st, log), log), arch, st
}