- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
98 lines
4.2 KiB
Go
98 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|