From 790efc16aa2787b7afc9be2b09116055eb75fc1f Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Tue, 30 May 2023 20:29:29 +0300 Subject: [PATCH] fix(databaseLocks): Fixing database lock errors on transactions The default transaction lock is `deferred`. This means that the transaction will automatically become read or write transaction based on the first DB operation. In case the first operation is `SELECT` the transaction becomes read transaction, otherwise write transaction. When a read transaction tries to write the DB sqlite will promote the transaction to a write transaction if there is no other transaction that holds a lock. When the promotion fails `database is locked` error is returned. The error is returned immediately and does not use the busy handler. In our case almost all read transaction would fail with `database is locked` error. This fix is changing the default transaction lock to `IMMEDIATE`. It translates to `BEGIN IMMEDIATE` instead of `BEGIN`. In this mode, the transaction will be created as a write transaction no matter what DB operation will run as part of the transaction. The write transaction will try to obtain the DB lock immediately when `BEGIN IMMEDIATE` is called and the busy handler is used when the DB is locked by other transaction. Fixing: https://github.com/status-im/status-desktop/issues/10838 --- sqlite/sqlite.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 35980b6d4..4f130cfb4 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -5,8 +5,10 @@ import ( "database/sql/driver" "errors" "fmt" + "net/url" "os" "runtime" + "strings" sqlcipher "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation @@ -77,6 +79,31 @@ func EncryptDB(unencryptedPath string, encryptedPath string, key string, kdfIter return err } +func buildSqlcipherDSN(path string) (string, error) { + if path == InMemoryPath { + return InMemoryPath, nil + } + + // Adding sqlcipher query parameter to the DSN + queryOperator := "?" + + if queryStart := strings.IndexRune(path, '?'); queryStart != -1 { + params, err := url.ParseQuery(path[queryStart+1:]) + if err != nil { + return "", err + } + + if len(params) > 0 { + queryOperator = "&" + } + } + + // We need to set txlock=immediate to avoid "database is locked" errors during concurrent write operations + // This could happen when a read transaction is promoted to write transaction + // https://www.sqlite.org/lang_transaction.html + return path + queryOperator + "_txlock=immediate", nil +} + func openDB(path string, key string, kdfIterationsNumber int) (*sql.DB, error) { driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers())) sql.Register(driverName, &sqlcipher.SQLiteDriver{ @@ -111,7 +138,13 @@ func openDB(path string, key string, kdfIterationsNumber int) (*sql.DB, error) { }, }) - db, err := sql.Open(driverName, path) + dsn, err := buildSqlcipherDSN(path) + + if err != nil { + return nil, err + } + + db, err := sql.Open(driverName, dsn) if err != nil { return nil, err }