Files
healthlog/internal/replay/worker_internal_test.go
T
av b278501a6e store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной
  доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил
  меньшее значение, из-за чего step_count терял род и verify:archive был красным
- правило перестало быть коммутативным осознанно, поэтому порядок свёртки
  приведён к журнальному: проход воркера прекращается на отложенной доставке,
  а свёртка вне порядка журнала пишет WARN
- заведены счётчики PointsHeld и PointsErased — удержание полнотой и
  единственное направление, в котором правило теряет содержание
2026-08-04 11:16:24 +03:00

148 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package replay
import (
"bytes"
"context"
"log/slog"
"path/filepath"
"strings"
"testing"
"time"
"git.vakhrushev.me/av/healthlog/internal/store"
)
// Отложенная доставка ДЕРЖИТ очередь: проход обязан прекратиться на ней, а не
// свернуть её преемниц раньше неё.
//
// Свойство это — единственное, на чём держится равенство «пересборка = приём»
// после смены тай-брейка: при равной полноте побеждает пришедшая доставка, то
// есть исход слияния стал функцией порядка свёртки. Перешагнув отложенную
// доставку, воркер оставил бы в витрине версию не той доставки, что стоит в
// журнале последней, — и расхождение было бы молчаливым.
//
// Свёртка подменяется полем, а не удерживается настоящая блокировка базы:
// `Deferred` наступает от занятости, а удержать её по-настоящему стоит
// двадцать пять секунд (`busy_timeout` 5 с × 5 повторов транзакции). Столько
// стоит `task verify:busy`, который в гейт намеренно не входит, — то есть
// проверка барьера жила бы только в прогоне, который никто не гоняет по
// расписанию.
func TestПроходПрекращаетсяНаОтложеннойДоставке(t *testing.T) {
t.Parallel()
dir := t.TempDir()
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
if err != nil {
t.Fatalf("база: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
ctx := context.Background()
base := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
ids := []string{"01AAAAAAAAAAAAAAAAAAAAAAAA", "01BBBBBBBBBBBBBBBBBBBBBBBB", "01CCCCCCCCCCCCCCCCCCCCCCCC"}
for i, id := range ids {
d := store.Delivery{
ID: id,
ReceivedAt: base.Add(time.Duration(i) * time.Second),
Bytes: 1,
SHA256: "-",
RawPath: "x",
ParseStatus: store.ParsePending,
}
if err := st.CreateDelivery(ctx, d); err != nil {
t.Fatalf("учёт доставки: %v", err)
}
}
w := &Worker{
store: st,
log: slog.New(slog.DiscardHandler),
wake: make(chan struct{}, 1),
}
var seen []string
w.fold = func(_ context.Context, id string) Outcome {
seen = append(seen, id)
if id == ids[1] {
return Outcome{Deferred: 1}
}
return Outcome{Folded: 1}
}
out, err := w.Pass(ctx)
if err != nil {
t.Fatalf("проход: %v", err)
}
if len(seen) != 2 || seen[0] != ids[0] || seen[1] != ids[1] {
t.Fatalf("проход тронул %v: он обязан остановиться на второй доставке", seen)
}
if out.Folded != 1 || out.Deferred != 1 {
t.Errorf("исход прохода %+v: ожидались одна свёрнутая и одна отложенная", out)
}
// Следующий проход начинает с начала очереди и доходит до преемниц: барьер
// откладывает работу, а не отменяет её. Порядок остаётся журнальным.
//
// Первая доставка появляется в списке снова потому, что подменённая свёртка
// исхода разбора не пишет и очередь не покидает: проверяется здесь порядок,
// а не учёт.
seen = nil
w.fold = func(_ context.Context, id string) Outcome {
seen = append(seen, id)
return Outcome{Folded: 1}
}
if _, err := w.Pass(ctx); err != nil {
t.Fatalf("второй проход: %v", err)
}
if len(seen) != 3 || seen[0] != ids[0] || seen[1] != ids[1] || seen[2] != ids[2] {
t.Fatalf("второй проход тронул %v, ожидался порядок журнала %v", seen, ids)
}
}
// Метка отставания обязана срабатывать и на выходе по барьеру.
//
// Иначе барьер отменял бы её ровно в том состоянии, ради которого она заведена:
// занятая голова очереди не пропускает проход до пустой выборки НИКОГДА, и
// «очередь стоит два часа» стало бы неотличимо от «споткнулась один раз».
// Дельта capability приёма обещает это дословно: «Стоящая голова очереди
// молчать MUST NOT».
func TestМеткаОтставанияСрабатываетНаБарьере(t *testing.T) {
t.Parallel()
dir := t.TempDir()
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
if err != nil {
t.Fatalf("база: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
ctx := context.Background()
// Доставка ждёт заведомо дольше порога задержки.
at := store.Now().Add(-time.Hour)
for i, id := range []string{"01AAAAAAAAAAAAAAAAAAAAAAAA", "01BBBBBBBBBBBBBBBBBBBBBBBB"} {
d := store.Delivery{
ID: id, ReceivedAt: at.Add(time.Duration(i) * time.Second),
Bytes: 1, SHA256: "-", RawPath: "x", ParseStatus: store.ParsePending,
}
if err := st.CreateDelivery(ctx, d); err != nil {
t.Fatalf("учёт доставки: %v", err)
}
}
var buf bytes.Buffer
w := &Worker{
store: st,
log: slog.New(slog.NewJSONHandler(&buf, nil)),
wake: make(chan struct{}, 1),
}
// Первый проход уже был: метка задолженности старта подавляется намеренно.
w.startupDone = true
w.fold = func(_ context.Context, _ string) Outcome { return Outcome{Deferred: 1} }
if _, err := w.Pass(ctx); err != nil {
t.Fatalf("проход: %v", err)
}
if !strings.Contains(buf.String(), "deliveries waited for fold") {
t.Errorf("метка отставания молчит на выходе по барьеру:\n%s", buf.String())
}
}