package fold_test import ( "bytes" "context" "database/sql" "errors" "flag" "log/slog" "path/filepath" "strings" "testing" _ "modernc.org/sqlite" // тот же чистый Go-драйвер, что и у хранилища "git.vakhrushev.me/av/healthlog/internal/archive" "git.vakhrushev.me/av/healthlog/internal/fold" "git.vakhrushev.me/av/healthlog/internal/store" ) // Прогон под удерживаемой блокировкой намеренно не входит в `task test` и // `task gate`: `busy_timeout` — пять секунд, повторов транзакции пять, то есть // один этот тест стоит около двадцати пяти секунд, а гейт гоняет тесты трижды // (обычно, на флаки и под детектором гонок). // // Проверяет он при этом центральное решение задачи «разнести ответ и свёртку»: // занятость базы — обстоятельство, а не свойство доставки, и доставка обязана // остаться в очереди. Ошибка здесь означает молчаливую потерю: `failed` фоновая // свёртка не подбирает никогда, а вернуть доставку может только пересборка с // остановкой сервиса и ручной подменой базы. var runBusy = flag.Bool("healthlog.busy", false, "прогнать свёртку под удерживаемой блокировкой базы (около 25 секунд)") func TestBusyЗанятаяБазаОставляетДоставкуВОчереди(t *testing.T) { if !*runBusy { t.Skip("прогон под блокировкой выключен: задайте -healthlog.busy") } dir := t.TempDir() dbPath := filepath.Join(dir, "healthlog.db") arch, err := archive.New(filepath.Join(dir, "raw")) if err != nil { t.Fatalf("архив: %v", err) } st, err := store.Open(dbPath) if err != nil { t.Fatalf("база: %v", err) } defer func() { _ = st.Close() }() var logs bytes.Buffer f := fold.New(arch, st, 0, slog.New(slog.NewJSONHandler(&logs, nil))) deliver(t, arch, st, "d1", "Minutes", "auto-1", fixture(t, "minute.json")) // Второе соединение держит запись, как её держит свёртка широкой доставки: // измерено 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(context.Background(), nil) if err != nil { t.Fatalf("удержание записи: %v", err) } if _, err := tx.Exec(`UPDATE delivery SET points = points WHERE id = 'd1'`); err != nil { t.Fatalf("удержание записи: %v", err) } defer func() { _ = tx.Rollback() }() _, err = f.Fold(context.Background(), "d1") if !errors.Is(err, store.ErrBusy) { t.Fatalf("ошибка свёртки = %v, ожидалась %v", err, store.ErrBusy) } status, err := st.DeliveryStatus(context.Background(), "d1") if err != nil { t.Fatalf("DeliveryStatus: %v", err) } if status != store.ParsePending { t.Errorf("parse_status = %q, ожидался %q: занятость базы вывела доставку из очереди", status, store.ParsePending) } // Статуса мало: пока база занята, запись `failed` тоже не проходит, и // `pending` получился бы и без правила. Различает их лог — свёртка обязана // сказать «отложено», а не «отказ». out := logs.String() if !strings.Contains(out, "delivery fold deferred") { t.Errorf("нет записи об отложенной свёртке:\n%s", out) } if strings.Contains(out, "delivery fold failed") { t.Errorf("занятость базы записана отказом доставки:\n%s", out) } }