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

96 lines
4.4 KiB
Go

package replay_test
import (
"context"
"database/sql"
"flag"
"log/slog"
"path/filepath"
"testing"
_ "modernc.org/sqlite" // тот же чистый Go-драйвер, что и у хранилища
"git.vakhrushev.me/av/healthlog/internal/fold"
"git.vakhrushev.me/av/healthlog/internal/replay"
)
// Прогон под удерживаемой блокировкой намеренно не входит в `task test` и
// `task gate`: `busy_timeout` — пять секунд, повторов транзакции пять, то есть
// один этот тест стоит около двадцати пяти секунд, а гейт гоняет тесты трижды.
// Запускается командой `task verify:busy`.
var runBusy = flag.Bool("healthlog.busy", false,
"прогнать свёртку под удерживаемой блокировкой базы (около 25 секунд)")
// Композиция, ради которой заведён барьер: занятость базы откладывает доставку,
// проход прекращается на ней, и живая витрина всё равно совпадает с пересборкой.
//
// Порознь это уже проверено — барьер в `worker_internal_test.go` на подменённой
// свёртке, сходимость в `order_test.go` на журнале без отложенной доставки. Но
// свойство, ради которого изменение существует, — именно композиция: тай-брейк
// «побеждает пришедшая» сделал содержимое витрины функцией порядка свёртки, и
// занятость базы это ровно тот случай, который порядок разводит. Здесь она
// НАСТОЯЩАЯ, а не подменённая: второе соединение держит запись.
func TestBusyОтложеннаяДоставкаНеРазводитПриёмИПересборку(t *testing.T) {
if !*runBusy {
t.Skip("прогон под блокировкой выключен: задайте -healthlog.busy")
}
ctx := context.Background()
dir := t.TempDir()
dbPath := filepath.Join(dir, "live.db")
arch := openArchive(t, filepath.Join(dir, "raw"))
src := openStore(t, dbPath)
log := slog.New(slog.DiscardHandler)
items := collidingJournal(t, 3)
for i := range items {
writeBody(t, arch, src, items[i], bumped(t, items[i].fixture, i))
}
w := replay.NewWorker(src, fold.New(arch, src, 0, log), log)
// Второе соединение держит запись, как её держит свёртка широкой доставки:
// измерено 11 секунд на 16 тысячах объектов, то есть окно реальное.
holder, err := sql.Open("sqlite", "file:"+dbPath+"?_pragma=busy_timeout(100)&_txlock=immediate")
if err != nil {
t.Fatalf("второе соединение: %v", err)
}
defer func() { _ = holder.Close() }()
tx, err := holder.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("удержание записи: %v", err)
}
if _, err := tx.Exec(`UPDATE delivery SET points = points`); err != nil {
t.Fatalf("удержание записи: %v", err)
}
out, err := w.Pass(ctx)
if err != nil {
t.Fatalf("проход под блокировкой: %v", err)
}
if out.Deferred != 1 {
t.Fatalf("отложенных %d, ожидалась 1: занятость не дошла до классификации, %+v", out.Deferred, out)
}
if out.Folded != 0 {
t.Fatalf("свёрнуто %d при отложенной ГОЛОВЕ очереди: барьер не сработал, %+v", out.Folded, out)
}
_ = tx.Rollback()
// Очередь пошла: те же доставки, тот же порядок журнала.
out, err = w.Pass(ctx)
if err != nil {
t.Fatalf("проход после разблокировки: %v", err)
}
if out.Folded != len(items) {
t.Fatalf("свёрнуто %d из %d: %+v", out.Folded, len(items), out)
}
rep, _ := run(t, ctx, arch, src, filepath.Join(dir, "rebuild.db"))
if got, want := rep.Fingerprint, fingerprint(t, src); got != want {
t.Errorf("отложенная занятостью доставка развела приём и пересборку:\n приём %s\n пересборка %s",
want, got)
}
}