aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/sqlite.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/sqlite.cpp')
-rw-r--r--src/wallet/sqlite.cpp77
1 files changed, 67 insertions, 10 deletions
diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp
index db9989163d..5e3a8179a2 100644
--- a/src/wallet/sqlite.cpp
+++ b/src/wallet/sqlite.cpp
@@ -2,6 +2,8 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#include <config/bitcoin-config.h> // IWYU pragma: keep
+
#include <wallet/sqlite.h>
#include <chainparams.h>
@@ -110,7 +112,7 @@ Mutex SQLiteDatabase::g_sqlite_mutex;
int SQLiteDatabase::g_sqlite_count = 0;
SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
- : 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)
+ : 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)
{
{
LOCK(g_sqlite_mutex);
@@ -377,6 +379,17 @@ void SQLiteDatabase::Close()
m_db = nullptr;
}
+bool SQLiteDatabase::HasActiveTxn()
+{
+ // 'sqlite3_get_autocommit' returns true by default, and false if a transaction has begun and not been committed or rolled back.
+ return m_db && sqlite3_get_autocommit(m_db) == 0;
+}
+
+int SQliteExecHandler::Exec(SQLiteDatabase& database, const std::string& statement)
+{
+ return sqlite3_exec(database.m_db, statement.data(), nullptr, nullptr, nullptr);
+}
+
std::unique_ptr<DatabaseBatch> SQLiteDatabase::MakeBatch(bool flush_on_close)
{
// We ignore flush_on_close because we don't do manual flushing for SQLite
@@ -394,12 +407,18 @@ SQLiteBatch::SQLiteBatch(SQLiteDatabase& database)
void SQLiteBatch::Close()
{
- // If m_db is in a transaction (i.e. not in autocommit mode), then abort the transaction in progress
- if (m_database.m_db && sqlite3_get_autocommit(m_database.m_db) == 0) {
+ bool force_conn_refresh = false;
+
+ // If we began a transaction, and it wasn't committed, abort the transaction in progress
+ if (m_txn) {
if (TxnAbort()) {
LogPrintf("SQLiteBatch: Batch closed unexpectedly without the transaction being explicitly committed or aborted\n");
} else {
- LogPrintf("SQLiteBatch: Batch closed and failed to abort transaction\n");
+ // If transaction cannot be aborted, it means there is a bug or there has been data corruption. Try to recover in this case
+ // by closing and reopening the database. Closing the database should also ensure that any changes made since the transaction
+ // was opened will be rolled back and future transactions can succeed without committing old data.
+ force_conn_refresh = true;
+ LogPrintf("SQLiteBatch: Batch closed and failed to abort transaction, resetting db connection..\n");
}
}
@@ -420,6 +439,19 @@ void SQLiteBatch::Close()
}
*stmt_prepared = nullptr;
}
+
+ if (force_conn_refresh) {
+ m_database.Close();
+ try {
+ m_database.Open();
+ // If TxnAbort failed and we refreshed the connection, the semaphore was not released, so release it here to avoid deadlocks on future writes.
+ m_database.m_write_semaphore.post();
+ } catch (const std::runtime_error&) {
+ // If open fails, cleanup this object and rethrow the exception
+ m_database.Close();
+ throw;
+ }
+ }
}
bool SQLiteBatch::ReadKey(DataStream&& key, DataStream& value)
@@ -465,6 +497,9 @@ bool SQLiteBatch::WriteKey(DataStream&& key, DataStream&& value, bool overwrite)
if (!BindBlobToStatement(stmt, 1, key, "key")) return false;
if (!BindBlobToStatement(stmt, 2, value, "value")) return false;
+ // Acquire semaphore if not previously acquired when creating a transaction.
+ if (!m_txn) m_database.m_write_semaphore.wait();
+
// Execute
int res = sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
@@ -472,6 +507,9 @@ bool SQLiteBatch::WriteKey(DataStream&& key, DataStream&& value, bool overwrite)
if (res != SQLITE_DONE) {
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
}
+
+ if (!m_txn) m_database.m_write_semaphore.post();
+
return res == SQLITE_DONE;
}
@@ -483,6 +521,9 @@ bool SQLiteBatch::ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob)
// Bind: leftmost parameter in statement is index 1
if (!BindBlobToStatement(stmt, 1, blob, "key")) return false;
+ // Acquire semaphore if not previously acquired when creating a transaction.
+ if (!m_txn) m_database.m_write_semaphore.wait();
+
// Execute
int res = sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
@@ -490,6 +531,9 @@ bool SQLiteBatch::ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob)
if (res != SQLITE_DONE) {
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
}
+
+ if (!m_txn) m_database.m_write_semaphore.post();
+
return res == SQLITE_DONE;
}
@@ -606,30 +650,43 @@ std::unique_ptr<DatabaseCursor> SQLiteBatch::GetNewPrefixCursor(Span<const std::
bool SQLiteBatch::TxnBegin()
{
- if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) == 0) return false;
- int res = sqlite3_exec(m_database.m_db, "BEGIN TRANSACTION", nullptr, nullptr, nullptr);
+ if (!m_database.m_db || m_txn) return false;
+ m_database.m_write_semaphore.wait();
+ Assert(!m_database.HasActiveTxn());
+ int res = Assert(m_exec_handler)->Exec(m_database, "BEGIN TRANSACTION");
if (res != SQLITE_OK) {
LogPrintf("SQLiteBatch: Failed to begin the transaction\n");
+ m_database.m_write_semaphore.post();
+ } else {
+ m_txn = true;
}
return res == SQLITE_OK;
}
bool SQLiteBatch::TxnCommit()
{
- if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
- int res = sqlite3_exec(m_database.m_db, "COMMIT TRANSACTION", nullptr, nullptr, nullptr);
+ if (!m_database.m_db || !m_txn) return false;
+ Assert(m_database.HasActiveTxn());
+ int res = Assert(m_exec_handler)->Exec(m_database, "COMMIT TRANSACTION");
if (res != SQLITE_OK) {
LogPrintf("SQLiteBatch: Failed to commit the transaction\n");
+ } else {
+ m_txn = false;
+ m_database.m_write_semaphore.post();
}
return res == SQLITE_OK;
}
bool SQLiteBatch::TxnAbort()
{
- if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
- int res = sqlite3_exec(m_database.m_db, "ROLLBACK TRANSACTION", nullptr, nullptr, nullptr);
+ if (!m_database.m_db || !m_txn) return false;
+ Assert(m_database.HasActiveTxn());
+ int res = Assert(m_exec_handler)->Exec(m_database, "ROLLBACK TRANSACTION");
if (res != SQLITE_OK) {
LogPrintf("SQLiteBatch: Failed to abort the transaction\n");
+ } else {
+ m_txn = false;
+ m_database.m_write_semaphore.post();
}
return res == SQLITE_OK;
}