diff options
Diffstat (limited to 'dom/cache/Context.cpp')
-rw-r--r-- | dom/cache/Context.cpp | 1149 |
1 files changed, 1149 insertions, 0 deletions
diff --git a/dom/cache/Context.cpp b/dom/cache/Context.cpp new file mode 100644 index 000000000..db66ae90e --- /dev/null +++ b/dom/cache/Context.cpp @@ -0,0 +1,1149 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "mozilla/dom/cache/Context.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/dom/cache/Action.h" +#include "mozilla/dom/cache/FileUtils.h" +#include "mozilla/dom/cache/Manager.h" +#include "mozilla/dom/cache/ManagerId.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozIStorageConnection.h" +#include "nsIFile.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsThreadUtils.h" + +namespace { + +using mozilla::dom::cache::Action; +using mozilla::dom::cache::QuotaInfo; + +class NullAction final : public Action +{ +public: + NullAction() + { + } + + virtual void + RunOnTarget(Resolver* aResolver, const QuotaInfo&, Data*) override + { + // Resolve success immediately. This Action does no actual work. + MOZ_DIAGNOSTIC_ASSERT(aResolver); + aResolver->Resolve(NS_OK); + } +}; + +} // namespace + +namespace mozilla { +namespace dom { +namespace cache { + +using mozilla::dom::quota::AssertIsOnIOThread; +using mozilla::dom::quota::OpenDirectoryListener; +using mozilla::dom::quota::QuotaManager; +using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT; +using mozilla::dom::quota::PersistenceType; + +class Context::Data final : public Action::Data +{ +public: + explicit Data(nsIThread* aTarget) + : mTarget(aTarget) + { + MOZ_DIAGNOSTIC_ASSERT(mTarget); + } + + virtual mozIStorageConnection* + GetConnection() const override + { + MOZ_ASSERT(mTarget == NS_GetCurrentThread()); + return mConnection; + } + + virtual void + SetConnection(mozIStorageConnection* aConn) override + { + MOZ_ASSERT(mTarget == NS_GetCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(!mConnection); + mConnection = aConn; + MOZ_DIAGNOSTIC_ASSERT(mConnection); + } + +private: + ~Data() + { + // We could proxy release our data here, but instead just assert. The + // Context code should guarantee that we are destroyed on the target + // thread once the connection is initialized. If we're not, then + // QuotaManager might race and try to clear the origin out from under us. + MOZ_ASSERT_IF(mConnection, mTarget == NS_GetCurrentThread()); + } + + nsCOMPtr<nsIThread> mTarget; + nsCOMPtr<mozIStorageConnection> mConnection; + + // Threadsafe counting because we're created on the PBackground thread + // and destroyed on the target IO thread. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Context::Data) +}; + +// Executed to perform the complicated dance of steps necessary to initialize +// the QuotaManager. This must be performed for each origin before any disk +// IO occurrs. +class Context::QuotaInitRunnable final : public nsIRunnable + , public OpenDirectoryListener +{ +public: + QuotaInitRunnable(Context* aContext, + Manager* aManager, + Data* aData, + nsIThread* aTarget, + Action* aInitAction) + : mContext(aContext) + , mThreadsafeHandle(aContext->CreateThreadsafeHandle()) + , mManager(aManager) + , mData(aData) + , mTarget(aTarget) + , mInitAction(aInitAction) + , mInitiatingThread(NS_GetCurrentThread()) + , mResult(NS_OK) + , mState(STATE_INIT) + , mCanceled(false) + { + MOZ_DIAGNOSTIC_ASSERT(mContext); + MOZ_DIAGNOSTIC_ASSERT(mManager); + MOZ_DIAGNOSTIC_ASSERT(mData); + MOZ_DIAGNOSTIC_ASSERT(mTarget); + MOZ_DIAGNOSTIC_ASSERT(mInitiatingThread); + MOZ_DIAGNOSTIC_ASSERT(mInitAction); + } + + nsresult Dispatch() + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_INIT); + + mState = STATE_GET_INFO; + nsresult rv = NS_DispatchToMainThread(this, nsIThread::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + mState = STATE_COMPLETE; + Clear(); + } + return rv; + } + + void Cancel() + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(!mCanceled); + mCanceled = true; + mInitAction->CancelOnInitiatingThread(); + } + + void OpenDirectory(); + + // OpenDirectoryListener methods + virtual void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void + DirectoryLockFailed() override; + +private: + class SyncResolver final : public Action::Resolver + { + public: + SyncResolver() + : mResolved(false) + , mResult(NS_OK) + { } + + virtual void + Resolve(nsresult aRv) override + { + MOZ_DIAGNOSTIC_ASSERT(!mResolved); + mResolved = true; + mResult = aRv; + }; + + bool Resolved() const { return mResolved; } + nsresult Result() const { return mResult; } + + private: + ~SyncResolver() { } + + bool mResolved; + nsresult mResult; + + NS_INLINE_DECL_REFCOUNTING(Context::QuotaInitRunnable::SyncResolver, override) + }; + + ~QuotaInitRunnable() + { + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_COMPLETE); + MOZ_DIAGNOSTIC_ASSERT(!mContext); + MOZ_DIAGNOSTIC_ASSERT(!mInitAction); + } + + enum State + { + STATE_INIT, + STATE_GET_INFO, + STATE_CREATE_QUOTA_MANAGER, + STATE_OPEN_DIRECTORY, + STATE_WAIT_FOR_DIRECTORY_LOCK, + STATE_ENSURE_ORIGIN_INITIALIZED, + STATE_RUN_ON_TARGET, + STATE_RUNNING, + STATE_COMPLETING, + STATE_COMPLETE + }; + + void Complete(nsresult aResult) + { + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_RUNNING || NS_FAILED(aResult)); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(mResult)); + mResult = aResult; + + mState = STATE_COMPLETING; + MOZ_ALWAYS_SUCCEEDS( + mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL)); + } + + void Clear() + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(mContext); + mContext = nullptr; + mManager = nullptr; + mInitAction = nullptr; + } + + RefPtr<Context> mContext; + RefPtr<ThreadsafeHandle> mThreadsafeHandle; + RefPtr<Manager> mManager; + RefPtr<Data> mData; + nsCOMPtr<nsIThread> mTarget; + RefPtr<Action> mInitAction; + nsCOMPtr<nsIThread> mInitiatingThread; + nsresult mResult; + QuotaInfo mQuotaInfo; + RefPtr<DirectoryLock> mDirectoryLock; + State mState; + Atomic<bool> mCanceled; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE +}; + +void +Context::QuotaInitRunnable::OpenDirectory() +{ + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CREATE_QUOTA_MANAGER || + mState == STATE_OPEN_DIRECTORY); + MOZ_DIAGNOSTIC_ASSERT(QuotaManager::Get()); + + // QuotaManager::OpenDirectory() will hold a reference to us as + // a listener. We will then get DirectoryLockAcquired() on the owning + // thread when it is safe to access our storage directory. + mState = STATE_WAIT_FOR_DIRECTORY_LOCK; + QuotaManager::Get()->OpenDirectory(PERSISTENCE_TYPE_DEFAULT, + mQuotaInfo.mGroup, + mQuotaInfo.mOrigin, + mQuotaInfo.mIsApp, + quota::Client::DOMCACHE, + /* aExclusive */ false, + this); +} + +void +Context::QuotaInitRunnable::DirectoryLockAcquired(DirectoryLock* aLock) +{ + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_WAIT_FOR_DIRECTORY_LOCK); + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + if (mCanceled) { + Complete(NS_ERROR_ABORT); + return; + } + + QuotaManager* qm = QuotaManager::Get(); + MOZ_DIAGNOSTIC_ASSERT(qm); + + mState = STATE_ENSURE_ORIGIN_INITIALIZED; + nsresult rv = qm->IOThread()->Dispatch(this, nsIThread::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + Complete(rv); + return; + } +} + +void +Context::QuotaInitRunnable::DirectoryLockFailed() +{ + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_WAIT_FOR_DIRECTORY_LOCK); + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock); + + NS_WARNING("Failed to acquire a directory lock!"); + + Complete(NS_ERROR_FAILURE); +} + +NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::QuotaInitRunnable, nsIRunnable); + +// The QuotaManager init state machine is represented in the following diagram: +// +// +---------------+ +// | Start | Resolve(error) +// | (Orig Thread) +---------------------+ +// +-------+-------+ | +// | | +// +----------v-----------+ | +// | GetInfo | Resolve(error) | +// | (Main Thread) +-----------------+ +// +----------+-----------+ | +// | | +// +----------v-----------+ | +// | CreateQuotaManager | Resolve(error) | +// | (Orig Thread) +-----------------+ +// +----------+-----------+ | +// | | +// +----------v-----------+ | +// | OpenDirectory | Resolve(error) | +// | (Orig Thread) +-----------------+ +// +----------+-----------+ | +// | | +// +----------v-----------+ | +// | WaitForDirectoryLock | Resolve(error) | +// | (Orig Thread) +-----------------+ +// +----------+-----------+ | +// | | +// +----------v------------+ | +// |EnsureOriginInitialized| Resolve(error) | +// | (Quota IO Thread) +----------------+ +// +----------+------------+ | +// | | +// +----------v------------+ | +// | RunOnTarget | Resolve(error) | +// | (Target Thread) +----------------+ +// +----------+------------+ | +// | | +// +---------v---------+ +------v------+ +// | Running | | Completing | +// | (Target Thread) +------------>(Orig Thread)| +// +-------------------+ +------+------+ +// | +// +-----v----+ +// | Complete | +// +----------+ +// +// The initialization process proceeds through the main states. If an error +// occurs, then we transition to Completing state back on the original thread. +NS_IMETHODIMP +Context::QuotaInitRunnable::Run() +{ + // May run on different threads depending on the state. See individual + // state cases for thread assertions. + + RefPtr<SyncResolver> resolver = new SyncResolver(); + + switch(mState) { + // ----------------------------------- + case STATE_GET_INFO: + { + MOZ_ASSERT(NS_IsMainThread()); + + if (mCanceled) { + resolver->Resolve(NS_ERROR_ABORT); + break; + } + + RefPtr<ManagerId> managerId = mManager->GetManagerId(); + nsCOMPtr<nsIPrincipal> principal = managerId->Principal(); + nsresult rv = QuotaManager::GetInfoFromPrincipal(principal, + &mQuotaInfo.mSuffix, + &mQuotaInfo.mGroup, + &mQuotaInfo.mOrigin, + &mQuotaInfo.mIsApp); + if (NS_WARN_IF(NS_FAILED(rv))) { + resolver->Resolve(rv); + break; + } + + mState = STATE_CREATE_QUOTA_MANAGER; + MOZ_ALWAYS_SUCCEEDS( + mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL)); + break; + } + // ---------------------------------- + case STATE_CREATE_QUOTA_MANAGER: + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + + if (mCanceled || QuotaManager::IsShuttingDown()) { + resolver->Resolve(NS_ERROR_ABORT); + break; + } + + if (QuotaManager::Get()) { + OpenDirectory(); + return NS_OK; + } + + mState = STATE_OPEN_DIRECTORY; + QuotaManager::GetOrCreate(this); + break; + } + // ---------------------------------- + case STATE_OPEN_DIRECTORY: + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + + if (NS_WARN_IF(!QuotaManager::Get())) { + resolver->Resolve(NS_ERROR_FAILURE); + break; + } + + OpenDirectory(); + break; + } + // ---------------------------------- + case STATE_ENSURE_ORIGIN_INITIALIZED: + { + AssertIsOnIOThread(); + + if (mCanceled) { + resolver->Resolve(NS_ERROR_ABORT); + break; + } + + QuotaManager* qm = QuotaManager::Get(); + MOZ_DIAGNOSTIC_ASSERT(qm); + nsresult rv = qm->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT, + mQuotaInfo.mSuffix, + mQuotaInfo.mGroup, + mQuotaInfo.mOrigin, + mQuotaInfo.mIsApp, + getter_AddRefs(mQuotaInfo.mDir)); + if (NS_FAILED(rv)) { + resolver->Resolve(rv); + break; + } + + mState = STATE_RUN_ON_TARGET; + + MOZ_ALWAYS_SUCCEEDS( + mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL)); + break; + } + // ------------------- + case STATE_RUN_ON_TARGET: + { + MOZ_ASSERT(NS_GetCurrentThread() == mTarget); + + mState = STATE_RUNNING; + + // Execute the provided initialization Action. The Action must Resolve() + // before returning. + mInitAction->RunOnTarget(resolver, mQuotaInfo, mData); + MOZ_DIAGNOSTIC_ASSERT(resolver->Resolved()); + + mData = nullptr; + + // If the database was opened, then we should always succeed when creating + // the marker file. If it wasn't opened successfully, then no need to + // create a marker file anyway. + if (NS_SUCCEEDED(resolver->Result())) { + MOZ_ALWAYS_SUCCEEDS(CreateMarkerFile(mQuotaInfo)); + } + + break; + } + // ------------------- + case STATE_COMPLETING: + { + NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable); + mInitAction->CompleteOnInitiatingThread(mResult); + mContext->OnQuotaInit(mResult, mQuotaInfo, mDirectoryLock.forget()); + mState = STATE_COMPLETE; + + // Explicitly cleanup here as the destructor could fire on any of + // the threads we have bounced through. + Clear(); + break; + } + // ----- + case STATE_WAIT_FOR_DIRECTORY_LOCK: + default: + { + MOZ_CRASH("unexpected state in QuotaInitRunnable"); + } + } + + if (resolver->Resolved()) { + Complete(resolver->Result()); + } + + return NS_OK; +} + +// Runnable wrapper around Action objects dispatched on the Context. This +// runnable executes the Action on the appropriate threads while the Context +// is initialized. +class Context::ActionRunnable final : public nsIRunnable + , public Action::Resolver + , public Context::Activity +{ +public: + ActionRunnable(Context* aContext, Data* aData, nsIEventTarget* aTarget, + Action* aAction, const QuotaInfo& aQuotaInfo) + : mContext(aContext) + , mData(aData) + , mTarget(aTarget) + , mAction(aAction) + , mQuotaInfo(aQuotaInfo) + , mInitiatingThread(NS_GetCurrentThread()) + , mState(STATE_INIT) + , mResult(NS_OK) + , mExecutingRunOnTarget(false) + { + MOZ_DIAGNOSTIC_ASSERT(mContext); + // mData may be nullptr + MOZ_DIAGNOSTIC_ASSERT(mTarget); + MOZ_DIAGNOSTIC_ASSERT(mAction); + // mQuotaInfo.mDir may be nullptr if QuotaInitRunnable failed + MOZ_DIAGNOSTIC_ASSERT(mInitiatingThread); + } + + nsresult Dispatch() + { + NS_ASSERT_OWNINGTHREAD(ActionRunnable); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_INIT); + + mState = STATE_RUN_ON_TARGET; + nsresult rv = mTarget->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + mState = STATE_COMPLETE; + Clear(); + } + return rv; + } + + virtual bool + MatchesCacheId(CacheId aCacheId) const override + { + NS_ASSERT_OWNINGTHREAD(ActionRunnable); + return mAction->MatchesCacheId(aCacheId); + } + + virtual void + Cancel() override + { + NS_ASSERT_OWNINGTHREAD(ActionRunnable); + mAction->CancelOnInitiatingThread(); + } + + virtual void Resolve(nsresult aRv) override + { + MOZ_ASSERT(mTarget == NS_GetCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_RUNNING); + + mResult = aRv; + + // We ultimately must complete on the initiating thread, but bounce through + // the current thread again to ensure that we don't destroy objects and + // state out from under the currently running action's stack. + mState = STATE_RESOLVING; + + // If we were resolved synchronously within Action::RunOnTarget() then we + // can avoid a thread bounce and just resolve once RunOnTarget() returns. + // The Run() method will handle this by looking at mState after + // RunOnTarget() returns. + if (mExecutingRunOnTarget) { + return; + } + + // Otherwise we are in an asynchronous resolve. And must perform a thread + // bounce to run on the target thread again. + MOZ_ALWAYS_SUCCEEDS( + mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL)); + } + +private: + ~ActionRunnable() + { + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_COMPLETE); + MOZ_DIAGNOSTIC_ASSERT(!mContext); + MOZ_DIAGNOSTIC_ASSERT(!mAction); + } + + void Clear() + { + NS_ASSERT_OWNINGTHREAD(ActionRunnable); + MOZ_DIAGNOSTIC_ASSERT(mContext); + MOZ_DIAGNOSTIC_ASSERT(mAction); + mContext->RemoveActivity(this); + mContext = nullptr; + mAction = nullptr; + } + + enum State + { + STATE_INIT, + STATE_RUN_ON_TARGET, + STATE_RUNNING, + STATE_RESOLVING, + STATE_COMPLETING, + STATE_COMPLETE + }; + + RefPtr<Context> mContext; + RefPtr<Data> mData; + nsCOMPtr<nsIEventTarget> mTarget; + RefPtr<Action> mAction; + const QuotaInfo mQuotaInfo; + nsCOMPtr<nsIThread> mInitiatingThread; + State mState; + nsresult mResult; + + // Only accessible on target thread; + bool mExecutingRunOnTarget; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE +}; + +NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::ActionRunnable, nsIRunnable); + +// The ActionRunnable has a simpler state machine. It basically needs to run +// the action on the target thread and then complete on the original thread. +// +// +-------------+ +// | Start | +// |(Orig Thread)| +// +-----+-------+ +// | +// +-------v---------+ +// | RunOnTarget | +// |Target IO Thread)+---+ Resolve() +// +-------+---------+ | +// | | +// +-------v----------+ | +// | Running | | +// |(Target IO Thread)| | +// +------------------+ | +// | Resolve() | +// +-------v----------+ | +// | Resolving <--+ +-------------+ +// | | | Completing | +// |(Target IO Thread)+---------------------->(Orig Thread)| +// +------------------+ +-------+-----+ +// | +// | +// +----v---+ +// |Complete| +// +--------+ +// +// Its important to note that synchronous actions will effectively Resolve() +// out of the Running state immediately. Asynchronous Actions may remain +// in the Running state for some time, but normally the ActionRunnable itself +// does not see any execution there. Its all handled internal to the Action. +NS_IMETHODIMP +Context::ActionRunnable::Run() +{ + switch(mState) { + // ---------------------- + case STATE_RUN_ON_TARGET: + { + MOZ_ASSERT(NS_GetCurrentThread() == mTarget); + MOZ_DIAGNOSTIC_ASSERT(!mExecutingRunOnTarget); + + // Note that we are calling RunOnTarget(). This lets us detect + // if Resolve() is called synchronously. + AutoRestore<bool> executingRunOnTarget(mExecutingRunOnTarget); + mExecutingRunOnTarget = true; + + mState = STATE_RUNNING; + mAction->RunOnTarget(this, mQuotaInfo, mData); + + mData = nullptr; + + // Resolve was called synchronously from RunOnTarget(). We can + // immediately move to completing now since we are sure RunOnTarget() + // completed. + if (mState == STATE_RESOLVING) { + // Use recursion instead of switch case fall-through... Seems slightly + // easier to understand. + Run(); + } + + break; + } + // ----------------- + case STATE_RESOLVING: + { + MOZ_ASSERT(NS_GetCurrentThread() == mTarget); + // The call to Action::RunOnTarget() must have returned now if we + // are running on the target thread again. We may now proceed + // with completion. + mState = STATE_COMPLETING; + // Shutdown must be delayed until all Contexts are destroyed. Crash + // for this invariant violation. + MOZ_ALWAYS_SUCCEEDS( + mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL)); + break; + } + // ------------------- + case STATE_COMPLETING: + { + NS_ASSERT_OWNINGTHREAD(ActionRunnable); + mAction->CompleteOnInitiatingThread(mResult); + mState = STATE_COMPLETE; + // Explicitly cleanup here as the destructor could fire on any of + // the threads we have bounced through. + Clear(); + break; + } + // ----------------- + default: + { + MOZ_CRASH("unexpected state in ActionRunnable"); + break; + } + } + return NS_OK; +} + +void +Context::ThreadsafeHandle::AllowToClose() +{ + if (mOwningThread == NS_GetCurrentThread()) { + AllowToCloseOnOwningThread(); + return; + } + + // Dispatch is guaranteed to succeed here because we block shutdown until + // all Contexts have been destroyed. + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod(this, &ThreadsafeHandle::AllowToCloseOnOwningThread); + MOZ_ALWAYS_SUCCEEDS( + mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)); +} + +void +Context::ThreadsafeHandle::InvalidateAndAllowToClose() +{ + if (mOwningThread == NS_GetCurrentThread()) { + InvalidateAndAllowToCloseOnOwningThread(); + return; + } + + // Dispatch is guaranteed to succeed here because we block shutdown until + // all Contexts have been destroyed. + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod(this, &ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread); + MOZ_ALWAYS_SUCCEEDS( + mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)); +} + +Context::ThreadsafeHandle::ThreadsafeHandle(Context* aContext) + : mStrongRef(aContext) + , mWeakRef(aContext) + , mOwningThread(NS_GetCurrentThread()) +{ +} + +Context::ThreadsafeHandle::~ThreadsafeHandle() +{ + // Normally we only touch mStrongRef on the owning thread. This is safe, + // however, because when we do use mStrongRef on the owning thread we are + // always holding a strong ref to the ThreadsafeHandle via the owning + // runnable. So we cannot run the ThreadsafeHandle destructor simultaneously. + if (!mStrongRef || mOwningThread == NS_GetCurrentThread()) { + return; + } + + // Dispatch is guaranteed to succeed here because we block shutdown until + // all Contexts have been destroyed. + NS_ProxyRelease(mOwningThread, mStrongRef.forget()); +} + +void +Context::ThreadsafeHandle::AllowToCloseOnOwningThread() +{ + MOZ_ASSERT(mOwningThread == NS_GetCurrentThread()); + + // A Context "closes" when its ref count drops to zero. Dropping this + // strong ref is necessary, but not sufficient for the close to occur. + // Any outstanding IO will continue and keep the Context alive. Once + // the Context is idle, it will be destroyed. + + // First, tell the context to flush any target thread shared data. This + // data must be released on the target thread prior to running the Context + // destructor. This will schedule an Action which ensures that the + // ~Context() is not immediately executed when we drop the strong ref. + if (mStrongRef) { + mStrongRef->DoomTargetData(); + } + + // Now drop our strong ref and let Context finish running any outstanding + // Actions. + mStrongRef = nullptr; +} + +void +Context::ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread() +{ + MOZ_ASSERT(mOwningThread == NS_GetCurrentThread()); + // Cancel the Context through the weak reference. This means we can + // allow the Context to close by dropping the strong ref, but then + // still cancel ongoing IO if necessary. + if (mWeakRef) { + mWeakRef->Invalidate(); + } + // We should synchronously have AllowToCloseOnOwningThread called when + // the Context is canceled. + MOZ_DIAGNOSTIC_ASSERT(!mStrongRef); +} + +void +Context::ThreadsafeHandle::ContextDestroyed(Context* aContext) +{ + MOZ_ASSERT(mOwningThread == NS_GetCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(!mStrongRef); + MOZ_DIAGNOSTIC_ASSERT(mWeakRef); + MOZ_DIAGNOSTIC_ASSERT(mWeakRef == aContext); + mWeakRef = nullptr; +} + +// static +already_AddRefed<Context> +Context::Create(Manager* aManager, nsIThread* aTarget, + Action* aInitAction, Context* aOldContext) +{ + RefPtr<Context> context = new Context(aManager, aTarget, aInitAction); + context->Init(aOldContext); + return context.forget(); +} + +Context::Context(Manager* aManager, nsIThread* aTarget, Action* aInitAction) + : mManager(aManager) + , mTarget(aTarget) + , mData(new Data(aTarget)) + , mState(STATE_CONTEXT_PREINIT) + , mOrphanedData(false) + , mInitAction(aInitAction) +{ + MOZ_DIAGNOSTIC_ASSERT(mManager); + MOZ_DIAGNOSTIC_ASSERT(mTarget); +} + +void +Context::Dispatch(Action* aAction) +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(aAction); + + MOZ_DIAGNOSTIC_ASSERT(mState != STATE_CONTEXT_CANCELED); + if (mState == STATE_CONTEXT_CANCELED) { + return; + } else if (mState == STATE_CONTEXT_INIT || + mState == STATE_CONTEXT_PREINIT) { + PendingAction* pending = mPendingActions.AppendElement(); + pending->mAction = aAction; + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_READY); + DispatchAction(aAction); +} + +void +Context::CancelAll() +{ + NS_ASSERT_OWNINGTHREAD(Context); + + // In PREINIT state we have not dispatch the init action yet. Just + // forget it. + if (mState == STATE_CONTEXT_PREINIT) { + MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable); + mInitAction = nullptr; + + // In INIT state we have dispatched the runnable, but not received the + // async completion yet. Cancel the runnable, but don't forget about it + // until we get OnQuotaInit() callback. + } else if (mState == STATE_CONTEXT_INIT) { + mInitRunnable->Cancel(); + } + + mState = STATE_CONTEXT_CANCELED; + mPendingActions.Clear(); + { + ActivityList::ForwardIterator iter(mActivityList); + while (iter.HasMore()) { + iter.GetNext()->Cancel(); + } + } + AllowToClose(); +} + +bool +Context::IsCanceled() const +{ + NS_ASSERT_OWNINGTHREAD(Context); + return mState == STATE_CONTEXT_CANCELED; +} + +void +Context::Invalidate() +{ + NS_ASSERT_OWNINGTHREAD(Context); + mManager->NoteClosing(); + CancelAll(); +} + +void +Context::AllowToClose() +{ + NS_ASSERT_OWNINGTHREAD(Context); + if (mThreadsafeHandle) { + mThreadsafeHandle->AllowToClose(); + } +} + +void +Context::CancelForCacheId(CacheId aCacheId) +{ + NS_ASSERT_OWNINGTHREAD(Context); + + // Remove matching pending actions + for (int32_t i = mPendingActions.Length() - 1; i >= 0; --i) { + if (mPendingActions[i].mAction->MatchesCacheId(aCacheId)) { + mPendingActions.RemoveElementAt(i); + } + } + + // Cancel activities and let them remove themselves + ActivityList::ForwardIterator iter(mActivityList); + while (iter.HasMore()) { + Activity* activity = iter.GetNext(); + if (activity->MatchesCacheId(aCacheId)) { + activity->Cancel(); + } + } +} + +Context::~Context() +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(mManager); + MOZ_DIAGNOSTIC_ASSERT(!mData); + + if (mThreadsafeHandle) { + mThreadsafeHandle->ContextDestroyed(this); + } + + // Note, this may set the mOrphanedData flag. + mManager->RemoveContext(this); + + if (mQuotaInfo.mDir && !mOrphanedData) { + MOZ_ALWAYS_SUCCEEDS(DeleteMarkerFile(mQuotaInfo)); + } + + if (mNextContext) { + mNextContext->Start(); + } +} + +void +Context::Init(Context* aOldContext) +{ + NS_ASSERT_OWNINGTHREAD(Context); + + if (aOldContext) { + aOldContext->SetNextContext(this); + return; + } + + Start(); +} + +void +Context::Start() +{ + NS_ASSERT_OWNINGTHREAD(Context); + + // Previous context closing delayed our start, but then we were canceled. + // In this case, just do nothing here. + if (mState == STATE_CONTEXT_CANCELED) { + MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable); + MOZ_DIAGNOSTIC_ASSERT(!mInitAction); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_PREINIT); + MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable); + + mInitRunnable = new QuotaInitRunnable(this, mManager, mData, mTarget, + mInitAction); + mInitAction = nullptr; + + mState = STATE_CONTEXT_INIT; + + nsresult rv = mInitRunnable->Dispatch(); + if (NS_FAILED(rv)) { + // Shutdown must be delayed until all Contexts are destroyed. Shutdown + // must also prevent any new Contexts from being constructed. Crash + // for this invariant violation. + MOZ_CRASH("Failed to dispatch QuotaInitRunnable."); + } +} + +void +Context::DispatchAction(Action* aAction, bool aDoomData) +{ + NS_ASSERT_OWNINGTHREAD(Context); + + RefPtr<ActionRunnable> runnable = + new ActionRunnable(this, mData, mTarget, aAction, mQuotaInfo); + + if (aDoomData) { + mData = nullptr; + } + + nsresult rv = runnable->Dispatch(); + if (NS_FAILED(rv)) { + // Shutdown must be delayed until all Contexts are destroyed. Crash + // for this invariant violation. + MOZ_CRASH("Failed to dispatch ActionRunnable to target thread."); + } + AddActivity(runnable); +} + +void +Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo, + already_AddRefed<DirectoryLock> aDirectoryLock) +{ + NS_ASSERT_OWNINGTHREAD(Context); + + MOZ_DIAGNOSTIC_ASSERT(mInitRunnable); + mInitRunnable = nullptr; + + mQuotaInfo = aQuotaInfo; + + // Always save the directory lock to ensure QuotaManager does not shutdown + // before the Context has gone away. + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock); + mDirectoryLock = aDirectoryLock; + + // If we opening the context failed, but we were not explicitly canceled, + // still treat the entire context as canceled. We don't want to allow + // new actions to be dispatched. We also cannot leave the context in + // the INIT state after failing to open. + if (NS_FAILED(aRv)) { + mState = STATE_CONTEXT_CANCELED; + } + + if (mState == STATE_CONTEXT_CANCELED) { + for (uint32_t i = 0; i < mPendingActions.Length(); ++i) { + mPendingActions[i].mAction->CompleteOnInitiatingThread(aRv); + } + mPendingActions.Clear(); + mThreadsafeHandle->AllowToClose(); + // Context will destruct after return here and last ref is released. + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_INIT); + mState = STATE_CONTEXT_READY; + + for (uint32_t i = 0; i < mPendingActions.Length(); ++i) { + DispatchAction(mPendingActions[i].mAction); + } + mPendingActions.Clear(); +} + +void +Context::AddActivity(Activity* aActivity) +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(aActivity); + MOZ_ASSERT(!mActivityList.Contains(aActivity)); + mActivityList.AppendElement(aActivity); +} + +void +Context::RemoveActivity(Activity* aActivity) +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(aActivity); + MOZ_ALWAYS_TRUE(mActivityList.RemoveElement(aActivity)); + MOZ_ASSERT(!mActivityList.Contains(aActivity)); +} + +void +Context::NoteOrphanedData() +{ + NS_ASSERT_OWNINGTHREAD(Context); + // This may be called more than once + mOrphanedData = true; +} + +already_AddRefed<Context::ThreadsafeHandle> +Context::CreateThreadsafeHandle() +{ + NS_ASSERT_OWNINGTHREAD(Context); + if (!mThreadsafeHandle) { + mThreadsafeHandle = new ThreadsafeHandle(this); + } + RefPtr<ThreadsafeHandle> ref = mThreadsafeHandle; + return ref.forget(); +} + +void +Context::SetNextContext(Context* aNextContext) +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(aNextContext); + MOZ_DIAGNOSTIC_ASSERT(!mNextContext); + mNextContext = aNextContext; +} + +void +Context::DoomTargetData() +{ + NS_ASSERT_OWNINGTHREAD(Context); + MOZ_DIAGNOSTIC_ASSERT(mData); + + // We are about to drop our reference to the Data. We need to ensure that + // the ~Context() destructor does not run until contents of Data have been + // released on the Target thread. + + // Dispatch a no-op Action. This will hold the Context alive through a + // roundtrip to the target thread and back to the owning thread. The + // ref to the Data object is cleared on the owning thread after creating + // the ActionRunnable, but before dispatching it. + RefPtr<Action> action = new NullAction(); + DispatchAction(action, true /* doomed data */); + + MOZ_DIAGNOSTIC_ASSERT(!mData); +} + +} // namespace cache +} // namespace dom +} // namespace mozilla |