Files
CyberStrikeAI/internal/database/transaction_lock_test.go
T

100 lines
2.7 KiB
Go

package database
import (
"database/sql"
"errors"
"path/filepath"
"testing"
"time"
"github.com/mattn/go-sqlite3"
"go.uber.org/zap"
)
func TestSQLiteBeginReservesWriterWithoutBlockingReaders(t *testing.T) {
for name, open := range map[string]func(string, *zap.Logger) (*DB, error){
"conversations": NewDB,
"knowledge": NewKnowledgeDB,
} {
t.Run(name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "transactions.db")
db, err := open(path, zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
// A separate connection with no busy wait makes the lock assertion
// deterministic, without relying on goroutine scheduling or sleeps.
other, err := sql.Open("sqlite3", path+"?_busy_timeout=0")
if err != nil {
t.Fatal(err)
}
defer other.Close()
other.SetMaxOpenConns(1)
if err := other.Ping(); err != nil {
t.Fatal(err)
}
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
// Begin must already own the writer reservation, even before SELECT.
_, err = other.Exec("BEGIN IMMEDIATE")
if err == nil {
_, _ = other.Exec("ROLLBACK")
t.Fatal("Begin did not reserve the writer lock")
}
var sqliteErr sqlite3.Error
if !errors.As(err, &sqliteErr) || sqliteErr.Code != sqlite3.ErrBusy {
t.Fatalf("competing writer: want SQLITE_BUSY, got %v", err)
}
var tables int
if err := other.QueryRow("SELECT COUNT(*) FROM sqlite_master").Scan(&tables); err != nil {
t.Fatalf("WAL reader blocked by writer reservation: %v", err)
}
if err := tx.Rollback(); err != nil {
t.Fatal(err)
}
if _, err := other.Exec("BEGIN IMMEDIATE"); err != nil {
t.Fatalf("writer reservation was not released: %v", err)
}
if _, err := other.Exec("ROLLBACK"); err != nil {
t.Fatal(err)
}
// A temporarily busy writer should make Begin wait, then succeed
// when that writer releases its reservation.
if _, err := other.Exec("BEGIN IMMEDIATE"); err != nil {
t.Fatal(err)
}
defer other.Exec("ROLLBACK")
done := make(chan error, 1)
go func() {
pending, err := db.Begin()
if err == nil {
err = pending.Rollback()
}
done <- err
}()
select {
case err := <-done:
t.Fatalf("Begin returned before the competing writer released its lock: %v", err)
case <-time.After(50 * time.Millisecond):
}
if _, err := other.Exec("ROLLBACK"); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
if err != nil {
t.Fatalf("Begin did not recover after writer released its lock: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("Begin did not resume after writer released its lock")
}
})
}
}