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