fix(db): use immediate txlock for SQLite instead of MaxOpenConns(1)

MaxOpenConns(1) caused Go-level deadlocks: when two goroutines needed
database connections concurrently, the second blocked forever waiting
for the single connection pool slot. This broke CI (sqlite web tests
timed out after 45min, e2e tests hung).

The actual "database is locked" errors were caused by SQLite's default
deferred transaction locking: two connections both acquire SHARED locks,
then deadlock when both try to promote to RESERVED for writing. SQLite
detects this instantly and returns SQLITE_BUSY, bypassing busy_timeout.

_txlock=immediate fixes this by acquiring the write lock at BEGIN time.
The second concurrent transaction waits (up to busy_timeout) instead of
deadlocking. Combined with WAL mode (concurrent readers + single writer),
this handles concurrency correctly without restricting the Go connection
pool.
This commit is contained in:
kolaente
2026-03-02 14:03:33 +01:00
parent 28f98a7a96
commit 26c0f71b6c

View File

@@ -254,6 +254,8 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
if err != nil {
return
}
// In-memory with shared cache requires a single connection to avoid
// "database is locked" since all connections share the same state.
engine.SetMaxOpenConns(1)
engine.SetMaxIdleConns(1)
return
@@ -282,12 +284,14 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
_ = os.Remove(path) // Remove the file to not prevent the db from creating another one
}
engine, err = xorm.NewEngine("sqlite3", path+"?cache=shared&_busy_timeout=5000&_journal_mode=WAL")
if err != nil {
return
}
engine.SetMaxOpenConns(1)
engine.SetMaxIdleConns(1)
// WAL mode allows concurrent readers alongside a single writer.
// _txlock=immediate makes transactions acquire the write lock upfront
// instead of deferring it (the default). Without this, two concurrent
// deferred transactions that both read then write cause a deadlock that
// SQLite detects instantly (SQLITE_BUSY, ignoring busy_timeout).
// With immediate locking the second transaction waits (up to
// busy_timeout ms) for the first to finish, avoiding the deadlock.
engine, err = xorm.NewEngine("sqlite3", path+"?_journal_mode=WAL&_txlock=immediate")
return
}