diff -r 000000000000 -r 6474c204b198 dom/indexedDB/IDBTransaction.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/indexedDB/IDBTransaction.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1275 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/basictypes.h" + +#include "IDBTransaction.h" + +#include "nsIAppShell.h" +#include "nsIScriptContext.h" + +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/storage.h" +#include "nsDOMClassInfoID.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/EventDispatcher.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsWidgetsCID.h" + +#include "AsyncConnectionHelper.h" +#include "DatabaseInfo.h" +#include "IDBCursor.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBObjectStore.h" +#include "IndexedDatabaseManager.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "TransactionThreadPool.h" + +#include "ipc/IndexedDBChild.h" + +#define SAVEPOINT_NAME "savepoint" + +using namespace mozilla; +using namespace mozilla::dom; +USING_INDEXEDDB_NAMESPACE +using mozilla::dom::quota::QuotaManager; +using mozilla::ErrorResult; + +namespace { + +NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID); + +#ifdef MOZ_ENABLE_PROFILER_SPS +uint64_t gNextTransactionSerialNumber = 1; +#endif + +PLDHashOperator +DoomCachedStatements(const nsACString& aQuery, + nsCOMPtr& aStatement, + void* aUserArg) +{ + CommitHelper* helper = static_cast(aUserArg); + helper->AddDoomedObject(aStatement); + return PL_DHASH_REMOVE; +} + +// This runnable doesn't actually do anything beyond "prime the pump" and get +// transactions in the right order on the transaction thread pool. +class StartTransactionRunnable : public nsIRunnable +{ +public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Run() + { + // NOP + return NS_OK; + } +}; + +// Could really use those NS_REFCOUNTING_HAHA_YEAH_RIGHT macros here. +NS_IMETHODIMP_(MozExternalRefCountType) StartTransactionRunnable::AddRef() +{ + return 2; +} + +NS_IMETHODIMP_(MozExternalRefCountType) StartTransactionRunnable::Release() +{ + return 1; +} + +NS_IMPL_QUERY_INTERFACE(StartTransactionRunnable, nsIRunnable) + +} // anonymous namespace + + +// static +already_AddRefed +IDBTransaction::CreateInternal(IDBDatabase* aDatabase, + const Sequence& aObjectStoreNames, + Mode aMode, + bool aDispatchDelayed, + bool aIsVersionChangeTransactionChild) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(IndexedDatabaseManager::IsMainProcess() || !aDispatchDelayed, + "No support for delayed-dispatch transactions in child " + "process!"); + NS_ASSERTION(!aIsVersionChangeTransactionChild || + (!IndexedDatabaseManager::IsMainProcess() && + aMode == IDBTransaction::VERSION_CHANGE), + "Busted logic!"); + + nsRefPtr transaction = new IDBTransaction(aDatabase); + + transaction->SetScriptOwner(aDatabase->GetScriptOwner()); + transaction->mDatabase = aDatabase; + transaction->mMode = aMode; + transaction->mDatabaseInfo = aDatabase->Info(); + transaction->mObjectStoreNames.AppendElements(aObjectStoreNames); + transaction->mObjectStoreNames.Sort(); + + IndexedDBTransactionChild* actor = nullptr; + + if (IndexedDatabaseManager::IsMainProcess()) { + if (aMode != IDBTransaction::VERSION_CHANGE) { + TransactionThreadPool* pool = TransactionThreadPool::GetOrCreate(); + NS_ENSURE_TRUE(pool, nullptr); + + static StartTransactionRunnable sStartTransactionRunnable; + pool->Dispatch(transaction, &sStartTransactionRunnable, false, nullptr); + } + } + else if (!aIsVersionChangeTransactionChild) { + IndexedDBDatabaseChild* dbActor = aDatabase->GetActorChild(); + NS_ASSERTION(dbActor, "Must have an actor here!"); + + ipc::NormalTransactionParams params; + params.names().AppendElements(aObjectStoreNames); + params.mode() = aMode; + + actor = new IndexedDBTransactionChild(); + + dbActor->SendPIndexedDBTransactionConstructor(actor, params); + } + + if (!aDispatchDelayed) { + nsCOMPtr appShell = do_GetService(kAppShellCID); + NS_ENSURE_TRUE(appShell, nullptr); + + nsresult rv = appShell->RunBeforeNextEvent(transaction); + NS_ENSURE_SUCCESS(rv, nullptr); + + transaction->mCreating = true; + } + + if (actor) { + NS_ASSERTION(!IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); + actor->SetTransaction(transaction); + } + + return transaction.forget(); +} + +IDBTransaction::IDBTransaction(IDBDatabase* aDatabase) +: IDBWrapperCache(aDatabase), + mReadyState(IDBTransaction::INITIAL), + mMode(IDBTransaction::READ_ONLY), + mPendingRequests(0), + mSavepointCount(0), + mActorChild(nullptr), + mActorParent(nullptr), + mAbortCode(NS_OK), +#ifdef MOZ_ENABLE_PROFILER_SPS + mSerialNumber(gNextTransactionSerialNumber++), +#endif + mCreating(false) +#ifdef DEBUG + , mFiredCompleteOrAbort(false) +#endif +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); +} + +IDBTransaction::~IDBTransaction() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + 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!"); + NS_ASSERTION(mFiredCompleteOrAbort, "Should have fired event!"); + + NS_ASSERTION(!mActorParent, "Actor parent owns us, how can we be dying?!"); + if (mActorChild) { + NS_ASSERTION(!IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); + mActorChild->Send__delete__(mActorChild); + NS_ASSERTION(!mActorChild, "Should have cleared in Send__delete__!"); + } +} + +void +IDBTransaction::OnNewRequest() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + if (!mPendingRequests) { + NS_ASSERTION(mReadyState == IDBTransaction::INITIAL, + "Reusing a transaction!"); + mReadyState = IDBTransaction::LOADING; + } + ++mPendingRequests; +} + +void +IDBTransaction::OnRequestFinished() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(mPendingRequests, "Mismatched calls!"); + --mPendingRequests; + if (!mPendingRequests) { + NS_ASSERTION(NS_FAILED(mAbortCode) || mReadyState == IDBTransaction::LOADING, + "Bad state!"); + mReadyState = IDBTransaction::COMMITTING; + CommitOrRollback(); + } +} + +void +IDBTransaction::OnRequestDisconnected() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(mPendingRequests, "Mismatched calls!"); + --mPendingRequests; +} + +void +IDBTransaction::RemoveObjectStore(const nsAString& aName) +{ + NS_ASSERTION(mMode == IDBTransaction::VERSION_CHANGE, + "Only remove object stores on VERSION_CHANGE transactions"); + + mDatabaseInfo->RemoveObjectStore(aName); + + for (uint32_t i = 0; i < mCreatedObjectStores.Length(); i++) { + if (mCreatedObjectStores[i]->Name() == aName) { + nsRefPtr objectStore = mCreatedObjectStores[i]; + mCreatedObjectStores.RemoveElementAt(i); + mDeletedObjectStores.AppendElement(objectStore); + break; + } + } +} + +void +IDBTransaction::SetTransactionListener(IDBTransactionListener* aListener) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!mListener, "Shouldn't already have a listener!"); + mListener = aListener; +} + +nsresult +IDBTransaction::CommitOrRollback() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (!IndexedDatabaseManager::IsMainProcess()) { + if (mActorChild) { + mActorChild->SendAllRequestsFinished(); + } + + return NS_OK; + } + + nsRefPtr helper = + new CommitHelper(this, mListener, mCreatedObjectStores); + + TransactionThreadPool* pool = TransactionThreadPool::GetOrCreate(); + NS_ENSURE_STATE(pool); + + mCachedStatements.Enumerate(DoomCachedStatements, helper); + NS_ASSERTION(!mCachedStatements.Count(), "Statements left!"); + + nsresult rv = pool->Dispatch(this, helper, true, helper); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool +IDBTransaction::StartSavepoint() +{ + NS_PRECONDITION(!NS_IsMainThread(), "Wrong thread!"); + NS_PRECONDITION(mConnection, "No connection!"); + + nsCOMPtr stmt = GetCachedStatement(NS_LITERAL_CSTRING( + "SAVEPOINT " SAVEPOINT_NAME + )); + NS_ENSURE_TRUE(stmt, false); + + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, false); + + if (IsWriteAllowed()) { + mUpdateFileRefcountFunction->StartSavepoint(); + } + + ++mSavepointCount; + + return true; +} + +nsresult +IDBTransaction::ReleaseSavepoint() +{ + NS_PRECONDITION(!NS_IsMainThread(), "Wrong thread!"); + NS_PRECONDITION(mConnection, "No connection!"); + + NS_ASSERTION(mSavepointCount, "Mismatch!"); + + nsCOMPtr stmt = GetCachedStatement(NS_LITERAL_CSTRING( + "RELEASE SAVEPOINT " SAVEPOINT_NAME + )); + NS_ENSURE_TRUE(stmt, NS_OK); + + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, NS_OK); + + if (IsWriteAllowed()) { + mUpdateFileRefcountFunction->ReleaseSavepoint(); + } + + --mSavepointCount; + + return NS_OK; +} + +void +IDBTransaction::RollbackSavepoint() +{ + NS_PRECONDITION(!NS_IsMainThread(), "Wrong thread!"); + NS_PRECONDITION(mConnection, "No connection!"); + + NS_ASSERTION(mSavepointCount == 1, "Mismatch!"); + mSavepointCount = 0; + + nsCOMPtr stmt = GetCachedStatement(NS_LITERAL_CSTRING( + "ROLLBACK TO SAVEPOINT " SAVEPOINT_NAME + )); + NS_ENSURE_TRUE_VOID(stmt); + + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS_VOID(rv); + + if (IsWriteAllowed()) { + mUpdateFileRefcountFunction->RollbackSavepoint(); + } +} + +nsresult +IDBTransaction::GetOrCreateConnection(mozIStorageConnection** aResult) +{ + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); + + PROFILER_LABEL("IndexedDB", "IDBTransaction::GetOrCreateConnection"); + + if (mDatabase->IsInvalidated()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!mConnection) { + nsCOMPtr connection = + IDBFactory::GetConnection(mDatabase->FilePath(), mDatabase->Type(), + mDatabase->Group(), mDatabase->Origin()); + NS_ENSURE_TRUE(connection, NS_ERROR_FAILURE); + + nsresult rv; + + nsRefPtr function; + nsCString beginTransaction; + if (mMode != IDBTransaction::READ_ONLY) { + function = new UpdateRefcountFunction(Database()->Manager()); + NS_ENSURE_TRUE(function, NS_ERROR_OUT_OF_MEMORY); + + rv = connection->CreateFunction( + NS_LITERAL_CSTRING("update_refcount"), 2, function); + NS_ENSURE_SUCCESS(rv, rv); + + beginTransaction.AssignLiteral("BEGIN IMMEDIATE TRANSACTION;"); + } + else { + beginTransaction.AssignLiteral("BEGIN TRANSACTION;"); + } + + nsCOMPtr stmt; + rv = connection->CreateStatement(beginTransaction, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + function.swap(mUpdateFileRefcountFunction); + connection.swap(mConnection); + } + + nsCOMPtr result(mConnection); + result.forget(aResult); + return NS_OK; +} + +already_AddRefed +IDBTransaction::GetCachedStatement(const nsACString& aQuery) +{ + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!aQuery.IsEmpty(), "Empty sql statement!"); + NS_ASSERTION(mConnection, "No connection!"); + + nsCOMPtr stmt; + + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + nsresult rv = mConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); +#ifdef DEBUG + if (NS_FAILED(rv)) { + nsCString error; + error.AppendLiteral("The statement `"); + error.Append(aQuery); + error.AppendLiteral("` failed to compile with the error message `"); + nsCString msg; + (void)mConnection->GetLastErrorString(msg); + error.Append(msg); + error.AppendLiteral("`."); + NS_ERROR(error.get()); + } +#endif + NS_ENSURE_SUCCESS(rv, nullptr); + + mCachedStatements.Put(aQuery, stmt); + } + + return stmt.forget(); +} + +bool +IDBTransaction::IsOpen() const +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // If we haven't started anything then we're open. + if (mReadyState == IDBTransaction::INITIAL) { + 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 == IDBTransaction::LOADING) { + if (mCreating) { + return true; + } + + if (AsyncConnectionHelper::GetCurrentTransaction() == this) { + return true; + } + } + + return false; +} + +already_AddRefed +IDBTransaction::GetOrCreateObjectStore(const nsAString& aName, + ObjectStoreInfo* aObjectStoreInfo, + bool aCreating) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aObjectStoreInfo, "Null pointer!"); + NS_ASSERTION(!aCreating || GetMode() == IDBTransaction::VERSION_CHANGE, + "How else can we create here?!"); + + nsRefPtr retval; + + for (uint32_t index = 0; index < mCreatedObjectStores.Length(); index++) { + nsRefPtr& objectStore = mCreatedObjectStores[index]; + if (objectStore->Name() == aName) { + retval = objectStore; + return retval.forget(); + } + } + + retval = IDBObjectStore::Create(this, aObjectStoreInfo, mDatabaseInfo->id, + aCreating); + + mCreatedObjectStores.AppendElement(retval); + + return retval.forget(); +} + +already_AddRefed +IDBTransaction::GetFileInfo(nsIDOMBlob* aBlob) +{ + nsRefPtr fileInfo; + mCreatedFileInfos.Get(aBlob, getter_AddRefs(fileInfo)); + return fileInfo.forget(); +} + +void +IDBTransaction::AddFileInfo(nsIDOMBlob* aBlob, FileInfo* aFileInfo) +{ + mCreatedFileInfos.Put(aBlob, aFileInfo); +} + +void +IDBTransaction::ClearCreatedFileInfos() +{ + mCreatedFileInfos.Clear(); +} + +nsresult +IDBTransaction::AbortInternal(nsresult aAbortCode, + already_AddRefed aError) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsRefPtr error = aError; + + if (IsFinished()) { + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + if (mActorChild) { + NS_ASSERTION(!IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); + mActorChild->SendAbort(aAbortCode); + } + + bool needToCommitOrRollback = mReadyState == IDBTransaction::INITIAL; + + mAbortCode = aAbortCode; + mReadyState = IDBTransaction::DONE; + mError = error.forget(); + + if (GetMode() == IDBTransaction::VERSION_CHANGE) { + // If a version change transaction is aborted, we must revert the world + // back to its previous state. + mDatabase->RevertToPreviousState(); + + DatabaseInfo* dbInfo = mDatabase->Info(); + + for (uint32_t i = 0; i < mCreatedObjectStores.Length(); i++) { + nsRefPtr& objectStore = mCreatedObjectStores[i]; + ObjectStoreInfo* info = dbInfo->GetObjectStore(objectStore->Name()); + + if (!info) { + info = new ObjectStoreInfo(*objectStore->Info()); + info->indexes.Clear(); + } + + objectStore->SetInfo(info); + } + + for (uint32_t i = 0; i < mDeletedObjectStores.Length(); i++) { + nsRefPtr& objectStore = mDeletedObjectStores[i]; + ObjectStoreInfo* info = dbInfo->GetObjectStore(objectStore->Name()); + + if (!info) { + info = new ObjectStoreInfo(*objectStore->Info()); + info->indexes.Clear(); + } + + objectStore->SetInfo(info); + } + + // and then the db must be closed + mDatabase->Close(); + } + + // Fire the abort event if there are no outstanding requests. Otherwise the + // abort event will be fired when all outstanding requests finish. + if (needToCommitOrRollback) { + return CommitOrRollback(); + } + + return NS_OK; +} + +nsresult +IDBTransaction::Abort(IDBRequest* aRequest) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aRequest, "This is undesirable."); + + ErrorResult rv; + nsRefPtr error = aRequest->GetError(rv); + + return AbortInternal(aRequest->GetErrorCode(), error.forget()); +} + +nsresult +IDBTransaction::Abort(nsresult aErrorCode) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsRefPtr error = new DOMError(GetOwner(), aErrorCode); + return AbortInternal(aErrorCode, error.forget()); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBTransaction) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBTransaction, + IDBWrapperCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDatabase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCreatedObjectStores) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeletedObjectStores) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBTransaction, IDBWrapperCache) + // Don't unlink mDatabase! + NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCreatedObjectStores) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDeletedObjectStores) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBTransaction) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) +NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache) + +NS_IMPL_ADDREF_INHERITED(IDBTransaction, IDBWrapperCache) +NS_IMPL_RELEASE_INHERITED(IDBTransaction, IDBWrapperCache) + +JSObject* +IDBTransaction::WrapObject(JSContext* aCx) +{ + return IDBTransactionBinding::Wrap(aCx, this); +} + +mozilla::dom::IDBTransactionMode +IDBTransaction::GetMode(ErrorResult& aRv) const +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + switch (mMode) { + case READ_ONLY: + return mozilla::dom::IDBTransactionMode::Readonly; + + case READ_WRITE: + return mozilla::dom::IDBTransactionMode::Readwrite; + + case VERSION_CHANGE: + return mozilla::dom::IDBTransactionMode::Versionchange; + + case MODE_INVALID: + default: + aRv.Throw(NS_ERROR_UNEXPECTED); + return mozilla::dom::IDBTransactionMode::Readonly; + } +} + +DOMError* +IDBTransaction::GetError(ErrorResult& aRv) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + return mError; +} + +already_AddRefed +IDBTransaction::GetObjectStoreNames(ErrorResult& aRv) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsRefPtr list(new DOMStringList()); + + if (mMode == IDBTransaction::VERSION_CHANGE) { + mDatabaseInfo->GetObjectStoreNames(list->StringArray()); + } + else { + list->StringArray() = mObjectStoreNames; + } + + return list.forget(); +} + +already_AddRefed +IDBTransaction::ObjectStore(const nsAString& aName, ErrorResult& aRv) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (IsFinished()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + ObjectStoreInfo* info = nullptr; + + if (mMode == IDBTransaction::VERSION_CHANGE || + mObjectStoreNames.Contains(aName)) { + info = mDatabaseInfo->GetObjectStore(aName); + } + + if (!info) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return nullptr; + } + + nsRefPtr objectStore = + GetOrCreateObjectStore(aName, info, false); + if (!objectStore) { + IDB_WARNING("Failed to get or create object store!"); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + return objectStore.forget(); +} + +nsresult +IDBTransaction::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = mDatabase; + return NS_OK; +} + +NS_IMETHODIMP +IDBTransaction::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // We're back at the event loop, no longer newborn. + mCreating = false; + + // Maybe set the readyState to DONE if there were no requests generated. + if (mReadyState == IDBTransaction::INITIAL) { + mReadyState = IDBTransaction::DONE; + + if (NS_FAILED(CommitOrRollback())) { + NS_WARNING("Failed to commit!"); + } + } + + return NS_OK; +} + +CommitHelper::CommitHelper( + IDBTransaction* aTransaction, + IDBTransactionListener* aListener, + const nsTArray >& aUpdatedObjectStores) +: mTransaction(aTransaction), + mListener(aListener), + mAbortCode(aTransaction->mAbortCode) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + mConnection.swap(aTransaction->mConnection); + mUpdateFileRefcountFunction.swap(aTransaction->mUpdateFileRefcountFunction); + + for (uint32_t i = 0; i < aUpdatedObjectStores.Length(); i++) { + ObjectStoreInfo* info = aUpdatedObjectStores[i]->Info(); + if (info->comittedAutoIncrementId != info->nextAutoIncrementId) { + mAutoIncrementObjectStores.AppendElement(aUpdatedObjectStores[i]); + } + } +} + +CommitHelper::CommitHelper(IDBTransaction* aTransaction, + nsresult aAbortCode) +: mTransaction(aTransaction), + mAbortCode(aAbortCode) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); +} + +CommitHelper::~CommitHelper() +{ +} + +NS_IMPL_ISUPPORTS(CommitHelper, nsIRunnable) + +NS_IMETHODIMP +CommitHelper::Run() +{ + if (NS_IsMainThread()) { + PROFILER_MAIN_THREAD_LABEL("IndexedDB", "CommitHelper::Run"); + + NS_ASSERTION(mDoomedObjects.IsEmpty(), "Didn't release doomed objects!"); + + mTransaction->mReadyState = IDBTransaction::DONE; + + // Release file infos on the main thread, so they will eventually get + // destroyed on correct thread. + mTransaction->ClearCreatedFileInfos(); + if (mUpdateFileRefcountFunction) { + mUpdateFileRefcountFunction->ClearFileInfoEntries(); + mUpdateFileRefcountFunction = nullptr; + } + + nsCOMPtr event; + if (NS_FAILED(mAbortCode)) { + if (mTransaction->GetMode() == IDBTransaction::VERSION_CHANGE) { + // This will make the database take a snapshot of it's DatabaseInfo + mTransaction->Database()->Close(); + // Then remove the info from the hash as it contains invalid data. + DatabaseInfo::Remove(mTransaction->Database()->Id()); + } + + event = CreateGenericEvent(mTransaction, + NS_LITERAL_STRING(ABORT_EVT_STR), + eDoesBubble, eNotCancelable); + + // The transaction may already have an error object (e.g. if one of the + // requests failed). If it doesn't, and it wasn't aborted + // programmatically, create one now. + if (!mTransaction->mError && + mAbortCode != NS_ERROR_DOM_INDEXEDDB_ABORT_ERR) { + mTransaction->mError = new DOMError(mTransaction->GetOwner(), mAbortCode); + } + } + else { + event = CreateGenericEvent(mTransaction, + NS_LITERAL_STRING(COMPLETE_EVT_STR), + eDoesNotBubble, eNotCancelable); + } + IDB_ENSURE_TRUE(event, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + + if (mListener) { + mListener->NotifyTransactionPreComplete(mTransaction); + } + + IDB_PROFILER_MARK("IndexedDB Transaction %llu: Complete (rv = %lu)", + "IDBTransaction[%llu] MT Complete", + mTransaction->GetSerialNumber(), mAbortCode); + + bool dummy; + if (NS_FAILED(mTransaction->DispatchEvent(event, &dummy))) { + NS_WARNING("Dispatch failed!"); + } + +#ifdef DEBUG + mTransaction->mFiredCompleteOrAbort = true; +#endif + + if (mListener) { + mListener->NotifyTransactionPostComplete(mTransaction); + } + + mTransaction = nullptr; + + return NS_OK; + } + + PROFILER_LABEL("IndexedDB", "CommitHelper::Run"); + + IDBDatabase* database = mTransaction->Database(); + if (database->IsInvalidated()) { + IDB_REPORT_INTERNAL_ERR(); + mAbortCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (mConnection) { + QuotaManager::SetCurrentWindow(database->GetOwner()); + + if (NS_SUCCEEDED(mAbortCode) && mUpdateFileRefcountFunction && + NS_FAILED(mUpdateFileRefcountFunction->WillCommit(mConnection))) { + IDB_REPORT_INTERNAL_ERR(); + mAbortCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (NS_SUCCEEDED(mAbortCode) && NS_FAILED(WriteAutoIncrementCounts())) { + IDB_REPORT_INTERNAL_ERR(); + mAbortCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (NS_SUCCEEDED(mAbortCode)) { + NS_NAMED_LITERAL_CSTRING(release, "COMMIT TRANSACTION"); + nsresult rv = mConnection->ExecuteSimpleSQL(release); + if (NS_SUCCEEDED(rv)) { + if (mUpdateFileRefcountFunction) { + mUpdateFileRefcountFunction->DidCommit(); + } + CommitAutoIncrementCounts(); + } + else if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + // mozstorage translates SQLITE_FULL to NS_ERROR_FILE_NO_DEVICE_SPACE, + // which we know better as NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR. + mAbortCode = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + else { + IDB_REPORT_INTERNAL_ERR(); + mAbortCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + if (NS_FAILED(mAbortCode)) { + if (mUpdateFileRefcountFunction) { + mUpdateFileRefcountFunction->DidAbort(); + } + RevertAutoIncrementCounts(); + NS_NAMED_LITERAL_CSTRING(rollback, "ROLLBACK TRANSACTION"); + if (NS_FAILED(mConnection->ExecuteSimpleSQL(rollback))) { + NS_WARNING("Failed to rollback transaction!"); + } + } + } + + mDoomedObjects.Clear(); + + if (mConnection) { + if (mUpdateFileRefcountFunction) { + nsresult rv = mConnection->RemoveFunction( + NS_LITERAL_CSTRING("update_refcount")); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to remove function!"); + } + } + + mConnection->Close(); + mConnection = nullptr; + + QuotaManager::SetCurrentWindow(nullptr); + } + + return NS_OK; +} + +nsresult +CommitHelper::WriteAutoIncrementCounts() +{ + nsCOMPtr stmt; + nsresult rv; + for (uint32_t i = 0; i < mAutoIncrementObjectStores.Length(); i++) { + ObjectStoreInfo* info = mAutoIncrementObjectStores[i]->Info(); + if (!stmt) { + rv = mConnection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE object_store SET auto_increment = :ai " + "WHERE id = :osid;"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + stmt->Reset(); + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), info->id); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("ai"), + info->nextAutoIncrementId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void +CommitHelper::CommitAutoIncrementCounts() +{ + for (uint32_t i = 0; i < mAutoIncrementObjectStores.Length(); i++) { + ObjectStoreInfo* info = mAutoIncrementObjectStores[i]->Info(); + info->comittedAutoIncrementId = info->nextAutoIncrementId; + } +} + +void +CommitHelper::RevertAutoIncrementCounts() +{ + for (uint32_t i = 0; i < mAutoIncrementObjectStores.Length(); i++) { + ObjectStoreInfo* info = mAutoIncrementObjectStores[i]->Info(); + info->nextAutoIncrementId = info->comittedAutoIncrementId; + } +} + +NS_IMPL_ISUPPORTS(UpdateRefcountFunction, mozIStorageFunction) + +NS_IMETHODIMP +UpdateRefcountFunction::OnFunctionCall(mozIStorageValueArray* aValues, + nsIVariant** _retval) +{ + *_retval = nullptr; + + uint32_t numEntries; + nsresult rv = aValues->GetNumEntries(&numEntries); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(numEntries == 2, "unexpected number of arguments"); + +#ifdef DEBUG + int32_t type1 = mozIStorageValueArray::VALUE_TYPE_NULL; + aValues->GetTypeOfIndex(0, &type1); + + int32_t type2 = mozIStorageValueArray::VALUE_TYPE_NULL; + aValues->GetTypeOfIndex(1, &type2); + + NS_ASSERTION(!(type1 == mozIStorageValueArray::VALUE_TYPE_NULL && + type2 == mozIStorageValueArray::VALUE_TYPE_NULL), + "Shouldn't be called!"); +#endif + + rv = ProcessValue(aValues, 0, eDecrement); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ProcessValue(aValues, 1, eIncrement); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +UpdateRefcountFunction::WillCommit(mozIStorageConnection* aConnection) +{ + DatabaseUpdateFunction function(aConnection, this); + + mFileInfoEntries.EnumerateRead(DatabaseUpdateCallback, &function); + + nsresult rv = function.ErrorCode(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateJournals(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +UpdateRefcountFunction::DidCommit() +{ + mFileInfoEntries.EnumerateRead(FileInfoUpdateCallback, nullptr); + + nsresult rv = RemoveJournals(mJournalsToRemoveAfterCommit); + NS_ENSURE_SUCCESS_VOID(rv); +} + +void +UpdateRefcountFunction::DidAbort() +{ + nsresult rv = RemoveJournals(mJournalsToRemoveAfterAbort); + NS_ENSURE_SUCCESS_VOID(rv); +} + +nsresult +UpdateRefcountFunction::ProcessValue(mozIStorageValueArray* aValues, + int32_t aIndex, + UpdateType aUpdateType) +{ + int32_t type; + aValues->GetTypeOfIndex(aIndex, &type); + if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { + return NS_OK; + } + + nsString ids; + aValues->GetString(aIndex, ids); + + nsTArray fileIds; + nsresult rv = IDBObjectStore::ConvertFileIdsToArray(ids, fileIds); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < fileIds.Length(); i++) { + int64_t id = fileIds.ElementAt(i); + + FileInfoEntry* entry; + if (!mFileInfoEntries.Get(id, &entry)) { + nsRefPtr fileInfo = mFileManager->GetFileInfo(id); + NS_ASSERTION(fileInfo, "Shouldn't be null!"); + + nsAutoPtr newEntry(new FileInfoEntry(fileInfo)); + mFileInfoEntries.Put(id, newEntry); + entry = newEntry.forget(); + } + + if (mInSavepoint) { + mSavepointEntriesIndex.Put(id, entry); + } + + switch (aUpdateType) { + case eIncrement: + entry->mDelta++; + if (mInSavepoint) { + entry->mSavepointDelta++; + } + break; + case eDecrement: + entry->mDelta--; + if (mInSavepoint) { + entry->mSavepointDelta--; + } + break; + default: + NS_NOTREACHED("Unknown update type!"); + } + } + + return NS_OK; +} + +nsresult +UpdateRefcountFunction::CreateJournals() +{ + nsCOMPtr journalDirectory = mFileManager->GetJournalDirectory(); + NS_ENSURE_TRUE(journalDirectory, NS_ERROR_FAILURE); + + for (uint32_t i = 0; i < mJournalsToCreateBeforeCommit.Length(); i++) { + int64_t id = mJournalsToCreateBeforeCommit[i]; + + nsCOMPtr file = + mFileManager->GetFileForId(journalDirectory, id); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + nsresult rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + NS_ENSURE_SUCCESS(rv, rv); + + mJournalsToRemoveAfterAbort.AppendElement(id); + } + + return NS_OK; +} + +nsresult +UpdateRefcountFunction::RemoveJournals(const nsTArray& aJournals) +{ + nsCOMPtr journalDirectory = mFileManager->GetJournalDirectory(); + NS_ENSURE_TRUE(journalDirectory, NS_ERROR_FAILURE); + + for (uint32_t index = 0; index < aJournals.Length(); index++) { + nsCOMPtr file = + mFileManager->GetFileForId(journalDirectory, aJournals[index]); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + if (NS_FAILED(file->Remove(false))) { + NS_WARNING("Failed to removed journal!"); + } + } + + return NS_OK; +} + +PLDHashOperator +UpdateRefcountFunction::DatabaseUpdateCallback(const uint64_t& aKey, + FileInfoEntry* aValue, + void* aUserArg) +{ + if (!aValue->mDelta) { + return PL_DHASH_NEXT; + } + + DatabaseUpdateFunction* function = + static_cast(aUserArg); + + if (!function->Update(aKey, aValue->mDelta)) { + return PL_DHASH_STOP; + } + + return PL_DHASH_NEXT; +} + +PLDHashOperator +UpdateRefcountFunction::FileInfoUpdateCallback(const uint64_t& aKey, + FileInfoEntry* aValue, + void* aUserArg) +{ + if (aValue->mDelta) { + aValue->mFileInfo->UpdateDBRefs(aValue->mDelta); + } + + return PL_DHASH_NEXT; +} + +PLDHashOperator +UpdateRefcountFunction::RollbackSavepointCallback(const uint64_t& aKey, + FileInfoEntry* aValue, + void* aUserArg) +{ + aValue->mDelta -= aValue->mSavepointDelta; + + return PL_DHASH_NEXT; +} + +bool +UpdateRefcountFunction::DatabaseUpdateFunction::Update(int64_t aId, + int32_t aDelta) +{ + nsresult rv = UpdateInternal(aId, aDelta); + if (NS_FAILED(rv)) { + mErrorCode = rv; + return false; + } + + return true; +} + +nsresult +UpdateRefcountFunction::DatabaseUpdateFunction::UpdateInternal(int64_t aId, + int32_t aDelta) +{ + nsresult rv; + + if (!mUpdateStatement) { + rv = mConnection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE file SET refcount = refcount + :delta WHERE id = :id" + ), getter_AddRefs(mUpdateStatement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + mozStorageStatementScoper updateScoper(mUpdateStatement); + + rv = mUpdateStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mUpdateStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mUpdateStatement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t rows; + rv = mConnection->GetAffectedRows(&rows); + NS_ENSURE_SUCCESS(rv, rv); + + if (rows > 0) { + if (!mSelectStatement) { + rv = mConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id FROM file where id = :id" + ), getter_AddRefs(mSelectStatement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + mozStorageStatementScoper selectScoper(mSelectStatement); + + rv = mSelectStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult; + rv = mSelectStatement->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasResult) { + // Don't have to create the journal here, we can create all at once, + // just before commit + mFunction->mJournalsToCreateBeforeCommit.AppendElement(aId); + } + + return NS_OK; + } + + if (!mInsertStatement) { + rv = mConnection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO file (id, refcount) VALUES(:id, :delta)" + ), getter_AddRefs(mInsertStatement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + mozStorageStatementScoper insertScoper(mInsertStatement); + + rv = mInsertStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInsertStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInsertStatement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + mFunction->mJournalsToRemoveAfterCommit.AppendElement(aId); + + return NS_OK; +}