- Remove _synchronous=NORMAL from DSN to preserve SQLite's default
durability guarantees; WAL mode alone is sufficient for the fix
- Add error handling for db.Exec and db.DB() in openTestDB helper
- Use context.WithTimeout(5s) in with_fix sub-test to prevent hang
on connection-never-released regression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLite's default DELETE journal mode combined with an unbounded GORM
connection pool allows multiple goroutines (XrayTrafficJob every 10s,
PeriodicResetJob hourly/daily, WebSocket tickers) to each obtain their
own connection and race for the write lock. When a long-running
transaction holds the lock longer than go-sqlite3's busy_timeout the
competing writer fails with "database is locked" (issue #3739).
Two changes to InitDB:
- Enable WAL journal mode (_journal_mode=WAL, _synchronous=NORMAL):
readers no longer block writers, reducing the write-lock window.
Restores the fix from PR #2645 that was accidentally reverted in
the "revert group management" commit (d18a1a37).
- SetMaxOpenConns(1): serialises all GORM access through a single
connection at the Go pool level so SQLite write-lock contention
cannot occur regardless of transaction duration.
Add database/concurrent_test.go with two test functions:
- TestConcurrentWrites: behavioural proof — reproduces the root cause
(two connections + short busy_timeout → "database is locked"), then
proves SetMaxOpenConns(1) eliminates it by serialising at pool level.
- TestInitDBConcurrencyConfig: verifies InitDB applies both settings,
completing the chain of proof that the production code path is fixed.
Closes#3739
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>