Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable multiple db connections with sqlcipher #3507

Merged
merged 1 commit into from
May 23, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 50 additions & 40 deletions sqlite/sqlite.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current configuration on a test project, when trying a few writes at the same time I get database is locked on 10-20% of them. The test is configured to write 1Kb of data per task.

Might need some tweaking in order to get the async writing in a better shape.
Other projects are using the busy_timeout to fix this, but I think we need to make sure every write we do doesn't take more than the timeout value.

mattn/go-sqlite3#274

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen the same error before these changes a couple of times too, with the discord import when the app was under heavy load.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a workaround (PRAGMA busy_timeout=60000) to mitigate the "database is locked" errors during concurrent write operations.

This approach retains behavior similar to our current setup with one 'always blocking' connection, which waits as long as needed for the connection to release lock. I believe setting busy_timeout to 60s (aka "infinite" wait) essentially approximates the same behavior.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package sqlite

import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"os"
"runtime"

_ "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

"github.com/status-im/status-go/protocol/sqlite"
)
Expand Down Expand Up @@ -75,52 +77,60 @@ func EncryptDB(unencryptedPath string, encryptedPath string, key string, kdfIter
return err
}

func openCipher(db *sql.DB, key string, kdfIterationsNumber int, inMemory bool) error {
keyString := fmt.Sprintf("PRAGMA key = '%s'", key)
if _, err := db.Exec(keyString); err != nil {
return errors.New("failed to set key pragma")
}

if kdfIterationsNumber <= 0 {
kdfIterationsNumber = sqlite.ReducedKDFIterationsNumber
}

if _, err := db.Exec(fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIterationsNumber)); err != nil {
return err
}

// readers do not block writers and faster i/o operations
// https://www.sqlite.org/draft/wal.html
// must be set after db is encrypted
var mode string
err := db.QueryRow("PRAGMA journal_mode=WAL").Scan(&mode)
if err != nil {
return err
}
if mode != WALMode && !inMemory {
return fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode)
}

return nil
}

func openDB(path string, key string, kdfIterationsNumber int) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers()))
sql.Register(driverName, &sqlcipher.SQLiteDriver{
ConnectHook: func(conn *sqlcipher.SQLiteConn) error {
if _, err := conn.Exec("PRAGMA foreign_keys=ON", []driver.Value{}); err != nil {
return errors.New("failed to set `foreign_keys` pragma")
}

if _, err := conn.Exec(fmt.Sprintf("PRAGMA key = '%s'", key), []driver.Value{}); err != nil {
return errors.New("failed to set `key` pragma")
}

if kdfIterationsNumber <= 0 {
kdfIterationsNumber = sqlite.ReducedKDFIterationsNumber
}

if _, err := conn.Exec(fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIterationsNumber), []driver.Value{}); err != nil {
return errors.New("failed to set `kdf_iter` pragma")
}

// readers do not block writers and faster i/o operations
if _, err := conn.Exec("PRAGMA journal_mode=WAL", []driver.Value{}); err != nil && path != InMemoryPath {
return errors.New("failed to set `journal_mode` pragma")
}

// workaround to mitigate the issue of "database is locked" errors during concurrent write operations
if _, err := conn.Exec("PRAGMA busy_timeout=60000", []driver.Value{}); err != nil {
return errors.New("failed to set `busy_timeout` pragma")
}

return nil
},
})

db, err := sql.Open(driverName, path)
if err != nil {
return nil, err
}

// Disable concurrent access as not supported by the driver
db.SetMaxOpenConns(1)

if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil {
return nil, err
if path == InMemoryPath {
db.SetMaxOpenConns(1)
} else {
nproc := func() int {
maxProcs := runtime.GOMAXPROCS(0)
numCPU := runtime.NumCPU()
if maxProcs < numCPU {
return maxProcs
}
return numCPU
}()
db.SetMaxOpenConns(nproc)
db.SetMaxIdleConns(nproc)
}

err = openCipher(db, key, kdfIterationsNumber, path == InMemoryPath)
if err != nil {
return nil, err
}
return db, nil
}

Expand Down