Bug 874814 - Close AsyncClose() off the main thread and loop until all statements are finalized. r=mak

This commit is contained in:
David Rajchenbach-Teller 2013-08-27 17:07:04 -04:00
parent a7618a6a15
commit bab0020ed0
6 changed files with 137 additions and 91 deletions

View File

@ -117,21 +117,20 @@ void
StorageBaseStatementInternal::asyncFinalize()
{
nsIEventTarget *target = mDBConnection->getAsyncExecutionTarget();
if (!target) {
// If we cannot get the background thread, we have to assume it has been
// shutdown (or is in the process of doing so). As a result, we should
// just finalize it here and now.
destructorAsyncFinalize();
}
else {
if (target) {
// Attempt to finalize asynchronously
nsCOMPtr<nsIRunnable> event =
new AsyncStatementFinalizer(this, mDBConnection);
// If the dispatching did not go as planned, finalize now.
if (NS_FAILED(target->Dispatch(event, NS_DISPATCH_NORMAL))) {
destructorAsyncFinalize();
}
// Dispatch. Note that dispatching can fail, typically if
// we have a race condition with asyncClose(). It's ok,
// let asyncClose() win.
(void)target->Dispatch(event, NS_DISPATCH_NORMAL);
}
// If we cannot get the background thread,
// mozStorageConnection::AsyncClose() has already been called and
// the statement either has been or will be cleaned up by
// internalClose().
}
void
@ -140,17 +139,28 @@ StorageBaseStatementInternal::destructorAsyncFinalize()
if (!mAsyncStatement)
return;
// If we reach this point, our owner has not finalized this
// statement, yet we are being destructed. If possible, we want to
// auto-finalize it early, to release the resources early.
nsIEventTarget *target = mDBConnection->getAsyncExecutionTarget();
if (target) {
// If we can get the async execution target, we can indeed finalize
// the statement, as the connection is still open.
bool isAsyncThread = false;
(void)target->IsOnCurrentThread(&isAsyncThread);
nsCOMPtr<nsIRunnable> event =
new LastDitchSqliteStatementFinalizer(mDBConnection, mAsyncStatement);
if (NS_SUCCEEDED(target->Dispatch(event, NS_DISPATCH_NORMAL))) {
mAsyncStatement = nullptr;
return;
if (isAsyncThread) {
(void)event->Run();
} else {
(void)target->Dispatch(event, NS_DISPATCH_NORMAL);
}
}
// (no async thread remains or we could not dispatch to it)
(void)::sqlite3_finalize(mAsyncStatement);
// We might not be able to dispatch to the background thread,
// presumably because it is being shutdown. Since said shutdown will
// finalize the statement, we just need to clean-up around here.
mAsyncStatement = nullptr;
}

View File

@ -133,12 +133,10 @@ public:
* calling thread).
*/
CompletionNotifier(mozIStorageStatementCallback *aCallback,
ExecutionState aReason,
StatementDataArray &aStatementData)
ExecutionState aReason)
: mCallback(aCallback)
, mReason(aReason)
{
mStatementData.SwapElements(aStatementData);
}
NS_IMETHOD Run()
@ -148,16 +146,10 @@ public:
NS_RELEASE(mCallback);
}
// The async thread could still hold onto a reference to us, so we need to
// make sure we release our reference to the StatementData now in case our
// destructor happens in a different thread.
mStatementData.Clear();
return NS_OK;
}
private:
StatementDataArray mStatementData;
mozIStorageStatementCallback *mCallback;
ExecutionState mReason;
};
@ -428,11 +420,17 @@ AsyncExecuteStatements::notifyComplete()
NS_ASSERTION(mState != PENDING,
"Still in a pending state when calling Complete!");
// Finalize our statements before we try to commit or rollback. If we are
// Reset our statements before we try to commit or rollback. If we are
// canceling and have statements that think they have pending work, the
// rollback will fail.
for (uint32_t i = 0; i < mStatements.Length(); i++)
mStatements[i].finalize();
mStatements[i].reset();
// Release references to the statement data as soon as possible. If this
// is the last reference, statements will be finalized immediately on the
// async thread, hence avoiding several bounces between threads and possible
// race conditions with AsyncClose().
mStatements.Clear();
// Handle our transaction, if we have one
if (mTransactionManager) {
@ -454,9 +452,7 @@ AsyncExecuteStatements::notifyComplete()
// Always generate a completion notification; it is what guarantees that our
// destruction does not happen here on the async thread.
nsRefPtr<CompletionNotifier> completionEvent =
new CompletionNotifier(mCallback, mState, mStatements);
NS_ASSERTION(mStatements.IsEmpty(),
"Should have given up ownership of mStatements!");
new CompletionNotifier(mCallback, mState);
// We no longer own mCallback (the CompletionNotifier takes ownership).
mCallback = nullptr;

View File

@ -329,15 +329,13 @@ WaitForUnlockNotify(sqlite3* aDatabase)
namespace {
class AsyncCloseConnection : public nsRunnable
class AsyncCloseConnection MOZ_FINAL: public nsRunnable
{
public:
AsyncCloseConnection(Connection *aConnection,
nsIEventTarget *aCallingThread,
nsIRunnable *aCallbackEvent,
already_AddRefed<nsIThread> aAsyncExecutionThread)
: mConnection(aConnection)
, mCallingThread(aCallingThread)
, mCallbackEvent(aCallbackEvent)
, mAsyncExecutionThread(aAsyncExecutionThread)
{
@ -345,46 +343,39 @@ public:
NS_METHOD Run()
{
// This event is first dispatched to the background thread to ensure that
// all pending asynchronous events are completed, and then back to the
// calling thread to actually close and notify.
bool onCallingThread = false;
(void)mCallingThread->IsOnCurrentThread(&onCallingThread);
if (!onCallingThread) {
#ifdef DEBUG
{
bool onAsyncThread = false;
(void)mAsyncExecutionThread->IsOnCurrentThread(&onAsyncThread);
MOZ_ASSERT(onAsyncThread);
}
#endif
(void)mCallingThread->Dispatch(this, NS_DISPATCH_NORMAL);
return NS_OK;
}
// This code is executed on the background thread
bool onAsyncThread = false;
(void)mAsyncExecutionThread->IsOnCurrentThread(&onAsyncThread);
MOZ_ASSERT(onAsyncThread);
#endif // DEBUG
// Internal close.
(void)mConnection->internalClose();
if (mCallbackEvent)
(void)mCallingThread->Dispatch(mCallbackEvent, NS_DISPATCH_NORMAL);
(void)mAsyncExecutionThread->Shutdown();
// Because we have no guarantee that the invocation of this method on the
// asynchronous thread has fully completed (including the Release of the
// reference to this object held by that event loop), we need to explicitly
// null out our pointers here. It is possible this object will be destroyed
// on the asynchronous thread and if the references are still alive we will
// release them on that thread. We definitely do not want that for
// mConnection and it's nice to avoid for mCallbackEvent too. We do not
// null out mCallingThread because it is conceivable the async thread might
// still be 'in' the object.
mConnection = nullptr;
mCallbackEvent = nullptr;
// Callback
if (mCallbackEvent) {
nsCOMPtr<nsIThread> thread;
(void)NS_GetMainThread(getter_AddRefs(thread));
(void)thread->Dispatch(mCallbackEvent, NS_DISPATCH_NORMAL);
}
return NS_OK;
}
~AsyncCloseConnection() {
nsCOMPtr<nsIThread> thread;
(void)NS_GetMainThread(getter_AddRefs(thread));
// Handle ambiguous nsISupports inheritance.
Connection *rawConnection = nullptr;
mConnection.swap(rawConnection);
(void)NS_ProxyRelease(thread,
NS_ISUPPORTS_CAST(mozIStorageConnection *,
rawConnection));
(void)NS_ProxyRelease(thread, mCallbackEvent);
}
private:
nsRefPtr<Connection> mConnection;
nsCOMPtr<nsIEventTarget> mCallingThread;
nsCOMPtr<nsIRunnable> mCallbackEvent;
nsCOMPtr<nsIThread> mAsyncExecutionThread;
};
@ -819,13 +810,9 @@ Connection::internalClose()
"Did not call setClosedState!");
}
{ // Ensure that we are being called on the thread we were opened with.
bool onOpenedThread = false;
(void)threadOpenedOn->IsOnCurrentThread(&onOpenedThread);
NS_ASSERTION(onOpenedThread,
"Not called on the thread the database was opened on!");
}
#endif
bool onOpeningThread = false;
(void)threadOpenedOn->IsOnCurrentThread(&onOpeningThread);
#endif // DEBUG
#ifdef PR_LOGGING
nsAutoCString leafName(":memory");
@ -835,20 +822,62 @@ Connection::internalClose()
leafName.get()));
#endif
#ifdef DEBUG
// Notify about any non-finalized statements.
sqlite3_stmt *stmt = nullptr;
while ((stmt = ::sqlite3_next_stmt(mDBConn, stmt))) {
char *msg = ::PR_smprintf("SQL statement '%s' was not finalized",
::sqlite3_sql(stmt));
NS_WARNING(msg);
::PR_smprintf_free(msg);
}
#endif
// At this stage, we may still have statements that need to be
// finalized. Attempt to close the database connection. This will
// always disconnect any virtual tables and cleanly finalize their
// internal statements. Once this is done, closing may fail due to
// unfinalized client statements, in which case we need to finalize
// these statements and close again.
int srv = ::sqlite3_close(mDBConn);
NS_ASSERTION(srv == SQLITE_OK,
int srv = sqlite3_close(mDBConn);
if (srv == SQLITE_BUSY) {
// We still have non-finalized statements. Finalize them.
sqlite3_stmt *stmt = NULL;
while ((stmt = ::sqlite3_next_stmt(mDBConn, stmt))) {
PR_LOG(gStorageLog, PR_LOG_NOTICE,
("Auto-finalizing SQL statement '%s' (%x)",
::sqlite3_sql(stmt),
stmt));
#ifdef DEBUG
char *msg = ::PR_smprintf("SQL statement '%s' (%x) should have been finalized",
::sqlite3_sql(stmt),
stmt);
NS_WARNING(msg);
::PR_smprintf_free(msg);
#endif // DEBUG
srv = ::sqlite3_finalize(stmt);
#ifdef DEBUG
if (srv != SQLITE_OK) {
char *msg = ::PR_smprintf("Could not finalize SQL statement '%s' (%x)",
::sqlite3_sql(stmt),
stmt);
NS_WARNING(msg);
::PR_smprintf_free(msg);
}
#endif // DEBUG
// Ensure that the loop continues properly, whether closing has succeeded
// or not.
if (srv == SQLITE_OK) {
stmt = NULL;
}
}
// Now that all statements have been finalized, we
// shoudl be able to close.
srv = ::sqlite3_close(mDBConn);
}
if (srv != SQLITE_OK) {
MOZ_ASSERT(srv == SQLITE_OK,
"sqlite3_close failed. There are probably outstanding statements that are listed above!");
}
mDBConn = nullptr;
return convertResultCode(srv);
@ -1047,7 +1076,7 @@ Connection::AsyncClose(mozIStorageCompletionCallback *aCallback)
return NS_ERROR_NOT_INITIALIZED;
nsIEventTarget *asyncThread = getAsyncExecutionTarget();
NS_ENSURE_TRUE(asyncThread, NS_ERROR_UNEXPECTED);
NS_ENSURE_TRUE(asyncThread, NS_ERROR_NOT_INITIALIZED);
nsresult rv = setClosedState();
NS_ENSURE_SUCCESS(rv, rv);
@ -1063,7 +1092,7 @@ Connection::AsyncClose(mozIStorageCompletionCallback *aCallback)
{
// We need to lock because we're modifying mAsyncExecutionThread
MutexAutoLock lockedScope(sharedAsyncExecutionMutex);
closeEvent = new AsyncCloseConnection(this, NS_GetCurrentThread(),
closeEvent = new AsyncCloseConnection(this,
completeEvent,
mAsyncExecutionThread.forget());
}

View File

@ -47,6 +47,14 @@ public:
StatementData()
{
}
~StatementData()
{
// We need to ensure that mParamsArray is released on the main thread,
// as the binding arguments may be XPConnect values, which are safe
// to release only on the main thread.
nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
(void)NS_ProxyRelease(mainThread, mParamsArray);
}
/**
* Return the sqlite statement, fetching it from the storage statement. In
@ -77,7 +85,7 @@ public:
* NULLs out our sqlite3_stmt (it is held by the owner) after reseting it and
* clear all bindings to it. This is expected to occur on the async thread.
*/
inline void finalize()
inline void reset()
{
NS_PRECONDITION(mStatementOwner, "Must have a statement owner!");
#ifdef DEBUG

View File

@ -228,9 +228,13 @@ function test_double_asyncClose_throws()
try {
conn.asyncClose();
do_throw("should have thrown");
}
catch (e) {
do_check_eq(e.result, Cr.NS_ERROR_UNEXPECTED);
// There is a small race condition here, which can cause either of
// Cr.NS_ERROR_NOT_INITIALIZED or Cr.NS_ERROR_UNEXPECTED to be thrown.
} catch (e if "result" in e && e.result == Cr.NS_ERROR_NOT_INITIALIZED) {
do_print("NS_ERROR_NOT_INITIALIZED");
} catch (e if "result" in e && e.result == Cr.NS_ERROR_UNEXPECTED) {
do_print("NS_ERROR_UNEXPECTED");
} catch (e) {
}
// Reset gDBConn so that later tests will get a new connection object.

View File

@ -289,7 +289,6 @@ add_task(function test_asyncClose_succeeds_with_finalized_async_statement()
stmt.executeAsync();
stmt.finalize();
let deferred = Promise.defer();
yield asyncClose(getOpenedDatabase());
// Reset gDBConn so that later tests will get a new connection object.
gDBConn = null;
@ -359,7 +358,7 @@ function standardAsyncTest(promisedDB, name, shouldInit = false) {
}
// Generate a name to insert and fetch back
let name = "worker bee " + Math.random() + " (" + name + ")";
name = "worker bee " + Math.random() + " (" + name + ")";
let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
stmt.params.name = name;