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()) } }