Bug 608186 - 'IndexedDB: Transactions should expire when we return to the event loop'. r=sicking, a=blocking+

This commit is contained in:
Ben Turner 2010-11-15 13:49:49 -08:00
parent a1ca2869fa
commit 801c52fd90
12 changed files with 489 additions and 30 deletions

View File

@ -145,8 +145,7 @@ AsyncConnectionHelper::Run()
mRequest->SetDone();
}
NS_ASSERTION(!gCurrentTransaction, "Should be null!");
gCurrentTransaction = mTransaction;
SetCurrentTransaction(mTransaction);
// Call OnError if the database had an error or if the OnSuccess handler
// has an error.
@ -155,8 +154,9 @@ AsyncConnectionHelper::Run()
OnError(mRequest, mResultCode);
}
NS_ASSERTION(gCurrentTransaction == mTransaction, "Should be unchanged!");
gCurrentTransaction = nsnull;
NS_ASSERTION(GetCurrentTransaction() == mTransaction,
"Should be unchanged!");
SetCurrentTransaction(nsnull);
if (mDispatched && mTransaction) {
mTransaction->OnRequestFinished();
@ -319,6 +319,20 @@ AsyncConnectionHelper::GetCurrentTransaction()
return gCurrentTransaction;
}
// static
void
AsyncConnectionHelper::SetCurrentTransaction(IDBTransaction* aTransaction)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
if (aTransaction) {
NS_ASSERTION(!gCurrentTransaction, "Overwriting current transaction!");
}
gCurrentTransaction = aTransaction;
}
nsresult
AsyncConnectionHelper::Init()
{

View File

@ -87,6 +87,7 @@ public:
}
static IDBTransaction* GetCurrentTransaction();
static void SetCurrentTransaction(IDBTransaction* aTransaction);
nsISupports* GetSource()
{

View File

@ -811,9 +811,15 @@ ContinueRunnable::Run()
return NS_ERROR_FAILURE;
}
AsyncConnectionHelper::SetCurrentTransaction(cursor->mTransaction);
PRBool dummy;
cursor->mRequest->DispatchEvent(event, &dummy);
NS_ASSERTION(AsyncConnectionHelper::GetCurrentTransaction() ==
cursor->mTransaction, "Should be unchanged!");
AsyncConnectionHelper::SetCurrentTransaction(nsnull);
cursor->mTransaction->OnRequestFinished();
return NS_OK;
}

View File

@ -636,7 +636,7 @@ IDBDatabase::SetVersion(const nsAString& aVersion,
nsTArray<nsString> storesToOpen;
nsRefPtr<IDBTransaction> transaction =
IDBTransaction::Create(this, storesToOpen, IDBTransaction::VERSION_CHANGE,
kDefaultDatabaseTimeoutSeconds);
kDefaultDatabaseTimeoutSeconds, true);
NS_ENSURE_TRUE(transaction, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
nsRefPtr<IDBVersionChangeRequest> request =

View File

@ -48,6 +48,7 @@
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
#include "AsyncConnectionHelper.h"
#include "DatabaseInfo.h"
#include "IDBCursor.h"
#include "IDBEvents.h"
@ -61,6 +62,8 @@ USING_INDEXEDDB_NAMESPACE
namespace {
IDBTransaction::ThreadObserver* gThreadObserver = nsnull;
PLDHashOperator
DoomCachedStatements(const nsACString& aQuery,
nsCOMPtr<mozIStorageStatement>& aStatement,
@ -78,7 +81,8 @@ already_AddRefed<IDBTransaction>
IDBTransaction::Create(IDBDatabase* aDatabase,
nsTArray<nsString>& aObjectStoreNames,
PRUint16 aMode,
PRUint32 aTimeout)
PRUint32 aTimeout,
bool aDispatchDelayed)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
@ -101,6 +105,13 @@ IDBTransaction::Create(IDBDatabase* aDatabase,
return nsnull;
}
if (!aDispatchDelayed) {
if (!ThreadObserver::BeginObserving(transaction)) {
return nsnull;
}
transaction->mCreating = true;
}
return transaction.forget();
}
@ -111,7 +122,7 @@ IDBTransaction::IDBTransaction()
mPendingRequests(0),
mSavepointCount(0),
mAborted(false),
mClosed(false)
mCreating(false)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
}
@ -122,6 +133,7 @@ IDBTransaction::~IDBTransaction()
NS_ASSERTION(!mPendingRequests, "Should have no pending requests here!");
NS_ASSERTION(!mSavepointCount, "Should have released them all!");
NS_ASSERTION(!mConnection, "Should have called CommitOrRollback!");
NS_ASSERTION(!mCreating, "Should have been cleared already!");
if (mListenerManager) {
mListenerManager->Disconnect();
@ -150,10 +162,6 @@ IDBTransaction::OnRequestFinished()
if (!mAborted) {
NS_ASSERTION(mReadyState == nsIIDBTransaction::LOADING, "Bad state!");
}
NS_ASSERTION(!mClosed, "Shouldn't be closed yet!");
mClosed = true;
CommitOrRollback();
}
}
@ -533,16 +541,35 @@ IDBTransaction::GetCachedStatement(const nsACString& aQuery)
return stmt.forget();
}
#ifdef DEBUG
bool
IDBTransaction::TransactionIsOpen() const
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
return (mReadyState == nsIIDBTransaction::INITIAL ||
mReadyState == nsIIDBTransaction::LOADING) &&
!mClosed;
// If we haven't started anything then we're open.
if (mReadyState == nsIIDBTransaction::INITIAL) {
NS_ASSERTION(AsyncConnectionHelper::GetCurrentTransaction() != this,
"This should be some other transaction (or null)!");
return true;
}
// If we've already started then we need to check to see if we still have the
// mCreating flag set. If we do (i.e. we haven't returned to the event loop
// from the time we were created) then we are open. Otherwise check the
// currently running transaction to see if it's the same. We only allow other
// requests to be made if this transaction is currently running.
if (mReadyState == nsIIDBTransaction::LOADING) {
if (mCreating) {
return true;
}
if (AsyncConnectionHelper::GetCurrentTransaction() == this) {
return true;
}
}
return false;
}
#endif
already_AddRefed<IDBObjectStore>
IDBTransaction::GetOrCreateObjectStore(const nsAString& aName,
@ -792,6 +819,234 @@ IDBTransaction::PreHandleEvent(nsEventChainPreVisitor& aVisitor)
return NS_OK;
}
IDBTransaction::
ThreadObserver::ThreadObserver()
: mBaseRecursionDepth(0),
mDone(false)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
NS_ASSERTION(!gThreadObserver, "Multiple observers?!");
}
IDBTransaction::
ThreadObserver::~ThreadObserver()
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
NS_ASSERTION(gThreadObserver == this, "Multiple observers?!");
#ifdef DEBUG
for (PRUint32 i = 0; i < mTransactions.Length(); i++) {
NS_ASSERTION(mTransactions[i].transactions.IsEmpty(),
"Unprocessed transactions!");
}
#endif
// Clear the global.
gThreadObserver = nsnull;
}
void
IDBTransaction::
ThreadObserver::UpdateNewlyCreatedTransactions(PRUint32 aRecursionDepth)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
for (PRUint32 i = 0; i < mTransactions.Length(); i++) {
TransactionInfo& info = mTransactions[i];
if (info.recursionDepth == aRecursionDepth) {
for (PRUint32 j = 0; j < info.transactions.Length(); j++) {
nsRefPtr<IDBTransaction>& transaction = info.transactions[j];
// Clear the mCreating flag now.
transaction->mCreating = false;
// And maybe set the readyState to DONE if there were no requests
// generated.
if (transaction->mReadyState == nsIIDBTransaction::INITIAL) {
transaction->mReadyState = nsIIDBTransaction::DONE;
}
}
// Don't hang on to transactions any longer than we have to.
info.transactions.Clear();
break;
}
}
}
// static
bool
IDBTransaction::
ThreadObserver::BeginObserving(IDBTransaction* aTransaction)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
NS_ASSERTION(aTransaction, "Null pointer!");
nsCOMPtr<nsIThreadInternal2> thread(do_QueryInterface(NS_GetCurrentThread()));
NS_ENSURE_TRUE(thread, false);
// We need the current recursion depth first.
PRUint32 depth;
nsresult rv = thread->GetRecursionDepth(&depth);
NS_ENSURE_SUCCESS(rv, false);
NS_ASSERTION(depth, "This should never be 0!");
depth--;
// If we've already got an observer created then simply append this
// transaction to its list.
if (gThreadObserver) {
for (PRUint32 i = 0; i < gThreadObserver->mTransactions.Length(); i++) {
TransactionInfo& info = gThreadObserver->mTransactions[i];
if (info.recursionDepth == depth) {
if (!info.transactions.AppendElement(aTransaction)) {
NS_WARNING("Out of memory!");
return false;
}
return true;
}
}
// No transactions at this depth yet, make a new entry
TransactionInfo* newInfo = gThreadObserver->mTransactions.AppendElement();
if (!newInfo || !newInfo->transactions.AppendElement(aTransaction)) {
NS_WARNING("Out of memory!");
return false;
}
newInfo->recursionDepth = depth;
return true;
}
// Make a new thread observer and install it.
nsRefPtr<ThreadObserver> observer(new ThreadObserver());
TransactionInfo* info = observer->mTransactions.AppendElement();
NS_ASSERTION(info, "This should never fail!");
info->recursionDepth = observer->mBaseRecursionDepth = depth;
if (!info->transactions.AppendElement(aTransaction)) {
NS_WARNING("Out of memory!");
return false;
}
// We need to keep the thread observer chain intact so grab the previous
// observer.
rv = thread->GetObserver(getter_AddRefs(observer->mPreviousObserver));
NS_ENSURE_SUCCESS(rv, false);
// Now set our new observer.
rv = thread->SetObserver(observer);
NS_ENSURE_SUCCESS(rv, false);
// And set the global so that we don't recreate it later.
gThreadObserver = observer;
return true;
}
NS_IMPL_THREADSAFE_ISUPPORTS1(IDBTransaction::ThreadObserver, nsIThreadObserver)
NS_IMETHODIMP
IDBTransaction::
ThreadObserver::OnDispatchedEvent(nsIThreadInternal* aThread)
{
// This may be called on any thread!
// Nothing special is needed here, just call the previous observer.
if (mPreviousObserver) {
return mPreviousObserver->OnDispatchedEvent(aThread);
}
return NS_OK;
}
NS_IMETHODIMP
IDBTransaction::
ThreadObserver::OnProcessNextEvent(nsIThreadInternal* aThread,
PRBool aMayWait,
PRUint32 aRecursionDepth)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
NS_ASSERTION(aThread, "This should never be null!");
NS_ASSERTION(!mKungFuDeathGrip, "Shouldn't have a self-ref here!");
// If we're at the base recursion depth here then we're ready to unset
// ourselves as the thread observer.
if (aRecursionDepth == mBaseRecursionDepth || mDone) {
// From here on we'll continue to try to unset ourselves.
mDone = true;
nsCOMPtr<nsIThreadObserver> currentObserver;
if (NS_FAILED(aThread->GetObserver(getter_AddRefs(currentObserver)))) {
NS_WARNING("Can't get current observer?!");
}
// We can only set the previous observer if this is the current observer.
// Otherwise someone else has installed themselves into the chain and we
// have to hang around until they unset themselves.
if (currentObserver == this) {
// Setting a different thread observer could delete us. Maintain a
// reference until AfterProcessNextEvent is called.
mKungFuDeathGrip = this;
// Set our previous observer back on the thread.
if (NS_FAILED(aThread->SetObserver(mPreviousObserver))) {
NS_ERROR("This should never fail!");
}
}
}
// Take care of any transactions that were created at this recursion depth.
UpdateNewlyCreatedTransactions(aRecursionDepth);
// And call the previous observer.
if (mPreviousObserver) {
return mPreviousObserver->OnProcessNextEvent(aThread, aMayWait,
aRecursionDepth);
}
return NS_OK;
}
NS_IMETHODIMP
IDBTransaction::
ThreadObserver::AfterProcessNextEvent(nsIThreadInternal* aThread,
PRUint32 aRecursionDepth)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
NS_ASSERTION(aThread, "This should never be null!");
nsRefPtr<ThreadObserver> kungFuDeathGrip;
nsCOMPtr<nsIThreadObserver> observer;
if (mKungFuDeathGrip) {
NS_ASSERTION(mDone, "Huh?!");
// We can drop the reference to this observer after this call.
kungFuDeathGrip.swap(mKungFuDeathGrip);
// And we don't need the previous observer after this call either.
observer.swap(mPreviousObserver);
}
else {
// Still call the previous observer.
observer = mPreviousObserver;
}
// We may have collected more transactions while the event was processed.
// Update them now.
UpdateNewlyCreatedTransactions(aRecursionDepth);
if (observer) {
return observer->AfterProcessNextEvent(aThread, aRecursionDepth);
}
return NS_OK;
}
CommitHelper::CommitHelper(IDBTransaction* aTransaction)
: mTransaction(aTransaction),
mAborted(!!aTransaction->mAborted),
@ -809,8 +1064,6 @@ NS_IMPL_THREADSAFE_ISUPPORTS1(CommitHelper, nsIRunnable)
NS_IMETHODIMP
CommitHelper::Run()
{
NS_ASSERTION(mTransaction->mClosed, "Should be closed!");
if (NS_IsMainThread()) {
NS_ASSERTION(mDoomedObjects.IsEmpty(), "Didn't release doomed objects!");

View File

@ -45,6 +45,7 @@
#include "nsIIDBTransaction.h"
#include "nsIRunnable.h"
#include "nsIThreadInternal.h"
#include "nsDOMEventTargetHelper.h"
#include "nsCycleCollectionParticipant.h"
@ -69,6 +70,7 @@ class IDBTransaction : public nsDOMEventTargetHelper,
{
friend class AsyncConnectionHelper;
friend class CommitHelper;
friend class ThreadObserver;
friend class TransactionThreadPool;
public:
@ -82,7 +84,8 @@ public:
Create(IDBDatabase* aDatabase,
nsTArray<nsString>& aObjectStoreNames,
PRUint16 aMode,
PRUint32 aTimeout);
PRUint32 aTimeout,
bool aDispatchDelayed = false);
// nsPIDOMEventTarget
virtual nsresult PreHandleEvent(nsEventChainPreVisitor& aVisitor);
@ -133,16 +136,7 @@ public:
return GetCachedStatement(query);
}
#ifdef DEBUG
bool TransactionIsOpen() const;
#else
bool TransactionIsOpen() const
{
return (mReadyState == nsIIDBTransaction::INITIAL ||
mReadyState == nsIIDBTransaction::LOADING) &&
!mClosed;
}
#endif
bool IsWriteAllowed() const
{
@ -165,6 +159,35 @@ public:
GetOrCreateObjectStore(const nsAString& aName,
ObjectStoreInfo* aObjectStoreInfo);
class ThreadObserver : public nsIThreadObserver
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSITHREADOBSERVER
static bool BeginObserving(IDBTransaction* aTransaction);
private:
ThreadObserver();
~ThreadObserver();
void UpdateNewlyCreatedTransactions(PRUint32 aRecursionDepth);
struct TransactionInfo
{
PRUint32 recursionDepth;
nsTArray<nsRefPtr<IDBTransaction> > transactions;
};
nsAutoTArray<TransactionInfo, 1> mTransactions;
nsCOMPtr<nsIThreadObserver> mPreviousObserver;
nsRefPtr<ThreadObserver> mKungFuDeathGrip;
PRUint32 mBaseRecursionDepth;
bool mDone;
};
private:
IDBTransaction();
~IDBTransaction();
@ -196,7 +219,7 @@ private:
nsTArray<nsRefPtr<IDBObjectStore> > mCreatedObjectStores;
bool mAborted;
bool mClosed;
bool mCreating;
};
class CommitHelper : public nsIRunnable

View File

@ -78,6 +78,8 @@ TEST_FILES = \
test_remove_objectStore.html \
test_request_readyState.html \
test_transaction_abort.html \
test_transaction_lifetimes.html \
test_transaction_lifetimes_nested.html \
test_setVersion.html \
test_setVersion_abort.html \
test_setVersion_events.html \

View File

@ -0,0 +1,55 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<html>
<head>
<title>Indexed Database Property Test</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="text/javascript;version=1.7">
function testSteps()
{
let request = moz_indexedDB.open(window.location.pathname);
request.onerror = errorHandler;
request.onsuccess = grabEventAndContinueHandler;
let event = yield;
let db = event.result;
db.onerror = errorHandler;
db.setVersion("1").onsuccess = grabEventAndContinueHandler;
let event = yield;
event.transaction.oncomplete = continueToNextStep;
db.createObjectStore("foo", "", true);
yield;
let transaction = db.transaction("foo");
continueToNextStep();
yield;
try {
transaction.objectStore("foo");
ok(false, "Should have thrown!");
}
catch (e) {
ok(e instanceof IDBDatabaseException, "Got database exception.");
is(e.code, IDBDatabaseException.NOT_ALLOWED_ERR, "Good error code.");
}
finishTest();
yield;
}
</script>
<script type="text/javascript;version=1.7" src="helpers.js"></script>
</head>
<body onload="runTest();"></body>
</html>

View File

@ -0,0 +1,79 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<html>
<head>
<title>Indexed Database Property Test</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="text/javascript;version=1.7">
function testSteps()
{
let request = moz_indexedDB.open(window.location.pathname);
request.onerror = errorHandler;
request.onsuccess = grabEventAndContinueHandler;
let event = yield;
let db = event.result;
db.onerror = errorHandler;
db.setVersion("1").onsuccess = grabEventAndContinueHandler;
event = yield;
event.transaction.oncomplete = continueToNextStep;
db.createObjectStore("foo", "");
yield;
let transaction1 = db.transaction("foo");
is(transaction1.readyState, IDBTransaction.INITIAL, "Correct readyState");
let transaction2;
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
let thread = Components.classes["@mozilla.org/thread-manager;1"]
.getService()
.currentThread;
let eventHasRun;
thread.dispatch(function() {
eventHasRun = true;
is(transaction1.readyState, IDBTransaction.INITIAL,
"Correct readyState");
transaction2 = db.transaction("foo");
is(transaction2.readyState, IDBTransaction.INITIAL,
"Correct readyState");
}, Components.interfaces.nsIThread.DISPATCH_NORMAL);
while (!eventHasRun) {
thread.processNextEvent(false);
}
is(transaction1.readyState, IDBTransaction.INITIAL, "Correct readyState");
ok(transaction2, "Non-null transaction2");
is(transaction2.readyState, IDBTransaction.DONE, "Correct readyState");
continueToNextStep();
yield;
is(transaction1.readyState, IDBTransaction.DONE, "Correct readyState");
finishTest();
yield;
}
</script>
<script type="text/javascript;version=1.7" src="helpers.js"></script>
</head>
<body onload="runTest();"></body>
</html>

View File

@ -162,3 +162,16 @@ interface nsIThreadEventFilter : nsISupports
*/
[notxpcom] boolean acceptEvent(in nsIRunnable event);
};
/**
* Temporary interface, will be merged into nsIThreadInternal.
*/
[scriptable, uuid(718e9346-74cb-4859-8bcc-c9ec37bfb668)]
interface nsIThreadInternal2 : nsIThreadInternal
{
/**
* The current recursion depth, 0 when no events are running, 1 when a single
* event is running, and higher when nested events are running.
*/
readonly attribute unsigned long recursionDepth;
};

View File

@ -157,6 +157,7 @@ NS_IMPL_THREADSAFE_RELEASE(nsThread)
NS_INTERFACE_MAP_BEGIN(nsThread)
NS_INTERFACE_MAP_ENTRY(nsIThread)
NS_INTERFACE_MAP_ENTRY(nsIThreadInternal)
NS_INTERFACE_MAP_ENTRY(nsIThreadInternal2)
NS_INTERFACE_MAP_ENTRY(nsIEventTarget)
NS_INTERFACE_MAP_ENTRY(nsISupportsPriority)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIThread)
@ -737,6 +738,17 @@ nsThread::nsChainedEventQueue::PutEvent(nsIRunnable *event)
return val;
}
//-----------------------------------------------------------------------------
// nsIThreadInternal2
NS_IMETHODIMP
nsThread::GetRecursionDepth(PRUint32 *depth)
{
NS_ENSURE_ARG_POINTER(depth);
*depth = mRunningEvent;
return NS_OK;
}
//-----------------------------------------------------------------------------
NS_IMETHODIMP

View File

@ -48,13 +48,14 @@
#include "nsAutoPtr.h"
// A native thread
class nsThread : public nsIThreadInternal, public nsISupportsPriority
class nsThread : public nsIThreadInternal2, public nsISupportsPriority
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIEVENTTARGET
NS_DECL_NSITHREAD
NS_DECL_NSITHREADINTERNAL
NS_DECL_NSITHREADINTERNAL2
NS_DECL_NSISUPPORTSPRIORITY
nsThread();