mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 14:37:20 +02:00
100 lines
2.7 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|
|
}
|