Bug 1694231 - Add support for nested mozStorageTransaction using savepoints; r=dom-storage-reviewers,sg

The nesting level is tracked on the storage connection. The thread safety is
ensured by holding a lock while a transaction is being started/commited/rolled
back. For these purposes, the sharedDBMutex has been exposed on
mozIStorageConnection interface and additional helper methods have been added
to the interface as well.

Differential Revision: https://phabricator.services.mozilla.com/D106019
This commit is contained in:
Jan Varga 2021-03-03 18:53:14 +00:00
parent 1fe22b7cde
commit 117488daa2
7 changed files with 239 additions and 44 deletions

View File

@ -251,4 +251,23 @@ Connection::GetQuotaObjects(QuotaObject** aDatabaseQuotaObject,
return mBase->GetQuotaObjects(aDatabaseQuotaObject, aJournalQuotaObject);
}
mozilla::storage::SQLiteMutex& Connection::GetSharedDBMutex() {
return mBase->GetSharedDBMutex();
}
uint32_t Connection::GetTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return mBase->GetTransactionNestingLevel(aProofOfLock);
}
uint32_t Connection::IncreaseTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return mBase->IncreaseTransactionNestingLevel(aProofOfLock);
}
uint32_t Connection::DecreaseTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return mBase->DecreaseTransactionNestingLevel(aProofOfLock);
}
} // namespace mozilla::dom::cache

View File

@ -48,6 +48,7 @@ EXPORTS.mozilla.storage += [
"mozStorageAsyncStatementParams.h",
"mozStorageStatementParams.h",
"mozStorageStatementRow.h",
"SQLiteMutex.h",
"StatementCache.h",
"Variant.h",
"Variant_inl.h",

View File

@ -7,17 +7,20 @@
#include "mozIStorageAsyncConnection.idl"
%{C++
namespace mozilla {
namespace dom {
namespace quota {
namespace mozilla::dom::quota {
class QuotaObject;
}
}
namespace mozilla::storage {
class SQLiteMutex;
class SQLiteMutexAutoLock;
}
%}
[ptr] native QuotaObject(mozilla::dom::quota::QuotaObject);
native SQLiteMutex(mozilla::storage::SQLiteMutex&);
native SQLiteMutexAutoLock(const mozilla::storage::SQLiteMutexAutoLock&);
interface mozIStorageAggregateFunction;
interface mozIStorageCompletionCallback;
@ -41,7 +44,7 @@ interface nsIFile;
*
* @threadsafe
*/
[scriptable, uuid(4aa2ac47-8d24-4004-9b31-ec0bd85f0cc3)]
[scriptable, builtinclass, uuid(4aa2ac47-8d24-4004-9b31-ec0bd85f0cc3)]
interface mozIStorageConnection : mozIStorageAsyncConnection {
/**
* Closes a database connection. Callers must finalize all statements created
@ -256,4 +259,25 @@ interface mozIStorageConnection : mozIStorageAsyncConnection {
*/
[noscript] void getQuotaObjects(out QuotaObject aDatabaseQuotaObject,
out QuotaObject aJournalQuotaObject);
/**
* The mutex used for protection of operations (BEGIN/COMMIT/ROLLBACK) in
* mozStorageTransaction. The lock must be held in a way that spans whole
* operation, not just when accessing the nesting level.
*/
[notxpcom, nostdcall] readonly attribute SQLiteMutex sharedDBMutex;
/**
* Helper methods for managing the transaction nesting level. The methods
* must be called with a proof of lock. Currently only used by
* mozStorageTransaction.
*/
[notxpcom, nostdcall] unsigned long getTransactionNestingLevel(
in SQLiteMutexAutoLock aProofOfLock);
[notxpcom, nostdcall] unsigned long increaseTransactionNestingLevel(
in SQLiteMutexAutoLock aProofOfLock);
[notxpcom, nostdcall] unsigned long decreaseTransactionNestingLevel(
in SQLiteMutexAutoLock aProofOfLock);
};

View File

@ -444,7 +444,8 @@ Connection::Connection(Service* aService, int aFlags,
mFlags(aFlags),
mIgnoreLockingMode(aIgnoreLockingMode),
mStorageService(aService),
mSupportedOperations(aSupportedOperations) {
mSupportedOperations(aSupportedOperations),
mTransactionNestingLevel(0) {
MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY,
"Can't ignore locking for a non-readonly connection!");
mStorageService->registerConnection(this);
@ -2344,4 +2345,21 @@ Connection::GetQuotaObjects(QuotaObject** aDatabaseQuotaObject,
return NS_OK;
}
SQLiteMutex& Connection::GetSharedDBMutex() { return sharedDBMutex; }
uint32_t Connection::GetTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return mTransactionNestingLevel;
}
uint32_t Connection::IncreaseTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return ++mTransactionNestingLevel;
}
uint32_t Connection::DecreaseTransactionNestingLevel(
const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) {
return --mTransactionNestingLevel;
}
} // namespace mozilla::storage

View File

@ -472,6 +472,8 @@ class Connection final : public mozIStorageConnection,
const ConnectionOperation mSupportedOperations;
nsresult synchronousClose();
uint32_t mTransactionNestingLevel;
};
/**

View File

@ -10,6 +10,7 @@
#include "nsString.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/storage/SQLiteMutex.h"
#include "mozIStorageConnection.h"
#include "mozIStorageStatement.h"
#include "mozIStoragePendingStatement.h"
@ -24,10 +25,11 @@
* (rollback), then call Commit() on this object manually when your function
* completes successfully.
*
* @note nested transactions are not supported by Sqlite, so if a transaction
* is already in progress, this object does nothing. Note that in this case,
* you may not get the transaction type you asked for, and you won't be able
* to rollback.
* @note nested transactions are not supported by Sqlite, only nested
* savepoints, so if a transaction is already in progress, this object creates
* a nested savepoint to the existing transaction which is considered as
* anonymous savepoint itself. However, aType and aAsyncCommit are ignored
* in the case of nested savepoints.
*
* @param aConnection
* The connection to create the transaction on.
@ -55,18 +57,31 @@
* solution and avoided completely if possible.
*/
class mozStorageTransaction {
using SQLiteMutexAutoLock = mozilla::storage::SQLiteMutexAutoLock;
public:
mozStorageTransaction(
mozIStorageConnection* aConnection, bool aCommitOnComplete,
int32_t aType = mozIStorageConnection::TRANSACTION_DEFAULT,
bool aAsyncCommit = false)
: mConnection(aConnection),
mNestingLevel(0),
mHasTransaction(false),
mCommitOnComplete(aCommitOnComplete),
mCompleted(false),
mAsyncCommit(aAsyncCommit) {
if (mConnection) {
nsAutoCString query("BEGIN");
SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex());
// We nee to speculatively set the nesting level to be able to decide
// if this is a top level transaction and to be able to generate the
// savepoint name.
TransactionStarted(lock);
nsAutoCString query;
if (TopLevelTransaction(lock)) {
query.Assign("BEGIN");
int32_t type = aType;
if (type == mozIStorageConnection::TRANSACTION_DEFAULT) {
MOZ_ALWAYS_SUCCEEDS(mConnection->GetDefaultTransactionType(&type));
@ -84,9 +99,15 @@ class mozStorageTransaction {
default:
MOZ_ASSERT(false, "Unknown transaction type");
}
// If a transaction is already in progress, this will fail, since Sqlite
// doesn't support nested transactions.
mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query));
} else {
query.Assign("SAVEPOINT sp"_ns + IntToCString(mNestingLevel));
}
// If the query fails to execute we need to revert the speculatively set
// nesting level on the connection.
if (NS_FAILED(mConnection->ExecuteSimpleSQL(query))) {
TransactionFinished(lock);
}
}
}
@ -111,11 +132,24 @@ class mozStorageTransaction {
*/
nsresult Commit() {
if (!mConnection || mCompleted || !mHasTransaction) return NS_OK;
SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex());
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock));
#else
if (!CurrentTransaction(lock)) {
return NS_ERROR_NOT_AVAILABLE;
}
#endif
mCompleted = true;
// TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle
// it, thus the transaction might stay open until the next COMMIT.
nsresult rv;
if (TopLevelTransaction(lock)) {
// TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't
// handle it, thus the transaction might stay open until the next COMMIT.
if (mAsyncCommit) {
nsCOMPtr<mozIStoragePendingStatement> ps;
rv = mConnection->ExecuteSimpleSQLAsync("COMMIT"_ns, nullptr,
@ -123,8 +157,14 @@ class mozStorageTransaction {
} else {
rv = mConnection->ExecuteSimpleSQL("COMMIT"_ns);
}
} else {
rv = mConnection->ExecuteSimpleSQL("RELEASE sp"_ns +
IntToCString(mNestingLevel));
}
if (NS_SUCCEEDED(rv)) mHasTransaction = false;
if (NS_SUCCEEDED(rv)) {
TransactionFinished(lock);
}
return rv;
}
@ -136,23 +176,79 @@ class mozStorageTransaction {
*/
nsresult Rollback() {
if (!mConnection || mCompleted || !mHasTransaction) return NS_OK;
SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex());
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock));
#else
if (!CurrentTransaction(lock)) {
return NS_ERROR_NOT_AVAILABLE;
}
#endif
mCompleted = true;
nsresult rv;
if (TopLevelTransaction(lock)) {
// TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return
// a busy error, so this handling can be removed.
nsresult rv;
do {
rv = mConnection->ExecuteSimpleSQL("ROLLBACK"_ns);
if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT);
} while (rv == NS_ERROR_STORAGE_BUSY);
} else {
const auto nestingLevelCString = IntToCString(mNestingLevel);
rv = mConnection->ExecuteSimpleSQL(
"ROLLBACK TO sp"_ns + nestingLevelCString + "; RELEASE sp"_ns +
nestingLevelCString);
}
if (NS_SUCCEEDED(rv)) mHasTransaction = false;
if (NS_SUCCEEDED(rv)) {
TransactionFinished(lock);
}
return rv;
}
protected:
void TransactionStarted(const SQLiteMutexAutoLock& aProofOfLock) {
MOZ_ASSERT(mConnection);
MOZ_ASSERT(!mHasTransaction);
MOZ_ASSERT(mNestingLevel == 0);
mHasTransaction = true;
mNestingLevel = mConnection->IncreaseTransactionNestingLevel(aProofOfLock);
}
bool CurrentTransaction(const SQLiteMutexAutoLock& aProofOfLock) const {
MOZ_ASSERT(mConnection);
MOZ_ASSERT(mHasTransaction);
MOZ_ASSERT(mNestingLevel > 0);
return mNestingLevel ==
mConnection->GetTransactionNestingLevel(aProofOfLock);
}
bool TopLevelTransaction(const SQLiteMutexAutoLock& aProofOfLock) const {
MOZ_ASSERT(mConnection);
MOZ_ASSERT(mHasTransaction);
MOZ_ASSERT(mNestingLevel > 0);
MOZ_ASSERT(CurrentTransaction(aProofOfLock));
return mNestingLevel == 1;
}
void TransactionFinished(const SQLiteMutexAutoLock& aProofOfLock) {
MOZ_ASSERT(mConnection);
MOZ_ASSERT(mHasTransaction);
MOZ_ASSERT(mNestingLevel > 0);
MOZ_ASSERT(CurrentTransaction(aProofOfLock));
mConnection->DecreaseTransactionNestingLevel(aProofOfLock);
mNestingLevel = 0;
mHasTransaction = false;
}
nsCOMPtr<mozIStorageConnection> mConnection;
uint32_t mNestingLevel;
bool mHasTransaction;
bool mCommitOnComplete;
bool mCompleted;

View File

@ -139,3 +139,38 @@ TEST(storage_transaction_helper, async_Commit)
blocking_async_close(db);
}
TEST(storage_transaction_helper, Nesting)
{
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
{
mozStorageTransaction transaction(db, false);
do_check_true(has_transaction(db));
do_check_success(
db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns));
{
mozStorageTransaction nestedTransaction(db, false);
do_check_true(has_transaction(db));
do_check_success(db->ExecuteSimpleSQL(
"CREATE TABLE nested_test (id INTEGER PRIMARY KEY)"_ns));
#ifndef MOZ_DIAGNOSTIC_ASSERT_ENABLED
do_check_true(transaction.Commit() == NS_ERROR_NOT_AVAILABLE);
do_check_true(transaction.Rollback() == NS_ERROR_NOT_AVAILABLE);
#endif
}
bool exists = false;
do_check_success(db->TableExists("nested_test"_ns, &exists));
do_check_false(exists);
(void)transaction.Commit();
}
do_check_false(has_transaction(db));
bool exists = false;
do_check_success(db->TableExists("test"_ns, &exists));
do_check_true(exists);
}