store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user