diff --git a/internal/db/connect.go b/internal/db/connect.go index e31d220c7454a3ccc4241b336bedbc7bf30d5fba..ef800c716efc44b137c163a188f599366f1c66e3 100644 --- a/internal/db/connect.go +++ b/internal/db/connect.go @@ -50,6 +50,13 @@ func Connect(ctx context.Context, dataDir string) (*sql.DB, error) { return nil, err } + // Serialize all access through a single connection. SQLite serializes + // writes at the file level anyway, and allowing multiple pool + // connections to interleave writes/checkpoints (especially under + // concurrent sub-agents) has caused WAL/header desync resulting in + // SQLITE_NOTADB (26) on the next open. + db.SetMaxOpenConns(1) + if err = db.PingContext(ctx); err != nil { db.Close() return nil, fmt.Errorf("failed to connect to database: %w", err) diff --git a/internal/db/connect_modernc.go b/internal/db/connect_modernc.go index 39c7faa42516297d4df497821baa0be56835be15..2e9676d6420c6fbda69cb924595a6e8a55740a5c 100644 --- a/internal/db/connect_modernc.go +++ b/internal/db/connect_modernc.go @@ -17,6 +17,9 @@ func openDB(dbPath string) (*sql.DB, error) { for name, value := range pragmas { params.Add("_pragma", fmt.Sprintf("%s(%s)", name, value)) } + // Use BEGIN IMMEDIATE so writers acquire the reserved lock up front, + // preventing deferred-to-writer upgrade deadlocks. + params.Set("_txlock", "immediate") dsn := fmt.Sprintf("file:%s?%s", dbPath, params.Encode()) db, err := sql.Open("sqlite", dsn) diff --git a/internal/db/connect_ncruces.go b/internal/db/connect_ncruces.go index 12946e29439862653e0253adc9eb6e2869bea559..d90833350030f08be364f940d483e0afb0b63eb5 100644 --- a/internal/db/connect_ncruces.go +++ b/internal/db/connect_ncruces.go @@ -11,7 +11,11 @@ import ( ) func openDB(dbPath string) (*sql.DB, error) { - db, err := driver.Open(dbPath, func(c *sqlite3.Conn) error { + // Use BEGIN IMMEDIATE so writers acquire the reserved lock up front, + // preventing deferred-to-writer upgrade deadlocks. The "file:" prefix + // is required for the ncruces driver to parse query parameters. + dsn := fmt.Sprintf("file:%s?_txlock=immediate", dbPath) + db, err := driver.Open(dsn, func(c *sqlite3.Conn) error { // Set pragmas for better performance via _pragma query params. // Format: PRAGMA name = value; for name, value := range pragmas {