summaryrefslogtreecommitdiffstats
path: root/dom/cache/Context.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/cache/Context.cpp')
-rw-r--r--dom/cache/Context.cpp1081
1 files changed, 1081 insertions, 0 deletions
diff --git a/dom/cache/Context.cpp b/dom/cache/Context.cpp
new file mode 100644
index 0000000000..e1d233735d
--- /dev/null
+++ b/dom/cache/Context.cpp
@@ -0,0 +1,1081 @@
+/* -*- 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 "CacheCommon.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/dom/SafeRefPtr.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 "nsIPrincipal.h"
+#include "nsIRunnable.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+namespace {
+
+using mozilla::dom::cache::Action;
+using mozilla::dom::cache::QuotaInfo;
+
+class NullAction final : public Action {
+ public:
+ NullAction() = default;
+
+ virtual void RunOnTarget(mozilla::SafeRefPtr<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::dom::cache {
+
+using mozilla::dom::quota::AssertIsOnIOThread;
+using mozilla::dom::quota::DirectoryLock;
+using mozilla::dom::quota::OpenDirectoryListener;
+using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
+using mozilla::dom::quota::PersistenceType;
+using mozilla::dom::quota::QuotaManager;
+
+class Context::Data final : public Action::Data {
+ public:
+ explicit Data(nsISerialEventTarget* aTarget) : mTarget(aTarget) {
+ MOZ_DIAGNOSTIC_ASSERT(mTarget);
+ }
+
+ virtual mozIStorageConnection* GetConnection() const override {
+ MOZ_ASSERT(mTarget->IsOnCurrentThread());
+ return mConnection;
+ }
+
+ virtual void SetConnection(mozIStorageConnection* aConn) override {
+ MOZ_ASSERT(mTarget->IsOnCurrentThread());
+ 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->IsOnCurrentThread());
+ }
+
+ nsCOMPtr<nsISerialEventTarget> 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(SafeRefPtr<Context> aContext, SafeRefPtr<Manager> aManager,
+ Data* aData, nsISerialEventTarget* aTarget,
+ SafeRefPtr<Action> aInitAction)
+ : mContext(std::move(aContext)),
+ mThreadsafeHandle(mContext->CreateThreadsafeHandle()),
+ mManager(std::move(aManager)),
+ mData(aData),
+ mTarget(aTarget),
+ mInitAction(std::move(aInitAction)),
+ mInitiatingEventTarget(GetCurrentSerialEventTarget()),
+ 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(mInitiatingEventTarget);
+ MOZ_DIAGNOSTIC_ASSERT(mInitAction);
+ }
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ 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() = default;
+
+ 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(
+ mInitiatingEventTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));
+ }
+
+ void Clear() {
+ NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
+ MOZ_DIAGNOSTIC_ASSERT(mContext);
+ mContext = nullptr;
+ mManager = nullptr;
+ mInitAction = nullptr;
+ }
+
+ SafeRefPtr<Context> mContext;
+ SafeRefPtr<ThreadsafeHandle> mThreadsafeHandle;
+ SafeRefPtr<Manager> mManager;
+ RefPtr<Data> mData;
+ nsCOMPtr<nsISerialEventTarget> mTarget;
+ SafeRefPtr<Action> mInitAction;
+ nsCOMPtr<nsIEventTarget> mInitiatingEventTarget;
+ 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;
+ RefPtr<DirectoryLock> pendingDirectoryLock =
+ QuotaManager::Get()->OpenDirectory(PERSISTENCE_TYPE_DEFAULT, mQuotaInfo,
+ quota::Client::DOMCACHE,
+ /* aExclusive */ false, this);
+}
+
+void Context::QuotaInitRunnable::DirectoryLockAcquired(DirectoryLock* aLock) {
+ NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
+ MOZ_DIAGNOSTIC_ASSERT(aLock);
+ MOZ_DIAGNOSTIC_ASSERT(mState == STATE_WAIT_FOR_DIRECTORY_LOCK);
+ MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);
+
+ mDirectoryLock = aLock;
+
+ MOZ_DIAGNOSTIC_ASSERT(mDirectoryLock->Id() >= 0);
+ mQuotaInfo.mDirectoryLockId = mDirectoryLock->Id();
+
+ 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.
+
+ SafeRefPtr<SyncResolver> resolver = MakeSafeRefPtr<SyncResolver>();
+
+ switch (mState) {
+ // -----------------------------------
+ case STATE_GET_INFO: {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mCanceled) {
+ resolver->Resolve(NS_ERROR_ABORT);
+ break;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = mManager->GetManagerId().Principal();
+ DebugOnly res =
+ QuotaManager::GetInfoFromPrincipal(principal)
+ .andThen([&self = *this](quota::QuotaInfo&& quotaInfo) {
+ static_cast<quota::QuotaInfo&>(self.mQuotaInfo) =
+ std::move(quotaInfo);
+
+ self.mState = STATE_CREATE_QUOTA_MANAGER;
+ MOZ_ALWAYS_SUCCEEDS(self.mInitiatingEventTarget->Dispatch(
+ &self, nsIThread::DISPATCH_NORMAL));
+
+ return Result<Ok, nsresult>{Ok{}};
+ })
+ .orElse([&resolver](const auto& res) {
+ resolver->Resolve(res);
+
+ return Result<Ok, nsresult>{Ok{}};
+ });
+ MOZ_ASSERT(res.inspect().isOk());
+ 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();
+
+ auto res = [this]() -> Result<Ok, nsresult> {
+ if (mCanceled) {
+ return Err(NS_ERROR_ABORT);
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_DIAGNOSTIC_ASSERT(quotaManager);
+
+ CACHE_TRY(quotaManager->EnsureStorageIsInitialized());
+
+ CACHE_TRY(quotaManager->EnsureTemporaryStorageIsInitialized());
+
+ CACHE_TRY_UNWRAP(mQuotaInfo.mDir,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo)
+ .map([](const auto& res) { return res.first; }));
+
+ mState = STATE_RUN_ON_TARGET;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));
+
+ return Ok{};
+ }();
+
+ if (res.isErr()) {
+ resolver->Resolve(res.inspectErr());
+ }
+
+ break;
+ }
+ // -------------------
+ case STATE_RUN_ON_TARGET: {
+ MOZ_ASSERT(mTarget->IsOnCurrentThread());
+
+ mState = STATE_RUNNING;
+
+ // Execute the provided initialization Action. The Action must Resolve()
+ // before returning.
+ mInitAction->RunOnTarget(resolver.clonePtr(), 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(SafeRefPtr<Context> aContext, Data* aData,
+ nsISerialEventTarget* aTarget, SafeRefPtr<Action> aAction,
+ const QuotaInfo& aQuotaInfo)
+ : mContext(std::move(aContext)),
+ mData(aData),
+ mTarget(aTarget),
+ mAction(std::move(aAction)),
+ mQuotaInfo(aQuotaInfo),
+ mInitiatingThread(GetCurrentEventTarget()),
+ 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->IsOnCurrentThread());
+ 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
+ };
+
+ SafeRefPtr<Context> mContext;
+ RefPtr<Data> mData;
+ nsCOMPtr<nsISerialEventTarget> mTarget;
+ SafeRefPtr<Action> mAction;
+ const QuotaInfo mQuotaInfo;
+ nsCOMPtr<nsIEventTarget> 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(mTarget->IsOnCurrentThread());
+ 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(SafeRefPtrFromThis(), 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(mTarget->IsOnCurrentThread());
+ // 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 (mOwningEventTarget->IsOnCurrentThread()) {
+ AllowToCloseOnOwningThread();
+ return;
+ }
+
+ // Dispatch is guaranteed to succeed here because we block shutdown until
+ // all Contexts have been destroyed.
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
+ "dom::cache::Context::ThreadsafeHandle::AllowToCloseOnOwningThread", this,
+ &ThreadsafeHandle::AllowToCloseOnOwningThread);
+ MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(runnable.forget(),
+ nsIThread::DISPATCH_NORMAL));
+}
+
+void Context::ThreadsafeHandle::InvalidateAndAllowToClose() {
+ if (mOwningEventTarget->IsOnCurrentThread()) {
+ InvalidateAndAllowToCloseOnOwningThread();
+ return;
+ }
+
+ // Dispatch is guaranteed to succeed here because we block shutdown until
+ // all Contexts have been destroyed.
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
+ "dom::cache::Context::ThreadsafeHandle::"
+ "InvalidateAndAllowToCloseOnOwningThread",
+ this, &ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread);
+ MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(runnable.forget(),
+ nsIThread::DISPATCH_NORMAL));
+}
+
+Context::ThreadsafeHandle::ThreadsafeHandle(SafeRefPtr<Context> aContext)
+ : mStrongRef(std::move(aContext)),
+ mWeakRef(mStrongRef.unsafeGetRawPtr()),
+ mOwningEventTarget(GetCurrentSerialEventTarget()) {}
+
+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 || mOwningEventTarget->IsOnCurrentThread()) {
+ return;
+ }
+
+ // Dispatch is guaranteed to succeed here because we block shutdown until
+ // all Contexts have been destroyed.
+ NS_ProxyRelease("Context::ThreadsafeHandle::mStrongRef", mOwningEventTarget,
+ mStrongRef.forget());
+}
+
+void Context::ThreadsafeHandle::AllowToCloseOnOwningThread() {
+ MOZ_ASSERT(mOwningEventTarget->IsOnCurrentThread());
+
+ // 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(mOwningEventTarget->IsOnCurrentThread());
+ // 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(mOwningEventTarget->IsOnCurrentThread());
+ MOZ_DIAGNOSTIC_ASSERT(!mStrongRef);
+ MOZ_DIAGNOSTIC_ASSERT(mWeakRef);
+ MOZ_DIAGNOSTIC_ASSERT(mWeakRef == &aContext);
+ mWeakRef = nullptr;
+}
+
+// static
+SafeRefPtr<Context> Context::Create(SafeRefPtr<Manager> aManager,
+ nsISerialEventTarget* aTarget,
+ SafeRefPtr<Action> aInitAction,
+ Maybe<Context&> aOldContext) {
+ auto context = MakeSafeRefPtr<Context>(std::move(aManager), aTarget,
+ std::move(aInitAction));
+ context->Init(aOldContext);
+ return context;
+}
+
+Context::Context(SafeRefPtr<Manager> aManager, nsISerialEventTarget* aTarget,
+ SafeRefPtr<Action> aInitAction)
+ : mManager(std::move(aManager)),
+ mTarget(aTarget),
+ mData(new Data(aTarget)),
+ mState(STATE_CONTEXT_PREINIT),
+ mOrphanedData(false),
+ mInitAction(std::move(aInitAction)) {
+ MOZ_DIAGNOSTIC_ASSERT(mManager);
+ MOZ_DIAGNOSTIC_ASSERT(mTarget);
+}
+
+void Context::Dispatch(SafeRefPtr<Action> aAction) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+ MOZ_DIAGNOSTIC_ASSERT(aAction);
+ MOZ_DIAGNOSTIC_ASSERT(mState != STATE_CONTEXT_CANCELED);
+
+ if (mState == STATE_CONTEXT_CANCELED) {
+ return;
+ }
+
+ if (mState == STATE_CONTEXT_INIT || mState == STATE_CONTEXT_PREINIT) {
+ PendingAction* pending = mPendingActions.AppendElement();
+ pending->mAction = std::move(aAction);
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_READY);
+ DispatchAction(std::move(aAction));
+}
+
+Maybe<DirectoryLock&> Context::MaybeDirectoryLockRef() const {
+ NS_ASSERT_OWNINGTHREAD(Context);
+
+ if (mState == STATE_CONTEXT_PREINIT) {
+ MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);
+ MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);
+
+ return Nothing();
+ }
+
+ if (mState == STATE_CONTEXT_INIT) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);
+
+ return mInitRunnable->MaybeDirectoryLockRef();
+ }
+
+ return ToMaybeRef(mDirectoryLock.get());
+}
+
+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();
+ for (const auto& activity : mActivityList.ForwardRange()) {
+ activity->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
+ mPendingActions.RemoveElementsBy([aCacheId](const auto& pendingAction) {
+ return pendingAction.mAction->MatchesCacheId(aCacheId);
+ });
+
+ // Cancel activities and let them remove themselves
+ for (const auto& activity : mActivityList.ForwardRange()) {
+ 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(Maybe<Context&> aOldContext) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+
+ if (aOldContext) {
+ aOldContext->SetNextContext(SafeRefPtrFromThis());
+ 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);
+ // If we can't initialize the quota subsystem we will never be able to
+ // clear our shared data object via the target IO thread. Instead just
+ // clear it here to maintain the invariant that the shared data is
+ // cleared before Context destruction.
+ mData = nullptr;
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_PREINIT);
+ MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);
+
+ mInitRunnable =
+ new QuotaInitRunnable(SafeRefPtrFromThis(), mManager.clonePtr(), mData,
+ mTarget, std::move(mInitAction));
+ 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(SafeRefPtr<Action> aAction, bool aDoomData) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+
+ auto runnable = MakeSafeRefPtr<ActionRunnable>(
+ SafeRefPtrFromThis(), mData, mTarget, std::move(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(std::move(mPendingActions[i].mAction));
+ }
+ mPendingActions.Clear();
+}
+
+void Context::AddActivity(Activity& aActivity) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+ MOZ_ASSERT(!mActivityList.Contains(&aActivity));
+ mActivityList.AppendElement(WrapNotNullUnchecked(&aActivity));
+}
+
+void Context::RemoveActivity(Activity& aActivity) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+ 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;
+}
+
+SafeRefPtr<Context::ThreadsafeHandle> Context::CreateThreadsafeHandle() {
+ NS_ASSERT_OWNINGTHREAD(Context);
+ if (!mThreadsafeHandle) {
+ mThreadsafeHandle = MakeSafeRefPtr<ThreadsafeHandle>(SafeRefPtrFromThis());
+ }
+ return mThreadsafeHandle.clonePtr();
+}
+
+void Context::SetNextContext(SafeRefPtr<Context> aNextContext) {
+ NS_ASSERT_OWNINGTHREAD(Context);
+ MOZ_DIAGNOSTIC_ASSERT(aNextContext);
+ MOZ_DIAGNOSTIC_ASSERT(!mNextContext);
+ mNextContext = std::move(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.
+ DispatchAction(MakeSafeRefPtr<NullAction>(), true /* doomed data */);
+
+ MOZ_DIAGNOSTIC_ASSERT(!mData);
+}
+
+} // namespace mozilla::dom::cache