Bug 1047811 - Part 1 - Allow to commit a main-thread Storage transaction asynchronously. r=asuth

This commit is contained in:
Marco Bonardo 2014-09-10 12:46:14 +02:00
parent 8e98559e06
commit 87f3e90cdc
7 changed files with 335 additions and 309 deletions

View File

@ -7,10 +7,13 @@
#define MOZSTORAGEHELPER_H #define MOZSTORAGEHELPER_H
#include "nsAutoPtr.h" #include "nsAutoPtr.h"
#include "nsStringGlue.h"
#include "mozilla/DebugOnly.h"
#include "mozIStorageAsyncConnection.h" #include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h" #include "mozIStorageConnection.h"
#include "mozIStorageStatement.h" #include "mozIStorageStatement.h"
#include "mozIStoragePendingStatement.h"
#include "nsError.h" #include "nsError.h"
/** /**
@ -18,59 +21,113 @@
* the transaction will be completed even if you have an exception or * the transaction will be completed even if you have an exception or
* return early. * return early.
* *
* aCommitOnComplete controls whether the transaction is committed or rolled * A common use is to create an instance with aCommitOnComplete = false (rollback),
* back when it goes out of scope. A common use is to create an instance with * then call Commit() on this object manually when your function completes
* commitOnComplete = FALSE (rollback), then call Commit on this object manually * successfully.
* when your function completes successfully.
* *
* Note that nested transactions are not supported by sqlite, so if a transaction * @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, * is already in progress, this object does nothing. Note that in this case,
* you may not get the transaction type you ask for, and you won't be able * you may not get the transaction type you asked for, and you won't be able
* to rollback. * to rollback.
* *
* Note: This class is templatized to be also usable with internal data * @param aConnection
* structures. External users of this class should generally use * The connection to create the transaction on.
* |mozStorageTransaction| instead. * @param aCommitOnComplete
* Controls whether the transaction is committed or rolled back when
* this object goes out of scope.
* @param aType [optional]
* The transaction type, as defined in mozIStorageConnection. Defaults
* to TRANSACTION_DEFERRED.
* @param aAsyncCommit [optional]
* Whether commit should be executed asynchronously on the helper thread.
* This is a special option introduced as an interim solution to reduce
* main-thread fsyncs in Places. Can only be used on main-thread.
*
* WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS!
*
* Notice that async commit might cause synchronous statements to fail
* with SQLITE_BUSY. A possible mitigation strategy is to use
* PRAGMA busy_timeout, but notice that might cause main-thread jank.
* Finally, if the database is using WAL journaling mode, other
* connections won't see the changes done in async committed transactions
* until commit is complete.
*
* For all of the above reasons, this should only be used as an interim
* solution and avoided completely if possible.
*/ */
template<typename T, typename U> class mozStorageTransaction
class mozStorageTransactionBase
{ {
public: public:
mozStorageTransactionBase(T* aConnection, mozStorageTransaction(mozIStorageConnection* aConnection,
bool aCommitOnComplete, bool aCommitOnComplete,
int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED) int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED,
bool aAsyncCommit = false)
: mConnection(aConnection), : mConnection(aConnection),
mHasTransaction(false), mHasTransaction(false),
mCommitOnComplete(aCommitOnComplete), mCommitOnComplete(aCommitOnComplete),
mCompleted(false) mCompleted(false),
mAsyncCommit(aAsyncCommit)
{ {
// We won't try to get a transaction if one is already in progress. if (mConnection) {
if (mConnection) nsAutoCString query("BEGIN");
mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType)); switch(aType) {
case mozIStorageConnection::TRANSACTION_IMMEDIATE:
query.AppendLiteral(" IMMEDIATE");
break;
case mozIStorageConnection::TRANSACTION_EXCLUSIVE:
query.AppendLiteral(" EXCLUSIVE");
break;
case mozIStorageConnection::TRANSACTION_DEFERRED:
query.AppendLiteral(" DEFERRED");
break;
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));
}
} }
~mozStorageTransactionBase()
~mozStorageTransaction()
{ {
if (mConnection && mHasTransaction && ! mCompleted) { if (mConnection && mHasTransaction && !mCompleted) {
if (mCommitOnComplete) if (mCommitOnComplete) {
mConnection->CommitTransaction(); mozilla::DebugOnly<nsresult> rv = Commit();
else NS_WARN_IF_FALSE(NS_SUCCEEDED(rv),
mConnection->RollbackTransaction(); "A transaction didn't commit correctly");
}
else {
mozilla::DebugOnly<nsresult> rv = Rollback();
NS_WARN_IF_FALSE(NS_SUCCEEDED(rv),
"A transaction didn't rollback correctly");
}
} }
} }
/** /**
* Commits the transaction if one is in progress. If one is not in progress, * Commits the transaction if one is in progress. If one is not in progress,
* this is a NOP since the actual owner of the transaction outside of our * this is a NOP since the actual owner of the transaction outside of our
* scope is in charge of finally comitting or rolling back the transaction. * scope is in charge of finally committing or rolling back the transaction.
*/ */
nsresult Commit() nsresult Commit()
{ {
if (!mConnection || mCompleted) if (!mConnection || mCompleted || !mHasTransaction)
return NS_OK; // no connection, or already done return NS_OK;
mCompleted = true; mCompleted = true;
if (! mHasTransaction)
return NS_OK; // transaction not ours, ignore // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle
nsresult rv = mConnection->CommitTransaction(); // it, thus the transaction might stay open until the next COMMIT.
nsresult rv;
if (mAsyncCommit) {
nsCOMPtr<mozIStoragePendingStatement> ps;
rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"),
nullptr, getter_AddRefs(ps));
}
else {
rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT"));
}
if (NS_SUCCEEDED(rv)) if (NS_SUCCEEDED(rv))
mHasTransaction = false; mHasTransaction = false;
@ -78,22 +135,21 @@ public:
} }
/** /**
* Rolls back the transaction in progress. You should only call this function * Rolls back the transaction if one is in progress. If one is not in progress,
* if this object has a real transaction (HasTransaction() = true) because * this is a NOP since the actual owner of the transaction outside of our
* otherwise, there is no transaction to roll back. * scope is in charge of finally rolling back the transaction.
*/ */
nsresult Rollback() nsresult Rollback()
{ {
if (!mConnection || mCompleted) if (!mConnection || mCompleted || !mHasTransaction)
return NS_OK; // no connection, or already done return NS_OK;
mCompleted = true; mCompleted = true;
if (! mHasTransaction)
return NS_ERROR_FAILURE;
// It is possible that a rollback will return busy, so we busy wait... // 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 = NS_OK; nsresult rv = NS_OK;
do { do {
rv = mConnection->RollbackTransaction(); rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK"));
if (rv == NS_ERROR_STORAGE_BUSY) if (rv == NS_ERROR_STORAGE_BUSY)
(void)PR_Sleep(PR_INTERVAL_NO_WAIT); (void)PR_Sleep(PR_INTERVAL_NO_WAIT);
} while (rv == NS_ERROR_STORAGE_BUSY); } while (rv == NS_ERROR_STORAGE_BUSY);
@ -104,42 +160,14 @@ public:
return rv; return rv;
} }
/**
* Returns whether this object wraps a real transaction. False means that
* this object doesn't do anything because there was already a transaction in
* progress when it was created.
*/
bool HasTransaction()
{
return mHasTransaction;
}
/**
* This sets the default action (commit or rollback) when this object goes
* out of scope.
*/
void SetDefaultAction(bool aCommitOnComplete)
{
mCommitOnComplete = aCommitOnComplete;
}
protected: protected:
U mConnection; nsCOMPtr<mozIStorageConnection> mConnection;
bool mHasTransaction; bool mHasTransaction;
bool mCommitOnComplete; bool mCommitOnComplete;
bool mCompleted; bool mCompleted;
bool mAsyncCommit;
}; };
/**
* An instance of the mozStorageTransaction<> family dedicated
* to |mozIStorageConnection|.
*/
typedef mozStorageTransactionBase<mozIStorageConnection,
nsCOMPtr<mozIStorageConnection> >
mozStorageTransaction;
/** /**
* This class wraps a statement so that it is guaraneed to be reset when * This class wraps a statement so that it is guaraneed to be reset when
* this object goes out of scope. * this object goes out of scope.

View File

@ -1218,6 +1218,7 @@ Connection::initializeClone(Connection* aClone, bool aReadOnly)
"journal_size_limit", "journal_size_limit",
"synchronous", "synchronous",
"wal_autocheckpoint", "wal_autocheckpoint",
"busy_timeout"
}; };
for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) { for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) {
// Read-only connections just need cache_size and temp_store pragmas. // Read-only connections just need cache_size and temp_store pragmas.

View File

@ -120,6 +120,14 @@ public:
::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData);
}; };
/**
* Gets autocommit status.
*/
bool getAutocommit() {
MOZ_ASSERT(mDBConn, "A connection must exist at this point");
return static_cast<bool>(::sqlite3_get_autocommit(mDBConn));
};
/** /**
* Lazily creates and returns a background execution thread. In the future, * Lazily creates and returns a background execution thread. In the future,
* the thread may be re-claimed if left idle, so you should call this * the thread may be re-claimed if left idle, so you should call this

View File

@ -5,9 +5,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "TestHarness.h" #include "TestHarness.h"
#include "nsMemory.h" #include "nsMemory.h"
#include "prthread.h"
#include "nsThreadUtils.h" #include "nsThreadUtils.h"
#include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceDefs.h"
#include "mozilla/ReentrantMonitor.h"
#include "mozIStorageService.h" #include "mozIStorageService.h"
#include "mozIStorageConnection.h" #include "mozIStorageConnection.h"
#include "mozIStorageStatementCallback.h" #include "mozIStorageStatementCallback.h"
@ -18,7 +22,10 @@
#include "mozIStorageStatement.h" #include "mozIStorageStatement.h"
#include "mozIStoragePendingStatement.h" #include "mozIStoragePendingStatement.h"
#include "mozIStorageError.h" #include "mozIStorageError.h"
#include "nsThreadUtils.h" #include "nsIInterfaceRequestorUtils.h"
#include "nsIEventTarget.h"
#include "sqlite3.h"
static int gTotalTests = 0; static int gTotalTests = 0;
static int gPassedTests = 0; static int gPassedTests = 0;
@ -53,13 +60,11 @@ static int gPassedTests = 0;
do_check_true(aExpected == aActual) do_check_true(aExpected == aActual)
#else #else
#include <sstream> #include <sstream>
// Print nsresult as uint32_t // Print nsresult as uint32_t
std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
{ {
return aStream << static_cast<uint32_t>(aInput); return aStream << static_cast<uint32_t>(aInput);
} }
#define do_check_eq(aExpected, aActual) \ #define do_check_eq(aExpected, aActual) \
PR_BEGIN_MACRO \ PR_BEGIN_MACRO \
gTotalTests++; \ gTotalTests++; \
@ -74,6 +79,8 @@ std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
PR_END_MACRO PR_END_MACRO
#endif #endif
#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
already_AddRefed<mozIStorageService> already_AddRefed<mozIStorageService>
getService() getService()
{ {
@ -224,3 +231,159 @@ blocking_async_close(mozIStorageConnection *db)
db->AsyncClose(spinner); db->AsyncClose(spinner);
spinner->SpinUntilCompleted(); spinner->SpinUntilCompleted();
} }
////////////////////////////////////////////////////////////////////////////////
//// Mutex Watching
/**
* Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on
* the caller (generally main) thread. We do this by decorating the sqlite
* mutex logic with our own code that checks what thread it is being invoked on
* and sets a flag if it is invoked on the main thread. We are able to easily
* decorate the SQLite mutex logic because SQLite allows us to retrieve the
* current function pointers being used and then provide a new set.
*/
sqlite3_mutex_methods orig_mutex_methods;
sqlite3_mutex_methods wrapped_mutex_methods;
bool mutex_used_on_watched_thread = false;
PRThread *watched_thread = nullptr;
/**
* Ugly hack to let us figure out what a connection's async thread is. If we
* were MOZILLA_INTERNAL_API and linked as such we could just include
* mozStorageConnection.h and just ask Connection directly. But that turns out
* poorly.
*
* When the thread a mutex is invoked on isn't watched_thread we save it to this
* variable.
*/
PRThread *last_non_watched_thread = nullptr;
/**
* Set a flag if the mutex is used on the thread we are watching, but always
* call the real mutex function.
*/
extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex)
{
PRThread *curThread = ::PR_GetCurrentThread();
if (curThread == watched_thread)
mutex_used_on_watched_thread = true;
else
last_non_watched_thread = curThread;
orig_mutex_methods.xMutexEnter(mutex);
}
extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex)
{
if (::PR_GetCurrentThread() == watched_thread)
mutex_used_on_watched_thread = true;
return orig_mutex_methods.xMutexTry(mutex);
}
void hook_sqlite_mutex()
{
// We need to initialize and teardown SQLite to get it to set up the
// default mutex handlers for us so we can steal them and wrap them.
do_check_ok(sqlite3_initialize());
do_check_ok(sqlite3_shutdown());
do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods));
do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods));
wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter;
wrapped_mutex_methods.xMutexTry = wrapped_MutexTry;
do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods));
}
/**
* Call to clear the watch state and to set the watching against this thread.
*
* Check |mutex_used_on_watched_thread| to see if the mutex has fired since
* this method was last called. Since we're talking about the current thread,
* there are no race issues to be concerned about
*/
void watch_for_mutex_use_on_this_thread()
{
watched_thread = ::PR_GetCurrentThread();
mutex_used_on_watched_thread = false;
}
////////////////////////////////////////////////////////////////////////////////
//// Thread Wedgers
/**
* A runnable that blocks until code on another thread invokes its unwedge
* method. By dispatching this to a thread you can ensure that no subsequent
* runnables dispatched to the thread will execute until you invoke unwedge.
*
* The wedger is self-dispatching, just construct it with its target.
*/
class ThreadWedger : public nsRunnable
{
public:
explicit ThreadWedger(nsIEventTarget *aTarget)
: mReentrantMonitor("thread wedger")
, unwedged(false)
{
aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL);
}
NS_IMETHOD Run()
{
mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor);
if (!unwedged)
automon.Wait();
return NS_OK;
}
void unwedge()
{
mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor);
unwedged = true;
automon.Notify();
}
private:
mozilla::ReentrantMonitor mReentrantMonitor;
bool unwedged;
};
////////////////////////////////////////////////////////////////////////////////
//// Async Helpers
/**
* A horrible hack to figure out what the connection's async thread is. By
* creating a statement and async dispatching we can tell from the mutex who
* is the async thread, PRThread style. Then we map that to an nsIThread.
*/
already_AddRefed<nsIThread>
get_conn_async_thread(mozIStorageConnection *db)
{
// Make sure we are tracking the current thread as the watched thread
watch_for_mutex_use_on_this_thread();
// - statement with nothing to bind
nsCOMPtr<mozIStorageAsyncStatement> stmt;
db->CreateAsyncStatement(
NS_LITERAL_CSTRING("SELECT 1"),
getter_AddRefs(stmt));
blocking_async_execute(stmt);
stmt->Finalize();
nsCOMPtr<nsIThreadManager> threadMan =
do_GetService("@mozilla.org/thread-manager;1");
nsCOMPtr<nsIThread> asyncThread;
threadMan->GetThreadFromPRThread(last_non_watched_thread,
getter_AddRefs(asyncThread));
// Additionally, check that the thread we get as the background thread is the
// same one as the one we report from getInterface.
nsCOMPtr<nsIEventTarget> target = do_GetInterface(db);
nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target);
PRThread *allegedPRThread;
(void)allegedAsyncThread->GetPRThread(&allegedPRThread);
do_check_eq(allegedPRThread, last_non_watched_thread);
return asyncThread.forget();
}

View File

@ -7,42 +7,19 @@
#include "storage_test_harness.h" #include "storage_test_harness.h"
#include "mozStorageHelper.h" #include "mozStorageHelper.h"
#include "mozStorageConnection.h"
using namespace mozilla;
using namespace mozilla::storage;
bool has_transaction(mozIStorageConnection* aDB) {
return !(static_cast<Connection *>(aDB)->getAutocommit());
}
/** /**
* This file test our Transaction helper in mozStorageHelper.h. * This file test our Transaction helper in mozStorageHelper.h.
*/ */
void
test_HasTransaction()
{
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
// First test that it holds the transaction after it should have gotten one.
{
mozStorageTransaction transaction(db, false);
do_check_true(transaction.HasTransaction());
(void)transaction.Commit();
// And that it does not have a transaction after we have committed.
do_check_false(transaction.HasTransaction());
}
// Check that no transaction is had after a rollback.
{
mozStorageTransaction transaction(db, false);
do_check_true(transaction.HasTransaction());
(void)transaction.Rollback();
do_check_false(transaction.HasTransaction());
}
// Check that we do not have a transaction if one is already obtained.
mozStorageTransaction outerTransaction(db, false);
do_check_true(outerTransaction.HasTransaction());
{
mozStorageTransaction innerTransaction(db, false);
do_check_false(innerTransaction.HasTransaction());
}
}
void void
test_Commit() test_Commit()
{ {
@ -52,11 +29,13 @@ test_Commit()
// exists after the transaction falls out of scope. // exists after the transaction falls out of scope.
{ {
mozStorageTransaction transaction(db, false); mozStorageTransaction transaction(db, false);
do_check_true(has_transaction(db));
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test (id INTEGER PRIMARY KEY)" "CREATE TABLE test (id INTEGER PRIMARY KEY)"
)); ));
(void)transaction.Commit(); (void)transaction.Commit();
} }
do_check_false(has_transaction(db));
bool exists = false; bool exists = false;
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
@ -72,11 +51,13 @@ test_Rollback()
// not exists after the transaction falls out of scope. // not exists after the transaction falls out of scope.
{ {
mozStorageTransaction transaction(db, true); mozStorageTransaction transaction(db, true);
do_check_true(has_transaction(db));
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test (id INTEGER PRIMARY KEY)" "CREATE TABLE test (id INTEGER PRIMARY KEY)"
)); ));
(void)transaction.Rollback(); (void)transaction.Rollback();
} }
do_check_false(has_transaction(db));
bool exists = true; bool exists = true;
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
@ -92,10 +73,12 @@ test_AutoCommit()
// transaction falls out of scope. This means the Commit was successful. // transaction falls out of scope. This means the Commit was successful.
{ {
mozStorageTransaction transaction(db, true); mozStorageTransaction transaction(db, true);
do_check_true(has_transaction(db));
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test (id INTEGER PRIMARY KEY)" "CREATE TABLE test (id INTEGER PRIMARY KEY)"
)); ));
} }
do_check_false(has_transaction(db));
bool exists = false; bool exists = false;
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
@ -112,68 +95,78 @@ test_AutoRollback()
// successful. // successful.
{ {
mozStorageTransaction transaction(db, false); mozStorageTransaction transaction(db, false);
do_check_true(has_transaction(db));
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test (id INTEGER PRIMARY KEY)" "CREATE TABLE test (id INTEGER PRIMARY KEY)"
)); ));
} }
do_check_false(has_transaction(db));
bool exists = true; bool exists = true;
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
do_check_false(exists); do_check_false(exists);
} }
void
test_SetDefaultAction()
{
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
// First we test that rollback happens when we first set it to automatically
// commit.
{
mozStorageTransaction transaction(db, true);
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test1 (id INTEGER PRIMARY KEY)"
));
transaction.SetDefaultAction(false);
}
bool exists = true;
(void)db->TableExists(NS_LITERAL_CSTRING("test1"), &exists);
do_check_false(exists);
// Now we do the opposite and test that a commit happens when we first set it
// to automatically rollback.
{
mozStorageTransaction transaction(db, false);
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test2 (id INTEGER PRIMARY KEY)"
));
transaction.SetDefaultAction(true);
}
exists = false;
(void)db->TableExists(NS_LITERAL_CSTRING("test2"), &exists);
do_check_true(exists);
}
void void
test_null_database_connection() test_null_database_connection()
{ {
// We permit the use of the Transaction helper when passing a null database // We permit the use of the Transaction helper when passing a null database
// in, so we need to make sure this still works without crashing. // in, so we need to make sure this still works without crashing.
mozStorageTransaction transaction(nullptr, false); mozStorageTransaction transaction(nullptr, false);
do_check_false(transaction.HasTransaction());
do_check_true(NS_SUCCEEDED(transaction.Commit())); do_check_true(NS_SUCCEEDED(transaction.Commit()));
do_check_true(NS_SUCCEEDED(transaction.Rollback())); do_check_true(NS_SUCCEEDED(transaction.Rollback()));
} }
void
test_async_Commit()
{
// note this will be active for any following test.
hook_sqlite_mutex();
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
// -- wedge the thread
nsCOMPtr<nsIThread> target(get_conn_async_thread(db));
do_check_true(target);
nsRefPtr<ThreadWedger> wedger (new ThreadWedger(target));
{
mozStorageTransaction transaction(db, false,
mozIStorageConnection::TRANSACTION_DEFERRED,
true);
do_check_true(has_transaction(db));
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE test (id INTEGER PRIMARY KEY)"
));
(void)transaction.Commit();
}
do_check_true(has_transaction(db));
// -- unwedge the async thread
wedger->unwedge();
// Ensure the transaction has done its job by enqueueing an async execution.
nsCOMPtr<mozIStorageAsyncStatement> stmt;
(void)db->CreateAsyncStatement(NS_LITERAL_CSTRING(
"SELECT NULL"
), getter_AddRefs(stmt));
blocking_async_execute(stmt);
stmt->Finalize();
do_check_false(has_transaction(db));
bool exists = false;
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
do_check_true(exists);
blocking_async_close(db);
}
void (*gTests[])(void) = { void (*gTests[])(void) = {
test_HasTransaction,
test_Commit, test_Commit,
test_Rollback, test_Rollback,
test_AutoCommit, test_AutoCommit,
test_AutoRollback, test_AutoRollback,
test_SetDefaultAction,
test_null_database_connection, test_null_database_connection,
test_async_Commit,
}; };
const char *file = __FILE__; const char *file = __FILE__;

View File

@ -5,175 +5,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "storage_test_harness.h" #include "storage_test_harness.h"
#include "prthread.h"
#include "nsIEventTarget.h"
#include "nsIInterfaceRequestorUtils.h"
#include "sqlite3.h"
#include "mozilla/ReentrantMonitor.h"
using mozilla::ReentrantMonitor;
using mozilla::ReentrantMonitorAutoEnter;
/**
* Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on
* the caller (generally main) thread. We do this by decorating the sqlite
* mutex logic with our own code that checks what thread it is being invoked on
* and sets a flag if it is invoked on the main thread. We are able to easily
* decorate the SQLite mutex logic because SQLite allows us to retrieve the
* current function pointers being used and then provide a new set.
*/
/* ===== Mutex Watching ===== */
sqlite3_mutex_methods orig_mutex_methods;
sqlite3_mutex_methods wrapped_mutex_methods;
bool mutex_used_on_watched_thread = false;
PRThread *watched_thread = nullptr;
/**
* Ugly hack to let us figure out what a connection's async thread is. If we
* were MOZILLA_INTERNAL_API and linked as such we could just include
* mozStorageConnection.h and just ask Connection directly. But that turns out
* poorly.
*
* When the thread a mutex is invoked on isn't watched_thread we save it to this
* variable.
*/
PRThread *last_non_watched_thread = nullptr;
/**
* Set a flag if the mutex is used on the thread we are watching, but always
* call the real mutex function.
*/
extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex)
{
PRThread *curThread = ::PR_GetCurrentThread();
if (curThread == watched_thread)
mutex_used_on_watched_thread = true;
else
last_non_watched_thread = curThread;
orig_mutex_methods.xMutexEnter(mutex);
}
extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex)
{
if (::PR_GetCurrentThread() == watched_thread)
mutex_used_on_watched_thread = true;
return orig_mutex_methods.xMutexTry(mutex);
}
#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
void hook_sqlite_mutex()
{
// We need to initialize and teardown SQLite to get it to set up the
// default mutex handlers for us so we can steal them and wrap them.
do_check_ok(sqlite3_initialize());
do_check_ok(sqlite3_shutdown());
do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods));
do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods));
wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter;
wrapped_mutex_methods.xMutexTry = wrapped_MutexTry;
do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods));
}
/**
* Call to clear the watch state and to set the watching against this thread.
*
* Check |mutex_used_on_watched_thread| to see if the mutex has fired since
* this method was last called. Since we're talking about the current thread,
* there are no race issues to be concerned about
*/
void watch_for_mutex_use_on_this_thread()
{
watched_thread = ::PR_GetCurrentThread();
mutex_used_on_watched_thread = false;
}
////////////////////////////////////////////////////////////////////////////////
//// Thread Wedgers
/**
* A runnable that blocks until code on another thread invokes its unwedge
* method. By dispatching this to a thread you can ensure that no subsequent
* runnables dispatched to the thread will execute until you invoke unwedge.
*
* The wedger is self-dispatching, just construct it with its target.
*/
class ThreadWedger : public nsRunnable
{
public:
explicit ThreadWedger(nsIEventTarget *aTarget)
: mReentrantMonitor("thread wedger")
, unwedged(false)
{
aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL);
}
NS_IMETHOD Run()
{
ReentrantMonitorAutoEnter automon(mReentrantMonitor);
if (!unwedged)
automon.Wait();
return NS_OK;
}
void unwedge()
{
ReentrantMonitorAutoEnter automon(mReentrantMonitor);
unwedged = true;
automon.Notify();
}
private:
ReentrantMonitor mReentrantMonitor;
bool unwedged;
};
////////////////////////////////////////////////////////////////////////////////
//// Async Helpers
/**
* A horrible hack to figure out what the connection's async thread is. By
* creating a statement and async dispatching we can tell from the mutex who
* is the async thread, PRThread style. Then we map that to an nsIThread.
*/
already_AddRefed<nsIThread>
get_conn_async_thread(mozIStorageConnection *db)
{
// Make sure we are tracking the current thread as the watched thread
watch_for_mutex_use_on_this_thread();
// - statement with nothing to bind
nsCOMPtr<mozIStorageAsyncStatement> stmt;
db->CreateAsyncStatement(
NS_LITERAL_CSTRING("SELECT 1"),
getter_AddRefs(stmt));
blocking_async_execute(stmt);
stmt->Finalize();
nsCOMPtr<nsIThreadManager> threadMan =
do_GetService("@mozilla.org/thread-manager;1");
nsCOMPtr<nsIThread> asyncThread;
threadMan->GetThreadFromPRThread(last_non_watched_thread,
getter_AddRefs(asyncThread));
// Additionally, check that the thread we get as the background thread is the
// same one as the one we report from getInterface.
nsCOMPtr<nsIEventTarget> target = do_GetInterface(db);
nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target);
PRThread *allegedPRThread;
(void)allegedAsyncThread->GetPRThread(&allegedPRThread);
do_check_eq(allegedPRThread, last_non_watched_thread);
return asyncThread.forget();
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
//// Tests //// Tests

View File

@ -736,6 +736,7 @@ add_task(function test_clone_copies_pragmas()
{ name: "journal_size_limit", value: 524288, copied: true }, { name: "journal_size_limit", value: 524288, copied: true },
{ name: "synchronous", value: 2, copied: true }, { name: "synchronous", value: 2, copied: true },
{ name: "wal_autocheckpoint", value: 16, copied: true }, { name: "wal_autocheckpoint", value: 16, copied: true },
{ name: "busy_timeout", value: 50, copied: true },
{ name: "ignore_check_constraints", value: 1, copied: false }, { name: "ignore_check_constraints", value: 1, copied: false },
]; ];
@ -778,6 +779,7 @@ add_task(function test_readonly_clone_copies_pragmas()
{ name: "journal_size_limit", value: 524288, copied: false }, { name: "journal_size_limit", value: 524288, copied: false },
{ name: "synchronous", value: 2, copied: false }, { name: "synchronous", value: 2, copied: false },
{ name: "wal_autocheckpoint", value: 16, copied: false }, { name: "wal_autocheckpoint", value: 16, copied: false },
{ name: "busy_timeout", value: 50, copied: false },
{ name: "ignore_check_constraints", value: 1, copied: false }, { name: "ignore_check_constraints", value: 1, copied: false },
]; ];