Skip to content

Commit 395bcd2

Browse files
committed
sqlite: Ensure that only one SQLiteBatch is writing to db at a time
A SQLiteBatch need to wait for any other batch to finish writing before it can begin writing, otherwise db txn state may be incorrectly modified. To enforce this, each SQLiteDatabase has a semaphore which acts as a lock and is acquired by a batch when it begins a write, erase, or a transaction, and is released by it when it is done. To avoid deadlocking on itself for writing during a transaction, SQLiteBatch also keeps track of whether it has begun a transaction.
1 parent 6f7395b commit 395bcd2

File tree

2 files changed

+48
-5
lines changed

2 files changed

+48
-5
lines changed

src/wallet/sqlite.cpp

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Mutex SQLiteDatabase::g_sqlite_mutex;
110110
int SQLiteDatabase::g_sqlite_count = 0;
111111

112112
SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
113-
: WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_use_unsafe_sync(options.use_unsafe_sync)
113+
: WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_write_semaphore(1), m_use_unsafe_sync(options.use_unsafe_sync)
114114
{
115115
{
116116
LOCK(g_sqlite_mutex);
@@ -408,7 +408,7 @@ void SQLiteBatch::Close()
408408
bool force_conn_refresh = false;
409409

410410
// If we began a transaction, and it wasn't committed, abort the transaction in progress
411-
if (m_database.HasActiveTxn()) {
411+
if (m_txn) {
412412
if (TxnAbort()) {
413413
LogPrintf("SQLiteBatch: Batch closed unexpectedly without the transaction being explicitly committed or aborted\n");
414414
} else {
@@ -442,6 +442,8 @@ void SQLiteBatch::Close()
442442
m_database.Close();
443443
try {
444444
m_database.Open();
445+
// If TxnAbort failed and we refreshed the connection, the semaphore was not released, so release it here to avoid deadlocks on future writes.
446+
m_database.m_write_semaphore.post();
445447
} catch (const std::runtime_error&) {
446448
// If open fails, cleanup this object and rethrow the exception
447449
m_database.Close();
@@ -493,13 +495,19 @@ bool SQLiteBatch::WriteKey(DataStream&& key, DataStream&& value, bool overwrite)
493495
if (!BindBlobToStatement(stmt, 1, key, "key")) return false;
494496
if (!BindBlobToStatement(stmt, 2, value, "value")) return false;
495497

498+
// Acquire semaphore if not previously acquired when creating a transaction.
499+
if (!m_txn) m_database.m_write_semaphore.wait();
500+
496501
// Execute
497502
int res = sqlite3_step(stmt);
498503
sqlite3_clear_bindings(stmt);
499504
sqlite3_reset(stmt);
500505
if (res != SQLITE_DONE) {
501506
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
502507
}
508+
509+
if (!m_txn) m_database.m_write_semaphore.post();
510+
503511
return res == SQLITE_DONE;
504512
}
505513

@@ -511,13 +519,19 @@ bool SQLiteBatch::ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob)
511519
// Bind: leftmost parameter in statement is index 1
512520
if (!BindBlobToStatement(stmt, 1, blob, "key")) return false;
513521

522+
// Acquire semaphore if not previously acquired when creating a transaction.
523+
if (!m_txn) m_database.m_write_semaphore.wait();
524+
514525
// Execute
515526
int res = sqlite3_step(stmt);
516527
sqlite3_clear_bindings(stmt);
517528
sqlite3_reset(stmt);
518529
if (res != SQLITE_DONE) {
519530
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
520531
}
532+
533+
if (!m_txn) m_database.m_write_semaphore.post();
534+
521535
return res == SQLITE_DONE;
522536
}
523537

@@ -634,30 +648,43 @@ std::unique_ptr<DatabaseCursor> SQLiteBatch::GetNewPrefixCursor(Span<const std::
634648

635649
bool SQLiteBatch::TxnBegin()
636650
{
637-
if (!m_database.m_db || m_database.HasActiveTxn()) return false;
651+
if (!m_database.m_db || m_txn) return false;
652+
m_database.m_write_semaphore.wait();
653+
Assert(!m_database.HasActiveTxn());
638654
int res = Assert(m_exec_handler)->Exec(m_database, "BEGIN TRANSACTION");
639655
if (res != SQLITE_OK) {
640656
LogPrintf("SQLiteBatch: Failed to begin the transaction\n");
657+
m_database.m_write_semaphore.post();
658+
} else {
659+
m_txn = true;
641660
}
642661
return res == SQLITE_OK;
643662
}
644663

645664
bool SQLiteBatch::TxnCommit()
646665
{
647-
if (!m_database.HasActiveTxn()) return false;
666+
if (!m_database.m_db || !m_txn) return false;
667+
Assert(m_database.HasActiveTxn());
648668
int res = Assert(m_exec_handler)->Exec(m_database, "COMMIT TRANSACTION");
649669
if (res != SQLITE_OK) {
650670
LogPrintf("SQLiteBatch: Failed to commit the transaction\n");
671+
} else {
672+
m_txn = false;
673+
m_database.m_write_semaphore.post();
651674
}
652675
return res == SQLITE_OK;
653676
}
654677

655678
bool SQLiteBatch::TxnAbort()
656679
{
657-
if (!m_database.HasActiveTxn()) return false;
680+
if (!m_database.m_db || !m_txn) return false;
681+
Assert(m_database.HasActiveTxn());
658682
int res = Assert(m_exec_handler)->Exec(m_database, "ROLLBACK TRANSACTION");
659683
if (res != SQLITE_OK) {
660684
LogPrintf("SQLiteBatch: Failed to abort the transaction\n");
685+
} else {
686+
m_txn = false;
687+
m_database.m_write_semaphore.post();
661688
}
662689
return res == SQLITE_OK;
663690
}

src/wallet/sqlite.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ class SQLiteBatch : public DatabaseBatch
5858
sqlite3_stmt* m_delete_stmt{nullptr};
5959
sqlite3_stmt* m_delete_prefix_stmt{nullptr};
6060

61+
/** Whether this batch has started a database transaction and whether it owns SQLiteDatabase::m_write_semaphore.
62+
* If the batch starts a db tx, it acquires the semaphore and sets this to true, keeping the semaphore
63+
* until the transaction ends to prevent other batch objects from writing to the database.
64+
*
65+
* If this batch did not start a transaction, the semaphore is acquired transiently when writing and m_txn
66+
* is not set.
67+
*
68+
* m_txn is different from HasActiveTxn() as it is only true when this batch has started the transaction,
69+
* not just when any batch has started a transaction.
70+
*/
71+
bool m_txn{false};
72+
6173
void SetupSQLStatements();
6274
bool ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob);
6375

@@ -115,6 +127,10 @@ class SQLiteDatabase : public WalletDatabase
115127

116128
~SQLiteDatabase();
117129

130+
// Batches must acquire this semaphore on writing, and release when done writing.
131+
// This ensures that only one batch is modifying the database at a time.
132+
CSemaphore m_write_semaphore;
133+
118134
bool Verify(bilingual_str& error);
119135

120136
/** Open the database if it is not already opened */

0 commit comments

Comments
 (0)