mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-05 13:45:46 +00:00
Bug 1047811 - Part 1 - Allow to commit a main-thread Storage transaction asynchronously. r=asuth
This commit is contained in:
parent
cba39c015e
commit
8d53c1682e
@ -7,10 +7,13 @@
|
||||
#define MOZSTORAGEHELPER_H
|
||||
|
||||
#include "nsAutoPtr.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "mozilla/DebugOnly.h"
|
||||
|
||||
#include "mozIStorageAsyncConnection.h"
|
||||
#include "mozIStorageConnection.h"
|
||||
#include "mozIStorageStatement.h"
|
||||
#include "mozIStoragePendingStatement.h"
|
||||
#include "nsError.h"
|
||||
|
||||
/**
|
||||
@ -18,59 +21,113 @@
|
||||
* the transaction will be completed even if you have an exception or
|
||||
* return early.
|
||||
*
|
||||
* aCommitOnComplete controls whether the transaction is committed or rolled
|
||||
* back when it goes out of scope. A common use is to create an instance with
|
||||
* commitOnComplete = FALSE (rollback), then call Commit on this object manually
|
||||
* when your function completes successfully.
|
||||
* A common use is to create an instance with aCommitOnComplete = false (rollback),
|
||||
* then call Commit() on this object manually when your function completes
|
||||
* successfully.
|
||||
*
|
||||
* Note that 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 ask for, and you won't be able
|
||||
* @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: This class is templatized to be also usable with internal data
|
||||
* structures. External users of this class should generally use
|
||||
* |mozStorageTransaction| instead.
|
||||
* @param aConnection
|
||||
* The connection to create the transaction on.
|
||||
* @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 mozStorageTransactionBase
|
||||
class mozStorageTransaction
|
||||
{
|
||||
public:
|
||||
mozStorageTransactionBase(T* aConnection,
|
||||
bool aCommitOnComplete,
|
||||
int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED)
|
||||
mozStorageTransaction(mozIStorageConnection* aConnection,
|
||||
bool aCommitOnComplete,
|
||||
int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED,
|
||||
bool aAsyncCommit = false)
|
||||
: mConnection(aConnection),
|
||||
mHasTransaction(false),
|
||||
mCommitOnComplete(aCommitOnComplete),
|
||||
mCompleted(false)
|
||||
mCompleted(false),
|
||||
mAsyncCommit(aAsyncCommit)
|
||||
{
|
||||
// We won't try to get a transaction if one is already in progress.
|
||||
if (mConnection)
|
||||
mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType));
|
||||
if (mConnection) {
|
||||
nsAutoCString query("BEGIN");
|
||||
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 (mCommitOnComplete)
|
||||
mConnection->CommitTransaction();
|
||||
else
|
||||
mConnection->RollbackTransaction();
|
||||
if (mConnection && mHasTransaction && !mCompleted) {
|
||||
if (mCommitOnComplete) {
|
||||
mozilla::DebugOnly<nsresult> rv = Commit();
|
||||
NS_WARN_IF_FALSE(NS_SUCCEEDED(rv),
|
||||
"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,
|
||||
* 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()
|
||||
{
|
||||
if (!mConnection || mCompleted)
|
||||
return NS_OK; // no connection, or already done
|
||||
if (!mConnection || mCompleted || !mHasTransaction)
|
||||
return NS_OK;
|
||||
mCompleted = true;
|
||||
if (! mHasTransaction)
|
||||
return NS_OK; // transaction not ours, ignore
|
||||
nsresult rv = mConnection->CommitTransaction();
|
||||
|
||||
// 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 (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))
|
||||
mHasTransaction = false;
|
||||
|
||||
@ -78,22 +135,21 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls back the transaction in progress. You should only call this function
|
||||
* if this object has a real transaction (HasTransaction() = true) because
|
||||
* otherwise, there is no transaction to roll back.
|
||||
* Rolls back 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
|
||||
* scope is in charge of finally rolling back the transaction.
|
||||
*/
|
||||
nsresult Rollback()
|
||||
{
|
||||
if (!mConnection || mCompleted)
|
||||
return NS_OK; // no connection, or already done
|
||||
if (!mConnection || mCompleted || !mHasTransaction)
|
||||
return NS_OK;
|
||||
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;
|
||||
do {
|
||||
rv = mConnection->RollbackTransaction();
|
||||
rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK"));
|
||||
if (rv == NS_ERROR_STORAGE_BUSY)
|
||||
(void)PR_Sleep(PR_INTERVAL_NO_WAIT);
|
||||
} while (rv == NS_ERROR_STORAGE_BUSY);
|
||||
@ -104,42 +160,14 @@ public:
|
||||
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:
|
||||
U mConnection;
|
||||
nsCOMPtr<mozIStorageConnection> mConnection;
|
||||
bool mHasTransaction;
|
||||
bool mCommitOnComplete;
|
||||
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 object goes out of scope.
|
||||
|
@ -1218,6 +1218,7 @@ Connection::initializeClone(Connection* aClone, bool aReadOnly)
|
||||
"journal_size_limit",
|
||||
"synchronous",
|
||||
"wal_autocheckpoint",
|
||||
"busy_timeout"
|
||||
};
|
||||
for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) {
|
||||
// Read-only connections just need cache_size and temp_store pragmas.
|
||||
|
@ -120,6 +120,14 @@ public:
|
||||
::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,
|
||||
* the thread may be re-claimed if left idle, so you should call this
|
||||
|
@ -5,9 +5,13 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "TestHarness.h"
|
||||
|
||||
#include "nsMemory.h"
|
||||
#include "prthread.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "nsDirectoryServiceDefs.h"
|
||||
#include "mozilla/ReentrantMonitor.h"
|
||||
|
||||
#include "mozIStorageService.h"
|
||||
#include "mozIStorageConnection.h"
|
||||
#include "mozIStorageStatementCallback.h"
|
||||
@ -18,7 +22,10 @@
|
||||
#include "mozIStorageStatement.h"
|
||||
#include "mozIStoragePendingStatement.h"
|
||||
#include "mozIStorageError.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "nsIInterfaceRequestorUtils.h"
|
||||
#include "nsIEventTarget.h"
|
||||
|
||||
#include "sqlite3.h"
|
||||
|
||||
static int gTotalTests = 0;
|
||||
static int gPassedTests = 0;
|
||||
@ -53,13 +60,11 @@ static int gPassedTests = 0;
|
||||
do_check_true(aExpected == aActual)
|
||||
#else
|
||||
#include <sstream>
|
||||
|
||||
// Print nsresult as uint32_t
|
||||
std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
|
||||
{
|
||||
return aStream << static_cast<uint32_t>(aInput);
|
||||
}
|
||||
|
||||
#define do_check_eq(aExpected, aActual) \
|
||||
PR_BEGIN_MACRO \
|
||||
gTotalTests++; \
|
||||
@ -74,6 +79,8 @@ std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
|
||||
PR_END_MACRO
|
||||
#endif
|
||||
|
||||
#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
|
||||
|
||||
already_AddRefed<mozIStorageService>
|
||||
getService()
|
||||
{
|
||||
@ -224,3 +231,159 @@ blocking_async_close(mozIStorageConnection *db)
|
||||
db->AsyncClose(spinner);
|
||||
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();
|
||||
}
|
||||
|
@ -7,42 +7,19 @@
|
||||
#include "storage_test_harness.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.
|
||||
*/
|
||||
|
||||
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
|
||||
test_Commit()
|
||||
{
|
||||
@ -52,11 +29,13 @@ test_Commit()
|
||||
// exists after the transaction falls out of scope.
|
||||
{
|
||||
mozStorageTransaction transaction(db, false);
|
||||
do_check_true(has_transaction(db));
|
||||
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TABLE test (id INTEGER PRIMARY KEY)"
|
||||
));
|
||||
(void)transaction.Commit();
|
||||
}
|
||||
do_check_false(has_transaction(db));
|
||||
|
||||
bool exists = false;
|
||||
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
|
||||
@ -72,11 +51,13 @@ test_Rollback()
|
||||
// not exists after the transaction falls out of scope.
|
||||
{
|
||||
mozStorageTransaction transaction(db, true);
|
||||
do_check_true(has_transaction(db));
|
||||
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TABLE test (id INTEGER PRIMARY KEY)"
|
||||
));
|
||||
(void)transaction.Rollback();
|
||||
}
|
||||
do_check_false(has_transaction(db));
|
||||
|
||||
bool exists = true;
|
||||
(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.
|
||||
{
|
||||
mozStorageTransaction transaction(db, true);
|
||||
do_check_true(has_transaction(db));
|
||||
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TABLE test (id INTEGER PRIMARY KEY)"
|
||||
));
|
||||
}
|
||||
do_check_false(has_transaction(db));
|
||||
|
||||
bool exists = false;
|
||||
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
|
||||
@ -112,68 +95,78 @@ test_AutoRollback()
|
||||
// successful.
|
||||
{
|
||||
mozStorageTransaction transaction(db, false);
|
||||
do_check_true(has_transaction(db));
|
||||
(void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TABLE test (id INTEGER PRIMARY KEY)"
|
||||
));
|
||||
}
|
||||
do_check_false(has_transaction(db));
|
||||
|
||||
bool exists = true;
|
||||
(void)db->TableExists(NS_LITERAL_CSTRING("test"), &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
|
||||
test_null_database_connection()
|
||||
{
|
||||
// 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.
|
||||
mozStorageTransaction transaction(nullptr, false);
|
||||
|
||||
do_check_false(transaction.HasTransaction());
|
||||
do_check_true(NS_SUCCEEDED(transaction.Commit()));
|
||||
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) = {
|
||||
test_HasTransaction,
|
||||
test_Commit,
|
||||
test_Rollback,
|
||||
test_AutoCommit,
|
||||
test_AutoRollback,
|
||||
test_SetDefaultAction,
|
||||
test_null_database_connection,
|
||||
test_async_Commit,
|
||||
};
|
||||
|
||||
const char *file = __FILE__;
|
||||
|
@ -5,175 +5,6 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#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
|
||||
|
@ -736,6 +736,7 @@ add_task(function test_clone_copies_pragmas()
|
||||
{ name: "journal_size_limit", value: 524288, copied: true },
|
||||
{ name: "synchronous", value: 2, copied: true },
|
||||
{ name: "wal_autocheckpoint", value: 16, copied: true },
|
||||
{ name: "busy_timeout", value: 50, copied: true },
|
||||
{ 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: "synchronous", value: 2, copied: false },
|
||||
{ name: "wal_autocheckpoint", value: 16, copied: false },
|
||||
{ name: "busy_timeout", value: 50, copied: false },
|
||||
{ name: "ignore_check_constraints", value: 1, copied: false },
|
||||
];
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user