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
This commit is contained in:
Alex Jbanca 2023-05-30 20:29:29 +03:00 committed by osmaczko
parent 46399d1905
commit 790efc16aa
1 changed files with 34 additions and 1 deletions

View File

@ -5,8 +5,10 @@ import (
"database/sql/driver" "database/sql/driver"
"errors" "errors"
"fmt" "fmt"
"net/url"
"os" "os"
"runtime" "runtime"
"strings"
sqlcipher "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation 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 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) { func openDB(path string, key string, kdfIterationsNumber int) (*sql.DB, error) {
driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers())) driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers()))
sql.Register(driverName, &sqlcipher.SQLiteDriver{ 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 { if err != nil {
return nil, err return nil, err
} }