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:
parent
46399d1905
commit
790efc16aa
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue