From 98ccb179f92db4e7979116c5687d5522aa91eefc Mon Sep 17 00:00:00 2001 From: Lily Ballard Date: Sun, 16 Feb 2025 22:27:33 -0800 Subject: [PATCH] libstore: use BEGIN IMMEDIATE SQLite transactions Upgrading a read transaction to a write transaction will immediately fail if another connection/process started a write transaction in the meantime. For transactions that we expect to perform a write after a read, starting with `BEGIN IMMEDIATE` gives us a write transaction immediately instead of beginning with a read transaction and upgrading it. Change-Id: I82b8a3843382940a4fa4ffe45f30b365b690ff86 --- lix/libstore/local-store.cc | 6 +++--- lix/libstore/sqlite.cc | 19 +++++++++++++++---- lix/libstore/sqlite.hh | 28 ++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/lix/libstore/local-store.cc b/lix/libstore/local-store.cc index 0b4c482c6..77594cf05 100644 --- a/lix/libstore/local-store.cc +++ b/lix/libstore/local-store.cc @@ -1116,7 +1116,7 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) return retrySQLite([&]() { auto state(_dbState.lockSync(always_progresses)); - SQLiteTxn txn = state->db.beginTransaction(); + SQLiteTxn txn = state->db.beginTransaction(SQLiteTxnType::Immediate); StorePathSet paths; for (auto & [_, i] : infos) { @@ -1508,7 +1508,7 @@ void LocalStore::invalidatePathChecked(const StorePath & path) retrySQLite([&]() { auto state(_dbState.lockSync(always_progresses)); - SQLiteTxn txn = state->db.beginTransaction(); + SQLiteTxn txn = state->db.beginTransaction(SQLiteTxnType::Immediate); if (isValidPath_(*state, path)) { StorePathSet referrers; queryReferrers(*state, path, referrers); @@ -1772,7 +1772,7 @@ void LocalStore::addSignatures(const StorePath & storePath, const StringSet & si retrySQLite([&]() { auto state(_dbState.lockSync(always_progresses)); - SQLiteTxn txn = state->db.beginTransaction(); + SQLiteTxn txn = state->db.beginTransaction(SQLiteTxnType::Immediate); auto info = std::const_pointer_cast(queryPathInfoInternal(*state, storePath)); diff --git a/lix/libstore/sqlite.cc b/lix/libstore/sqlite.cc index 8b19c18f7..a045a102f 100644 --- a/lix/libstore/sqlite.cc +++ b/lix/libstore/sqlite.cc @@ -117,9 +117,9 @@ SQLiteStmt SQLite::create(const std::string & stmt) return SQLiteStmt(db.get(), stmt); } -SQLiteTxn SQLite::beginTransaction() +SQLiteTxn SQLite::beginTransaction(SQLiteTxnType type) { - return SQLiteTxn(db.get()); + return SQLiteTxn(db.get(), type); } void SQLite::setPersistWAL(bool persist) @@ -253,9 +253,20 @@ bool SQLiteStmt::Use::isNull(int col) return sqlite3_column_type(stmt.stmt.get(), col) == SQLITE_NULL; } -SQLiteTxn::SQLiteTxn(sqlite3 * db) +SQLiteTxn::SQLiteTxn(sqlite3 * db, SQLiteTxnType type) { - if (sqlite3_exec(db, "begin;", 0, 0, 0) != SQLITE_OK) + const char * sql = "begin;"; + switch (type) { + case SQLiteTxnType::Deferred: + break; + case SQLiteTxnType::Immediate: + sql = "begin immediate;"; + break; + case SQLiteTxnType::Exclusive: + sql = "begin exclusive;"; + break; + } + if (sqlite3_exec(db, sql, 0, 0, 0) != SQLITE_OK) SQLiteError::throw_(db, "starting transaction"); this->db.reset(db); } diff --git a/lix/libstore/sqlite.hh b/lix/libstore/sqlite.hh index e4122e957..9098ab078 100644 --- a/lix/libstore/sqlite.hh +++ b/lix/libstore/sqlite.hh @@ -37,6 +37,30 @@ enum class SQLiteOpenMode { Immutable, }; +enum class SQLiteTxnType { + /** + * A deferred transaction does not actually begin until the database is first accessed. + * If the first statement in the transaction is a SELECT then a read transaction is started. + * Subsequent write statements will upgrade the transaction to a write transaction if possible, + * or return SQLITE_BUSY if another write transaction started on another database connection. + * If the first statement in the transaction is a write statement then a write transaction + * is started. + */ + Deferred, + /** + * An immediate transaction causes the database to start a write transaction immediately, + * without waiting for a write statement. The transaction might fail wth SQLITE_BUSY if another + * write transaction is already active on another database connection. + */ + Immediate, + /** + * An exclusive transaction causes the database to start a write transaction immediately. + * In WAL mode this is the same as Immediate, but in other journaling modes this prevents + * other database connections from reading the database while a transction is underway. + */ + Exclusive, +}; + struct SQLiteError; class SQLiteStmt; class SQLiteTxn; @@ -66,7 +90,7 @@ public: SQLiteStmt create(const std::string & stmt); - SQLiteTxn beginTransaction(); + SQLiteTxn beginTransaction(SQLiteTxnType type = SQLiteTxnType::Deferred); void setPersistWAL(bool persist); @@ -154,7 +178,7 @@ class SQLiteTxn }; std::unique_ptr db; - explicit SQLiteTxn(sqlite3 * db); + explicit SQLiteTxn(sqlite3 * db, SQLiteTxnType type); public: void commit();