mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-10-09 07:31:52 +00:00
fix🐛: give sqlite connections a busy timeout, WAL and immediate transactions
A file-backed sqlite database ran in rollback-journal mode with no busy timeout, so concurrent requests failed with "database is locked" instead of waiting for each other. Add the defaults to the sqlite3 connection string unless the configured source already sets them.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
//go:build sqlite3
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type lockRow struct {
|
||||
ID int `gorm:"primaryKey"`
|
||||
N int
|
||||
}
|
||||
|
||||
// hammer runs writers and readers against one file at the same time and
|
||||
// returns how many statements failed.
|
||||
func hammer(t *testing.T, dsn string) (failed int, firstErr error) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&lockRow{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.Create(&lockRow{ID: 1})
|
||||
|
||||
var mu sync.Mutex
|
||||
note := func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
failed++
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < 8; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 40; i++ {
|
||||
note(db.Create(&lockRow{N: i}).Error)
|
||||
// read-then-write transaction: the shape a deferred
|
||||
// transaction cannot upgrade without SQLITE_BUSY
|
||||
note(db.Transaction(func(tx *gorm.DB) error {
|
||||
var r lockRow
|
||||
if err := tx.First(&r, 1).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&lockRow{}).Where("id = 1").Update("n", r.N+1).Error
|
||||
}))
|
||||
}
|
||||
}()
|
||||
}
|
||||
for r := 0; r < 4; r++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 80; i++ {
|
||||
var c int64
|
||||
note(db.Model(&lockRow{}).Count(&c).Error)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return failed, firstErr
|
||||
}
|
||||
|
||||
func TestSqliteDefaultsPreventLocking(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "lock.db")
|
||||
if failed, err := hammer(t, sqliteDSN(file)); failed != 0 {
|
||||
t.Fatalf("%d statements failed with the defaults applied, first: %v", failed, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Documents what the defaults are for: the bare path loses statements.
|
||||
func TestSqliteBarePathLocks(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "bare.db")
|
||||
if failed, _ := hammer(t, file); failed == 0 {
|
||||
t.Skip("bare path did not lock on this machine; the comparison proves nothing here")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user