Merge bitcoin/bitcoin#29253: wallet: guard against dangling to-be-reverted db transactions

b298242c8d test: sqlite, add coverage for dangling to-be-reverted db txns (furszy)
fc0e747192 sqlite: guard against dangling to-be-reverted db transactions (furszy)
472d2ca981 sqlite: introduce HasActiveTxn method (furszy)
dca874e838 sqlite: add ability to interrupt statements (furszy)
fdf9f66909 test: wallet db, exercise deadlock after write failure (furszy)

Pull request description:

  Discovered while was reviewing #29112, specifically https://github.com/bitcoin/bitcoin/pull/29112#pullrequestreview-1821862931.

  If the db handler that initiated the database transaction is destroyed,
  the ongoing transaction cannot be left dangling when the db txn fails
  to abort. It must be forcefully reverted; otherwise, any subsequent
  db handler executing a write operation will dump the dangling,
  to-be-reverted transaction data to disk.

  This not only breaks the isolation property but also results in the
  improper storage of incomplete information on disk, impacting
  the wallet consistency.

  This PR fixes the issue by resetting the db connection, automatically
  rolling back the transaction (per https://www.sqlite.org/c3ref/close.html)
  when the handler object is being destroyed and the txn abortion failed.

  Testing Notes
  Can verify the failure by reverting the fix e5217fea and running the test.
  It will fail without e5217fea and pass with it.

ACKs for top commit:
  achow101:
    ACK b298242c8d
  ryanofsky:
    Code review ACK b298242c8d. Just fix for exec result codes and comment update since last review.

Tree-SHA512: 44ba0323ab21440e79e9d7791bc1c56a8873c8bd3e8f6a85641b91576e1293011fa8032d8ae5b0580f3fb7a949356f7b9676693d7ceffa617aaad9f6569993eb
This commit is contained in:
Ava Chow committed 2024-01-31 15:22:44 -05:00
commit a01da41112
3 files changed
+129 -9

No files matched your search

+77
View File
@@ -205,5 +205,82 @@ BOOST_AUTO_TEST_CASE(db_cursor_prefix_byte_test)
}
}
BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
{
// Ensures the database remains accessible without deadlocking after a write error.
// To simulate the behavior, record overwrites are disallowed, and the test verifies
// that the database remains active after failing to store an existing record.
for (const auto& database : TestDatabases(m_path_root)) {
// Write original record
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
std::string key = "key";
std::string value = "value";
std::string value2 = "value_2";
BOOST_CHECK(batch->Write(key, value));
// Attempt to overwrite the record (expect failure)
BOOST_CHECK(!batch->Write(key, value2, /*fOverwrite=*/false));
// Successfully overwrite the record
BOOST_CHECK(batch->Write(key, value2, /*fOverwrite=*/true));
// Sanity-check; read and verify the overwritten value
std::string read_value;
BOOST_CHECK(batch->Read(key, read_value));
BOOST_CHECK_EQUAL(read_value, value2);
}
}
#ifdef USE_SQLITE
// Test-only statement execution error
constexpr int TEST_SQLITE_ERROR = -999;
class DbExecBlocker : public SQliteExecHandler
{
private:
SQliteExecHandler m_base_exec;
std::set<std::string> m_blocked_statements;
public:
DbExecBlocker(std::set<std::string> blocked_statements) : m_blocked_statements(blocked_statements) {}
int Exec(SQLiteDatabase& database, const std::string& statement) override {
if (m_blocked_statements.contains(statement)) return TEST_SQLITE_ERROR;
return m_base_exec.Exec(database, statement);
}
};
BOOST_AUTO_TEST_CASE(txn_close_failure_dangling_txn)
{
// Verifies that there is no active dangling, to-be-reversed db txn
// after the batch object that initiated it is destroyed.
DatabaseOptions options;
DatabaseStatus status;
bilingual_str error;
std::unique_ptr<SQLiteDatabase> database = MakeSQLiteDatabase(m_path_root / "sqlite", options, status, error);
std::string key = "key";
std::string value = "value";
std::unique_ptr<SQLiteBatch> batch = std::make_unique<SQLiteBatch>(*database);
BOOST_CHECK(batch->TxnBegin());
BOOST_CHECK(batch->Write(key, value));
// Set a handler to prevent txn abortion during destruction.
// Mimicking a db statement execution failure.
batch->SetExecHandler(std::make_unique<DbExecBlocker>(std::set<std::string>{"ROLLBACK TRANSACTION"}));
// Destroy batch
batch.reset();
// Ensure there is no dangling, to-be-reversed db txn
BOOST_CHECK(!database->HasActiveTxn());
// And, just as a sanity check; verify that new batchs only write what they suppose to write
// and nothing else.
std::string key2 = "key2";
std::unique_ptr<SQLiteBatch> batch2 = std::make_unique<SQLiteBatch>(*database);
BOOST_CHECK(batch2->Write(key2, value));
// The first key must not exist
BOOST_CHECK(!batch2->Exists(key));
}
#endif // USE_SQLITE
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet