diff options
Diffstat (limited to 'dom/cache/Manager.cpp')
-rw-r--r-- | dom/cache/Manager.cpp | 2128 |
1 files changed, 2128 insertions, 0 deletions
diff --git a/dom/cache/Manager.cpp b/dom/cache/Manager.cpp new file mode 100644 index 0000000000..c1bdb443d8 --- /dev/null +++ b/dom/cache/Manager.cpp @@ -0,0 +1,2128 @@ +/* -*- 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/Manager.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/cache/Context.h" +#include "mozilla/dom/cache/DBAction.h" +#include "mozilla/dom/cache/DBSchema.h" +#include "mozilla/dom/cache/FileUtils.h" +#include "mozilla/dom/cache/ManagerId.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/dom/cache/SavedTypes.h" +#include "mozilla/dom/cache/StreamList.h" +#include "mozilla/dom/cache/Types.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozStorageHelper.h" +#include "nsIInputStream.h" +#include "nsID.h" +#include "nsIFile.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "nsTObserverArray.h" +#include "QuotaClientImpl.h" + +namespace mozilla::dom::cache { + +using mozilla::dom::quota::Client; +using mozilla::dom::quota::CloneFileAndAppend; +using mozilla::dom::quota::DirectoryLock; + +namespace { + +/** + * Note: The aCommitHook argument will be invoked while a lock is held. Callers + * should be careful not to pass a hook that might lock on something else and + * trigger a deadlock. + */ +template <typename Callable> +nsresult MaybeUpdatePaddingFile(nsIFile* aBaseDir, mozIStorageConnection* aConn, + const int64_t aIncreaseSize, + const int64_t aDecreaseSize, + Callable aCommitHook) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aBaseDir); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aIncreaseSize >= 0); + MOZ_DIAGNOSTIC_ASSERT(aDecreaseSize >= 0); + + RefPtr<CacheQuotaClient> cacheQuotaClient = CacheQuotaClient::Get(); + MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient); + + QM_TRY(MOZ_TO_RESULT(cacheQuotaClient->MaybeUpdatePaddingFileInternal( + *aBaseDir, *aConn, aIncreaseSize, aDecreaseSize, aCommitHook))); + + return NS_OK; +} + +// An Action that is executed when a Context is first created. It ensures that +// the directory and database are setup properly. This lets other actions +// not worry about these details. +class SetupAction final : public SyncDBAction { + public: + SetupAction() : SyncDBAction(DBAction::Create) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + QM_TRY(MOZ_TO_RESULT(BodyCreateDir(*aDBDir))); + + // executes in its own transaction + QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(*aConn))); + + // If the Context marker file exists, then the last session was + // not cleanly shutdown. In these cases sqlite will ensure that + // the database is valid, but we might still orphan data. Both + // Cache objects and body files can be referenced by DOM objects + // after they are "removed" from their parent. So we need to + // look and see if any of these late access objects have been + // orphaned. + // + // Note, this must be done after any schema version updates to + // ensure our DBSchema methods work correctly. + if (MarkerFileExists(aDirectoryMetadata)) { + NS_WARNING("Cache not shutdown cleanly! Cleaning up stale data..."); + mozStorageTransaction trans(aConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start())); + + // Clean up orphaned Cache objects + QM_TRY_INSPECT(const auto& orphanedCacheIdList, + db::FindOrphanedCacheIds(*aConn)); + + QM_TRY_INSPECT( + const CheckedInt64& overallDeletedPaddingSize, + Reduce( + orphanedCacheIdList, CheckedInt64(0), + [aConn, &aDirectoryMetadata, &aDBDir]( + CheckedInt64 oldValue, const Maybe<const CacheId&>& element) + -> Result<CheckedInt64, nsresult> { + QM_TRY_INSPECT(const auto& deletionInfo, + db::DeleteCacheId(*aConn, *element)); + + QM_TRY(MOZ_TO_RESULT( + BodyDeleteFiles(aDirectoryMetadata, *aDBDir, + deletionInfo.mDeletedBodyIdList))); + + if (deletionInfo.mDeletedPaddingSize > 0) { + DecreaseUsageForDirectoryMetadata( + aDirectoryMetadata, deletionInfo.mDeletedPaddingSize); + } + + return oldValue + deletionInfo.mDeletedPaddingSize; + })); + + // Clean up orphaned body objects + QM_TRY_INSPECT(const auto& knownBodyIdList, db::GetKnownBodyIds(*aConn)); + + QM_TRY(MOZ_TO_RESULT(BodyDeleteOrphanedFiles(aDirectoryMetadata, *aDBDir, + knownBodyIdList))); + + // Commit() explicitly here, because we want to ensure the padding file + // has the correct content. + // We'll restore padding file below, so just warn here if failure happens. + // + // XXX Before, if MaybeUpdatePaddingFile failed but we didn't enter the if + // body below, we would have propagated the MaybeUpdatePaddingFile + // failure, but if we entered it and RestorePaddingFile succeeded, we + // would have returned NS_OK. Now, we will never propagate a + // MaybeUpdatePaddingFile failure. + QM_WARNONLY_TRY(QM_TO_RESULT( + MaybeUpdatePaddingFile(aDBDir, aConn, /* aIncreaceSize */ 0, + overallDeletedPaddingSize.value(), + [&trans]() { return trans.Commit(); }))); + } + + if (DirectoryPaddingFileExists(*aDBDir, DirPaddingFile::TMP_FILE) || + !DirectoryPaddingFileExists(*aDBDir, DirPaddingFile::FILE)) { + QM_TRY(MOZ_TO_RESULT(RestorePaddingFile(aDBDir, aConn))); + } + + return NS_OK; + } +}; + +// ---------------------------------------------------------------------------- + +// Action that is executed when we determine that content has stopped using +// a body file that has been orphaned. +class DeleteOrphanedBodyAction final : public Action { + public: + using DeletedBodyIdList = AutoTArray<nsID, 64>; + + explicit DeleteOrphanedBodyAction(DeletedBodyIdList&& aDeletedBodyIdList) + : mDeletedBodyIdList(std::move(aDeletedBodyIdList)) {} + + explicit DeleteOrphanedBodyAction(const nsID& aBodyId) + : mDeletedBodyIdList{aBodyId} {} + + void RunOnTarget(SafeRefPtr<Resolver> aResolver, + const Maybe<CacheDirectoryMetadata>& aDirectoryMetadata, + Data*) override { + MOZ_DIAGNOSTIC_ASSERT(aResolver); + MOZ_DIAGNOSTIC_ASSERT(aDirectoryMetadata); + MOZ_DIAGNOSTIC_ASSERT(aDirectoryMetadata->mDir); + + // Note that since DeleteOrphanedBodyAction isn't used while the context is + // being initialized, we don't need to check for cancellation here. + + const auto resolve = [&aResolver](const nsresult rv) { + aResolver->Resolve(rv); + }; + + QM_TRY_INSPECT(const auto& dbDir, + CloneFileAndAppend(*aDirectoryMetadata->mDir, u"cache"_ns), + QM_VOID, resolve); + + QM_TRY(MOZ_TO_RESULT(BodyDeleteFiles(*aDirectoryMetadata, *dbDir, + mDeletedBodyIdList)), + QM_VOID, resolve); + + aResolver->Resolve(NS_OK); + } + + private: + DeletedBodyIdList mDeletedBodyIdList; +}; + +bool IsHeadRequest(const CacheRequest& aRequest, + const CacheQueryParams& aParams) { + return !aParams.ignoreMethod() && + aRequest.method().LowerCaseEqualsLiteral("head"); +} + +bool IsHeadRequest(const Maybe<CacheRequest>& aRequest, + const CacheQueryParams& aParams) { + if (aRequest.isSome()) { + return !aParams.ignoreMethod() && + aRequest.ref().method().LowerCaseEqualsLiteral("head"); + } + return false; +} + +auto MatchByCacheId(CacheId aCacheId) { + return [aCacheId](const auto& entry) { return entry.mCacheId == aCacheId; }; +} + +auto MatchByBodyId(const nsID& aBodyId) { + return [&aBodyId](const auto& entry) { return entry.mBodyId == aBodyId; }; +} + +} // namespace + +// ---------------------------------------------------------------------------- + +// Singleton class to track Manager instances and ensure there is only +// one for each unique ManagerId. +class Manager::Factory { + public: + friend class StaticAutoPtr<Manager::Factory>; + + static Result<SafeRefPtr<Manager>, nsresult> AcquireCreateIfNonExistent( + const SafeRefPtr<ManagerId>& aManagerId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + // If we get here during/after quota manager shutdown, we bail out. + MOZ_ASSERT(AppShutdown::GetCurrentShutdownPhase() < + ShutdownPhase::AppShutdownQM); + if (AppShutdown::GetCurrentShutdownPhase() >= + ShutdownPhase::AppShutdownQM) { + NS_WARNING( + "Attempt to AcquireCreateIfNonExistent a Manager during QM " + "shutdown."); + return Err(NS_ERROR_ILLEGAL_DURING_SHUTDOWN); + } + + // Ensure there is a factory instance. This forces the Acquire() call + // below to use the same factory. + QM_TRY(MOZ_TO_RESULT(MaybeCreateInstance())); + + SafeRefPtr<Manager> ref = Acquire(*aManagerId); + if (!ref) { + // TODO: replace this with a thread pool (bug 1119864) + // XXX Can't use QM_TRY_INSPECT because that causes a clang-plugin + // error of the NoNewThreadsChecker. + nsCOMPtr<nsIThread> ioThread; + QM_TRY(MOZ_TO_RESULT( + NS_NewNamedThread("DOMCacheThread", getter_AddRefs(ioThread)))); + + ref = MakeSafeRefPtr<Manager>(aManagerId.clonePtr(), ioThread, + ConstructorGuard{}); + + // There may be an old manager for this origin in the process of + // cleaning up. We need to tell the new manager about this so + // that it won't actually start until the old manager is done. + const SafeRefPtr<Manager> oldManager = Acquire(*aManagerId, Closing); + ref->Init(oldManager.maybeDeref()); + + MOZ_ASSERT(!sFactory->mManagerList.Contains(ref)); + sFactory->mManagerList.AppendElement( + WrapNotNullUnchecked(ref.unsafeGetRawPtr())); + } + + return ref; + } + + static void Remove(Manager& aManager) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(sFactory); + + MOZ_ALWAYS_TRUE(sFactory->mManagerList.RemoveElement(&aManager)); + + // This might both happen in late shutdown such that this event + // is executed even after the QuotaManager singleton passed away + // or if the QuotaManager has not yet been created. + quota::QuotaManager::SafeMaybeRecordQuotaClientShutdownStep( + quota::Client::DOMCACHE, "Manager removed"_ns); + + // clean up the factory singleton if there are no more managers + MaybeDestroyInstance(); + } + + static void Abort(const Client::DirectoryLockIdTable& aDirectoryLockIds) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + AbortMatching([&aDirectoryLockIds](const auto& manager) { + // Check if the Manager holds an acquired DirectoryLock. Origin clearing + // can't be blocked by this Manager if there is no acquired DirectoryLock. + // If there is an acquired DirectoryLock, check if the table contains the + // lock for the Manager. + return Client::IsLockForObjectAcquiredAndContainedInLockTable( + manager, aDirectoryLockIds); + }); + } + + static void AbortAll() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + AbortMatching([](const auto&) { return true; }); + } + + static void ShutdownAll() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!sFactory) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!sFactory->mManagerList.IsEmpty()); + + { + // Note that we are synchronously calling shutdown code here. If any + // of the shutdown code synchronously decides to delete the Factory + // we need to delay that delete until the end of this method. + AutoRestore<bool> restore(sFactory->mInSyncAbortOrShutdown); + sFactory->mInSyncAbortOrShutdown = true; + + for (const auto& manager : sFactory->mManagerList.ForwardRange()) { + auto pinnedManager = + SafeRefPtr{manager.get(), AcquireStrongRefFromRawPtr{}}; + pinnedManager->Shutdown(); + } + } + + MaybeDestroyInstance(); + } + + static bool IsShutdownAllComplete() { + mozilla::ipc::AssertIsOnBackgroundThread(); + return !sFactory; + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + static void RecordMayNotDeleteCSCP(int32_t aCacheStreamControlParentId) { + if (sFactory) { + sFactory->mPotentiallyUnreleasedCSCP.AppendElement( + aCacheStreamControlParentId); + } + } + + static void RecordHaveDeletedCSCP(int32_t aCacheStreamControlParentId) { + if (sFactory) { + sFactory->mPotentiallyUnreleasedCSCP.RemoveElement( + aCacheStreamControlParentId); + } + } +#endif + static nsCString GetShutdownStatus() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + nsCString data; + + if (sFactory && !sFactory->mManagerList.IsEmpty()) { + data.Append( + "Managers: "_ns + + IntToCString(static_cast<uint64_t>(sFactory->mManagerList.Length())) + + " ("_ns); + + for (const auto& manager : sFactory->mManagerList.NonObservingRange()) { + data.Append(quota::AnonymizedOriginString( + manager->GetManagerId().QuotaOrigin())); + + data.AppendLiteral(": "); + + data.Append(manager->GetState() == State::Open ? "Open"_ns + : "Closing"_ns); + + data.AppendLiteral(", "); + } + + data.AppendLiteral(" ) "); + if (sFactory->mPotentiallyUnreleasedCSCP.Length() > 0) { + data.Append( + "There have been CSCP instances whose" + "Send__delete__ might not have freed them."); + } + } + + return data; + } + + private: + Factory() : mInSyncAbortOrShutdown(false) { + MOZ_COUNT_CTOR(cache::Manager::Factory); + } + + ~Factory() { + MOZ_COUNT_DTOR(cache::Manager::Factory); + MOZ_DIAGNOSTIC_ASSERT(mManagerList.IsEmpty()); + MOZ_DIAGNOSTIC_ASSERT(!mInSyncAbortOrShutdown); + } + + static nsresult MaybeCreateInstance() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!sFactory) { + // We cannot use ClearOnShutdown() here because we're not on the main + // thread. Instead, we delete sFactory in Factory::Remove() after the + // last manager is removed. ShutdownObserver ensures this happens + // before shutdown. + sFactory = new Factory(); + } + + // Never return sFactory to code outside Factory. We need to delete it + // out from under ourselves just before we return from Remove(). This + // would be (even more) dangerous if other code had a pointer to the + // factory itself. + + return NS_OK; + } + + static void MaybeDestroyInstance() { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(sFactory); + + // If the factory is is still in use then we cannot delete yet. This + // could be due to managers still existing or because we are in the + // middle of aborting or shutting down. We need to be careful not to delete + // ourself synchronously during shutdown. + if (!sFactory->mManagerList.IsEmpty() || sFactory->mInSyncAbortOrShutdown) { + return; + } + + sFactory = nullptr; + } + + static SafeRefPtr<Manager> Acquire(const ManagerId& aManagerId, + State aState = Open) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + QM_TRY(MOZ_TO_RESULT(MaybeCreateInstance()), nullptr); + + // Iterate in reverse to find the most recent, matching Manager. This + // is important when looking for a Closing Manager. If a new Manager + // chains to an old Manager we want it to be the most recent one. + const auto range = Reversed(sFactory->mManagerList.NonObservingRange()); + const auto foundIt = std::find_if( + range.begin(), range.end(), [aState, &aManagerId](const auto& manager) { + return aState == manager->GetState() && + *manager->mManagerId == aManagerId; + }); + return foundIt != range.end() + ? SafeRefPtr{foundIt->get(), AcquireStrongRefFromRawPtr{}} + : nullptr; + } + + template <typename Condition> + static void AbortMatching(const Condition& aCondition) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!sFactory) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!sFactory->mManagerList.IsEmpty()); + + { + // Note that we are synchronously calling abort code here. If any + // of the shutdown code synchronously decides to delete the Factory + // we need to delay that delete until the end of this method. + AutoRestore<bool> restore(sFactory->mInSyncAbortOrShutdown); + sFactory->mInSyncAbortOrShutdown = true; + + for (const auto& manager : sFactory->mManagerList.ForwardRange()) { + if (aCondition(*manager)) { + auto pinnedManager = + SafeRefPtr{manager.get(), AcquireStrongRefFromRawPtr{}}; + pinnedManager->Abort(); + } + } + } + + MaybeDestroyInstance(); + } + + // Singleton created on demand and deleted when last Manager is cleared + // in Remove(). + // PBackground thread only. + static StaticAutoPtr<Factory> sFactory; + + // Weak references as we don't want to keep Manager objects alive forever. + // When a Manager is destroyed it calls Factory::Remove() to clear itself. + // PBackground thread only. + nsTObserverArray<NotNull<Manager*>> mManagerList; + + // This flag is set when we are looping through the list and calling Abort() + // or Shutdown() on each Manager. We need to be careful not to synchronously + // trigger the deletion of the factory while still executing this loop. + bool mInSyncAbortOrShutdown; + + nsTArray<int32_t> mPotentiallyUnreleasedCSCP; +}; + +// static +StaticAutoPtr<Manager::Factory> Manager::Factory::sFactory; + +// ---------------------------------------------------------------------------- + +// Abstract class to help implement the various Actions. The vast majority +// of Actions are synchronous and need to report back to a Listener on the +// Manager. +class Manager::BaseAction : public SyncDBAction { + protected: + BaseAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId) + : SyncDBAction(DBAction::Existing), + mManager(std::move(aManager)), + mListenerId(aListenerId) {} + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) = 0; + + virtual void CompleteOnInitiatingThread(nsresult aRv) override { + NS_ASSERT_OWNINGTHREAD(Manager::BaseAction); + Listener* listener = mManager->GetListener(mListenerId); + if (listener) { + Complete(listener, ErrorResult(aRv)); + } + + // ensure we release the manager on the initiating thread + mManager = nullptr; + } + + SafeRefPtr<Manager> mManager; + const ListenerId mListenerId; +}; + +// ---------------------------------------------------------------------------- + +// Action that is executed when we determine that content has stopped using +// a Cache object that has been orphaned. +class Manager::DeleteOrphanedCacheAction final : public SyncDBAction { + public: + DeleteOrphanedCacheAction(SafeRefPtr<Manager> aManager, CacheId aCacheId) + : SyncDBAction(DBAction::Existing), + mManager(std::move(aManager)), + mCacheId(aCacheId) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + mDirectoryMetadata.emplace(aDirectoryMetadata); + + mozStorageTransaction trans(aConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start())); + + QM_TRY_UNWRAP(mDeletionInfo, db::DeleteCacheId(*aConn, mCacheId)); + + QM_TRY(MOZ_TO_RESULT(MaybeUpdatePaddingFile( + aDBDir, aConn, /* aIncreaceSize */ 0, mDeletionInfo.mDeletedPaddingSize, + [&trans]() mutable { return trans.Commit(); }))); + + return NS_OK; + } + + virtual void CompleteOnInitiatingThread(nsresult aRv) override { + // If the transaction fails, we shouldn't delete the body files and decrease + // their padding size. + if (NS_FAILED(aRv)) { + mDeletionInfo.mDeletedBodyIdList.Clear(); + mDeletionInfo.mDeletedPaddingSize = 0; + } + + mManager->NoteOrphanedBodyIdList(mDeletionInfo.mDeletedBodyIdList); + + if (mDeletionInfo.mDeletedPaddingSize > 0) { + DecreaseUsageForDirectoryMetadata(*mDirectoryMetadata, + mDeletionInfo.mDeletedPaddingSize); + } + + // ensure we release the manager on the initiating thread + mManager = nullptr; + } + + private: + SafeRefPtr<Manager> mManager; + const CacheId mCacheId; + DeletionInfo mDeletionInfo; + Maybe<CacheDirectoryMetadata> mDirectoryMetadata; +}; + +// ---------------------------------------------------------------------------- + +class Manager::CacheMatchAction final : public Manager::BaseAction { + public: + CacheMatchAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + CacheId aCacheId, const CacheMatchArgs& aArgs, + SafeRefPtr<StreamList> aStreamList) + : BaseAction(std::move(aManager), aListenerId), + mCacheId(aCacheId), + mArgs(aArgs), + mStreamList(std::move(aStreamList)), + mFoundResponse(false) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + QM_TRY_INSPECT( + const auto& maybeResponse, + db::CacheMatch(*aConn, mCacheId, mArgs.request(), mArgs.params())); + + mFoundResponse = maybeResponse.isSome(); + if (mFoundResponse) { + mResponse = std::move(maybeResponse.ref()); + } + + if (!mFoundResponse || !mResponse.mHasBodyId || + IsHeadRequest(mArgs.request(), mArgs.params())) { + mResponse.mHasBodyId = false; + return NS_OK; + } + + nsCOMPtr<nsIInputStream> stream; + if (mArgs.openMode() == OpenMode::Eager) { + QM_TRY_UNWRAP(stream, + BodyOpen(aDirectoryMetadata, *aDBDir, mResponse.mBodyId)); + } + + mStreamList->Add(mResponse.mBodyId, std::move(stream)); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + if (!mFoundResponse) { + aListener->OnOpComplete(std::move(aRv), CacheMatchResult(Nothing())); + } else { + mStreamList->Activate(mCacheId); + aListener->OnOpComplete(std::move(aRv), CacheMatchResult(Nothing()), + mResponse, *mStreamList); + } + mStreamList = nullptr; + } + + virtual bool MatchesCacheId(CacheId aCacheId) const override { + return aCacheId == mCacheId; + } + + private: + const CacheId mCacheId; + const CacheMatchArgs mArgs; + SafeRefPtr<StreamList> mStreamList; + bool mFoundResponse; + SavedResponse mResponse; +}; + +// ---------------------------------------------------------------------------- + +class Manager::CacheMatchAllAction final : public Manager::BaseAction { + public: + CacheMatchAllAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + CacheId aCacheId, const CacheMatchAllArgs& aArgs, + SafeRefPtr<StreamList> aStreamList) + : BaseAction(std::move(aManager), aListenerId), + mCacheId(aCacheId), + mArgs(aArgs), + mStreamList(std::move(aStreamList)) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + QM_TRY_UNWRAP(mSavedResponses, + db::CacheMatchAll(*aConn, mCacheId, mArgs.maybeRequest(), + mArgs.params())); + + for (uint32_t i = 0; i < mSavedResponses.Length(); ++i) { + if (!mSavedResponses[i].mHasBodyId || + IsHeadRequest(mArgs.maybeRequest(), mArgs.params())) { + mSavedResponses[i].mHasBodyId = false; + continue; + } + + nsCOMPtr<nsIInputStream> stream; + if (mArgs.openMode() == OpenMode::Eager) { + QM_TRY_UNWRAP(stream, BodyOpen(aDirectoryMetadata, *aDBDir, + mSavedResponses[i].mBodyId)); + } + + mStreamList->Add(mSavedResponses[i].mBodyId, std::move(stream)); + } + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + mStreamList->Activate(mCacheId); + aListener->OnOpComplete(std::move(aRv), CacheMatchAllResult(), + mSavedResponses, *mStreamList); + mStreamList = nullptr; + } + + virtual bool MatchesCacheId(CacheId aCacheId) const override { + return aCacheId == mCacheId; + } + + private: + const CacheId mCacheId; + const CacheMatchAllArgs mArgs; + SafeRefPtr<StreamList> mStreamList; + nsTArray<SavedResponse> mSavedResponses; +}; + +// ---------------------------------------------------------------------------- + +// This is the most complex Action. It puts a request/response pair into the +// Cache. It does not complete until all of the body data has been saved to +// disk. This means its an asynchronous Action. +class Manager::CachePutAllAction final : public DBAction { + public: + CachePutAllAction( + SafeRefPtr<Manager> aManager, ListenerId aListenerId, CacheId aCacheId, + const nsTArray<CacheRequestResponse>& aPutList, + const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreamList, + const nsTArray<nsCOMPtr<nsIInputStream>>& aResponseStreamList) + : DBAction(DBAction::Existing), + mManager(std::move(aManager)), + mListenerId(aListenerId), + mCacheId(aCacheId), + mList(aPutList.Length()), + mExpectedAsyncCopyCompletions(1), + mAsyncResult(NS_OK), + mMutex("cache::Manager::CachePutAllAction"), + mUpdatedPaddingSize(0), + mDeletedPaddingSize(0) { + MOZ_DIAGNOSTIC_ASSERT(!aPutList.IsEmpty()); + MOZ_DIAGNOSTIC_ASSERT(aPutList.Length() == aRequestStreamList.Length()); + MOZ_DIAGNOSTIC_ASSERT(aPutList.Length() == aResponseStreamList.Length()); + + for (uint32_t i = 0; i < aPutList.Length(); ++i) { + Entry* entry = mList.AppendElement(); + entry->mRequest = aPutList[i].request(); + entry->mRequestStream = aRequestStreamList[i]; + entry->mResponse = aPutList[i].response(); + entry->mResponseStream = aResponseStreamList[i]; + } + } + + private: + ~CachePutAllAction() = default; + + virtual void RunWithDBOnTarget( + SafeRefPtr<Resolver> aResolver, + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aResolver); + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(!mResolver); + MOZ_DIAGNOSTIC_ASSERT(!mDBDir); + MOZ_DIAGNOSTIC_ASSERT(!mConn); + + MOZ_DIAGNOSTIC_ASSERT(!mTarget); + mTarget = GetCurrentSerialEventTarget(); + MOZ_DIAGNOSTIC_ASSERT(mTarget); + + // We should be pre-initialized to expect one async completion. This is + // the "manual" completion we call at the end of this method in all + // cases. + MOZ_DIAGNOSTIC_ASSERT(mExpectedAsyncCopyCompletions == 1); + + mResolver = std::move(aResolver); + mDBDir = aDBDir; + mConn = aConn; + mDirectoryMetadata.emplace(aDirectoryMetadata); + + // File bodies are streamed to disk via asynchronous copying. Start + // this copying now. Each copy will eventually result in a call + // to OnAsyncCopyComplete(). + const nsresult rv = [this, &aDirectoryMetadata]() -> nsresult { + QM_TRY(CollectEachInRange( + mList, [this, &aDirectoryMetadata](auto& entry) -> nsresult { + QM_TRY(MOZ_TO_RESULT( + StartStreamCopy(aDirectoryMetadata, entry, RequestStream, + &mExpectedAsyncCopyCompletions))); + + QM_TRY(MOZ_TO_RESULT( + StartStreamCopy(aDirectoryMetadata, entry, ResponseStream, + &mExpectedAsyncCopyCompletions))); + + return NS_OK; + })); + + return NS_OK; + }(); + + // Always call OnAsyncCopyComplete() manually here. This covers the + // case where there is no async copying and also reports any startup + // errors correctly. If we hit an error, then OnAsyncCopyComplete() + // will cancel any async copying. + OnAsyncCopyComplete(rv); + } + + // Called once for each asynchronous file copy whether it succeeds or + // fails. If a file copy is canceled, it still calls this method with + // an error code. + void OnAsyncCopyComplete(nsresult aRv) { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(mConn); + MOZ_DIAGNOSTIC_ASSERT(mResolver); + MOZ_DIAGNOSTIC_ASSERT(mExpectedAsyncCopyCompletions > 0); + + // Explicitly check for cancellation here to catch a race condition. + // Consider: + // + // 1) NS_AsyncCopy() executes on IO thread, but has not saved its + // copy context yet. + // 2) CancelAllStreamCopying() occurs on PBackground thread + // 3) Copy context from (1) is saved on IO thread. + // + // Checking for cancellation here catches this condition when we + // first call OnAsyncCopyComplete() manually from RunWithDBOnTarget(). + // + // This explicit cancellation check also handles the case where we + // are canceled just after all stream copying completes. We should + // abort the synchronous DB operations in this case if we have not + // started them yet. + if (NS_SUCCEEDED(aRv) && IsCanceled()) { + aRv = NS_ERROR_ABORT; + } + + // If any of the async copies fail, we need to still wait for them all to + // complete. Cancel any other streams still working and remember the + // error. All canceled streams will call OnAsyncCopyComplete(). + if (NS_FAILED(aRv) && NS_SUCCEEDED(mAsyncResult)) { + CancelAllStreamCopying(); + mAsyncResult = aRv; + } + + // Check to see if async copying is still on-going. If so, then simply + // return for now. We must wait for a later OnAsyncCopyComplete() call. + mExpectedAsyncCopyCompletions -= 1; + if (mExpectedAsyncCopyCompletions > 0) { + return; + } + + // We have finished with all async copying. Indicate this by clearing all + // our copy contexts. + { + MutexAutoLock lock(mMutex); + mCopyContextList.Clear(); + } + + // An error occurred while async copying. Terminate the Action. + // DoResolve() will clean up any files we may have written. + if (NS_FAILED(mAsyncResult)) { + DoResolve(mAsyncResult); + return; + } + + mozStorageTransaction trans(mConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start()), QM_VOID); + + const nsresult rv = [this, &trans]() -> nsresult { + QM_TRY(CollectEachInRange(mList, [this](Entry& e) -> nsresult { + if (e.mRequestStream) { + QM_TRY(MOZ_TO_RESULT(BodyFinalizeWrite(*mDBDir, e.mRequestBodyId))); + } + if (e.mResponseStream) { + // Gerenate padding size for opaque response if needed. + if (e.mResponse.type() == ResponseType::Opaque) { + // It'll generate padding if we've not set it yet. + QM_TRY(MOZ_TO_RESULT(BodyMaybeUpdatePaddingSize( + *mDirectoryMetadata, *mDBDir, e.mResponseBodyId, + e.mResponse.paddingInfo(), &e.mResponse.paddingSize()))); + + MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - e.mResponse.paddingSize() >= + mUpdatedPaddingSize); + mUpdatedPaddingSize += e.mResponse.paddingSize(); + } + + QM_TRY(MOZ_TO_RESULT(BodyFinalizeWrite(*mDBDir, e.mResponseBodyId))); + } + + QM_TRY_UNWRAP( + auto deletionInfo, + db::CachePut(*mConn, mCacheId, e.mRequest, + e.mRequestStream ? &e.mRequestBodyId : nullptr, + e.mResponse, + e.mResponseStream ? &e.mResponseBodyId : nullptr)); + + const int64_t deletedPaddingSize = deletionInfo.mDeletedPaddingSize; + mDeletedBodyIdList = std::move(deletionInfo.mDeletedBodyIdList); + + MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - mDeletedPaddingSize >= + deletedPaddingSize); + mDeletedPaddingSize += deletedPaddingSize; + + return NS_OK; + })); + + // Update padding file when it's necessary + QM_TRY(MOZ_TO_RESULT(MaybeUpdatePaddingFile( + mDBDir, mConn, mUpdatedPaddingSize, mDeletedPaddingSize, + [&trans]() mutable { return trans.Commit(); }))); + + return NS_OK; + }(); + + DoResolve(rv); + } + + virtual void CompleteOnInitiatingThread(nsresult aRv) override { + NS_ASSERT_OWNINGTHREAD(Action); + + for (uint32_t i = 0; i < mList.Length(); ++i) { + mList[i].mRequestStream = nullptr; + mList[i].mResponseStream = nullptr; + } + + // If the transaction fails, we shouldn't delete the body files and decrease + // their padding size. + if (NS_FAILED(aRv)) { + mDeletedBodyIdList.Clear(); + mDeletedPaddingSize = 0; + } + + mManager->NoteOrphanedBodyIdList(mDeletedBodyIdList); + + if (mDeletedPaddingSize > 0) { + DecreaseUsageForDirectoryMetadata(*mDirectoryMetadata, + mDeletedPaddingSize); + } + + Listener* listener = mManager->GetListener(mListenerId); + mManager = nullptr; + if (listener) { + listener->OnOpComplete(ErrorResult(aRv), CachePutAllResult()); + } + } + + virtual void CancelOnInitiatingThread() override { + NS_ASSERT_OWNINGTHREAD(Action); + Action::CancelOnInitiatingThread(); + CancelAllStreamCopying(); + } + + virtual bool MatchesCacheId(CacheId aCacheId) const override { + NS_ASSERT_OWNINGTHREAD(Action); + return aCacheId == mCacheId; + } + + struct Entry { + CacheRequest mRequest; + nsCOMPtr<nsIInputStream> mRequestStream; + nsID mRequestBodyId; + nsCOMPtr<nsISupports> mRequestCopyContext; + + CacheResponse mResponse; + nsCOMPtr<nsIInputStream> mResponseStream; + nsID mResponseBodyId; + nsCOMPtr<nsISupports> mResponseCopyContext; + }; + + enum StreamId { RequestStream, ResponseStream }; + + nsresult StartStreamCopy(const CacheDirectoryMetadata& aDirectoryMetadata, + Entry& aEntry, StreamId aStreamId, + uint32_t* aCopyCountOut) { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(aCopyCountOut); + + if (IsCanceled()) { + return NS_ERROR_ABORT; + } + + MOZ_DIAGNOSTIC_ASSERT(aStreamId == RequestStream || + aStreamId == ResponseStream); + + const auto& source = aStreamId == RequestStream ? aEntry.mRequestStream + : aEntry.mResponseStream; + + if (!source) { + return NS_OK; + } + + QM_TRY_INSPECT((const auto& [bodyId, copyContext]), + BodyStartWriteStream(aDirectoryMetadata, *mDBDir, *source, + this, AsyncCopyCompleteFunc)); + + if (aStreamId == RequestStream) { + aEntry.mRequestBodyId = bodyId; + } else { + aEntry.mResponseBodyId = bodyId; + } + + mBodyIdWrittenList.AppendElement(bodyId); + + if (copyContext) { + MutexAutoLock lock(mMutex); + mCopyContextList.AppendElement(copyContext); + } + + *aCopyCountOut += 1; + + return NS_OK; + } + + void CancelAllStreamCopying() { + // May occur on either owning thread or target thread + MutexAutoLock lock(mMutex); + for (uint32_t i = 0; i < mCopyContextList.Length(); ++i) { + MOZ_DIAGNOSTIC_ASSERT(mCopyContextList[i]); + BodyCancelWrite(*mCopyContextList[i]); + } + mCopyContextList.Clear(); + } + + static void AsyncCopyCompleteFunc(void* aClosure, nsresult aRv) { + // May be on any thread, including STS event target. + MOZ_DIAGNOSTIC_ASSERT(aClosure); + // Weak ref as we are guaranteed to the action is alive until + // CompleteOnInitiatingThread is called. + CachePutAllAction* action = static_cast<CachePutAllAction*>(aClosure); + action->CallOnAsyncCopyCompleteOnTargetThread(aRv); + } + + void CallOnAsyncCopyCompleteOnTargetThread(nsresult aRv) { + // May be on any thread, including STS event target. Non-owning runnable + // here since we are guaranteed the Action will survive until + // CompleteOnInitiatingThread is called. + nsCOMPtr<nsIRunnable> runnable = NewNonOwningRunnableMethod<nsresult>( + "dom::cache::Manager::CachePutAllAction::OnAsyncCopyComplete", this, + &CachePutAllAction::OnAsyncCopyComplete, aRv); + MOZ_ALWAYS_SUCCEEDS( + mTarget->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL)); + } + + void DoResolve(nsresult aRv) { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + // DoResolve() must not be called until all async copying has completed. +#ifdef DEBUG + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mCopyContextList.IsEmpty()); + } +#endif + + // Clean up any files we might have written before hitting the error. + if (NS_FAILED(aRv)) { + BodyDeleteFiles(*mDirectoryMetadata, *mDBDir, mBodyIdWrittenList); + if (mUpdatedPaddingSize > 0) { + DecreaseUsageForDirectoryMetadata(*mDirectoryMetadata, + mUpdatedPaddingSize); + } + } + + // Must be released on the target thread where it was opened. + mConn = nullptr; + + // Drop our ref to the target thread as we are done with this thread. + // Also makes our thread assertions catch any incorrect method calls + // after resolve. + mTarget = nullptr; + + // Make sure to de-ref the resolver per the Action API contract. + SafeRefPtr<Action::Resolver> resolver = std::move(mResolver); + resolver->Resolve(aRv); + } + + // initiating thread only + SafeRefPtr<Manager> mManager; + const ListenerId mListenerId; + + // Set on initiating thread, read on target thread. State machine guarantees + // these are not modified while being read by the target thread. + const CacheId mCacheId; + nsTArray<Entry> mList; + uint32_t mExpectedAsyncCopyCompletions; + + // target thread only + SafeRefPtr<Resolver> mResolver; + nsCOMPtr<nsIFile> mDBDir; + nsCOMPtr<mozIStorageConnection> mConn; + nsCOMPtr<nsISerialEventTarget> mTarget; + nsresult mAsyncResult; + nsTArray<nsID> mBodyIdWrittenList; + + // Written to on target thread, accessed on initiating thread after target + // thread activity is guaranteed complete + nsTArray<nsID> mDeletedBodyIdList; + + // accessed from any thread while mMutex locked + Mutex mMutex MOZ_UNANNOTATED; + nsTArray<nsCOMPtr<nsISupports>> mCopyContextList; + + Maybe<CacheDirectoryMetadata> mDirectoryMetadata; + // Track how much pad amount has been added for new entries so that it can be + // removed if an error occurs. + int64_t mUpdatedPaddingSize; + // Track any pad amount associated with overwritten entries. + int64_t mDeletedPaddingSize; +}; + +// ---------------------------------------------------------------------------- + +class Manager::CacheDeleteAction final : public Manager::BaseAction { + public: + CacheDeleteAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + CacheId aCacheId, const CacheDeleteArgs& aArgs) + : BaseAction(std::move(aManager), aListenerId), + mCacheId(aCacheId), + mArgs(aArgs), + mSuccess(false) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + mDirectoryMetadata.emplace(aDirectoryMetadata); + + mozStorageTransaction trans(aConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start())); + + QM_TRY_UNWRAP( + auto maybeDeletionInfo, + db::CacheDelete(*aConn, mCacheId, mArgs.request(), mArgs.params())); + + mSuccess = maybeDeletionInfo.isSome(); + if (mSuccess) { + mDeletionInfo = std::move(maybeDeletionInfo.ref()); + } + + QM_TRY(MOZ_TO_RESULT(MaybeUpdatePaddingFile( + aDBDir, aConn, /* aIncreaceSize */ 0, + mDeletionInfo.mDeletedPaddingSize, + [&trans]() mutable { return trans.Commit(); })), + QM_PROPAGATE, [this](const nsresult) { mSuccess = false; }); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + // If the transaction fails, we shouldn't delete the body files and decrease + // their padding size. + if (aRv.Failed()) { + mDeletionInfo.mDeletedBodyIdList.Clear(); + mDeletionInfo.mDeletedPaddingSize = 0; + } + + mManager->NoteOrphanedBodyIdList(mDeletionInfo.mDeletedBodyIdList); + + if (mDeletionInfo.mDeletedPaddingSize > 0) { + DecreaseUsageForDirectoryMetadata(*mDirectoryMetadata, + mDeletionInfo.mDeletedPaddingSize); + } + + aListener->OnOpComplete(std::move(aRv), CacheDeleteResult(mSuccess)); + } + + virtual bool MatchesCacheId(CacheId aCacheId) const override { + return aCacheId == mCacheId; + } + + private: + const CacheId mCacheId; + const CacheDeleteArgs mArgs; + bool mSuccess; + DeletionInfo mDeletionInfo; + Maybe<CacheDirectoryMetadata> mDirectoryMetadata; +}; + +// ---------------------------------------------------------------------------- + +class Manager::CacheKeysAction final : public Manager::BaseAction { + public: + CacheKeysAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + CacheId aCacheId, const CacheKeysArgs& aArgs, + SafeRefPtr<StreamList> aStreamList) + : BaseAction(std::move(aManager), aListenerId), + mCacheId(aCacheId), + mArgs(aArgs), + mStreamList(std::move(aStreamList)) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + QM_TRY_UNWRAP( + mSavedRequests, + db::CacheKeys(*aConn, mCacheId, mArgs.maybeRequest(), mArgs.params())); + + for (uint32_t i = 0; i < mSavedRequests.Length(); ++i) { + if (!mSavedRequests[i].mHasBodyId || + IsHeadRequest(mArgs.maybeRequest(), mArgs.params())) { + mSavedRequests[i].mHasBodyId = false; + continue; + } + + nsCOMPtr<nsIInputStream> stream; + if (mArgs.openMode() == OpenMode::Eager) { + QM_TRY_UNWRAP(stream, BodyOpen(aDirectoryMetadata, *aDBDir, + mSavedRequests[i].mBodyId)); + } + + mStreamList->Add(mSavedRequests[i].mBodyId, std::move(stream)); + } + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + mStreamList->Activate(mCacheId); + aListener->OnOpComplete(std::move(aRv), CacheKeysResult(), mSavedRequests, + *mStreamList); + mStreamList = nullptr; + } + + virtual bool MatchesCacheId(CacheId aCacheId) const override { + return aCacheId == mCacheId; + } + + private: + const CacheId mCacheId; + const CacheKeysArgs mArgs; + SafeRefPtr<StreamList> mStreamList; + nsTArray<SavedRequest> mSavedRequests; +}; + +// ---------------------------------------------------------------------------- + +class Manager::StorageMatchAction final : public Manager::BaseAction { + public: + StorageMatchAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + Namespace aNamespace, const StorageMatchArgs& aArgs, + SafeRefPtr<StreamList> aStreamList) + : BaseAction(std::move(aManager), aListenerId), + mNamespace(aNamespace), + mArgs(aArgs), + mStreamList(std::move(aStreamList)), + mFoundResponse(false) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + auto maybeResponse = + db::StorageMatch(*aConn, mNamespace, mArgs.request(), mArgs.params()); + if (NS_WARN_IF(maybeResponse.isErr())) { + return maybeResponse.unwrapErr(); + } + + mFoundResponse = maybeResponse.inspect().isSome(); + if (mFoundResponse) { + mSavedResponse = maybeResponse.unwrap().ref(); + } + + if (!mFoundResponse || !mSavedResponse.mHasBodyId || + IsHeadRequest(mArgs.request(), mArgs.params())) { + mSavedResponse.mHasBodyId = false; + return NS_OK; + } + + nsCOMPtr<nsIInputStream> stream; + if (mArgs.openMode() == OpenMode::Eager) { + QM_TRY_UNWRAP(stream, BodyOpen(aDirectoryMetadata, *aDBDir, + mSavedResponse.mBodyId)); + } + + mStreamList->Add(mSavedResponse.mBodyId, std::move(stream)); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + if (!mFoundResponse) { + aListener->OnOpComplete(std::move(aRv), StorageMatchResult(Nothing())); + } else { + mStreamList->Activate(mSavedResponse.mCacheId); + aListener->OnOpComplete(std::move(aRv), StorageMatchResult(Nothing()), + mSavedResponse, *mStreamList); + } + mStreamList = nullptr; + } + + private: + const Namespace mNamespace; + const StorageMatchArgs mArgs; + SafeRefPtr<StreamList> mStreamList; + bool mFoundResponse; + SavedResponse mSavedResponse; +}; + +// ---------------------------------------------------------------------------- + +class Manager::StorageHasAction final : public Manager::BaseAction { + public: + StorageHasAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + Namespace aNamespace, const StorageHasArgs& aArgs) + : BaseAction(std::move(aManager), aListenerId), + mNamespace(aNamespace), + mArgs(aArgs), + mCacheFound(false) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + QM_TRY_INSPECT(const auto& maybeCacheId, + db::StorageGetCacheId(*aConn, mNamespace, mArgs.key())); + + mCacheFound = maybeCacheId.isSome(); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + aListener->OnOpComplete(std::move(aRv), StorageHasResult(mCacheFound)); + } + + private: + const Namespace mNamespace; + const StorageHasArgs mArgs; + bool mCacheFound; +}; + +// ---------------------------------------------------------------------------- + +class Manager::StorageOpenAction final : public Manager::BaseAction { + public: + StorageOpenAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + Namespace aNamespace, const StorageOpenArgs& aArgs) + : BaseAction(std::move(aManager), aListenerId), + mNamespace(aNamespace), + mArgs(aArgs), + mCacheId(INVALID_CACHE_ID) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + // Cache does not exist, create it instead + mozStorageTransaction trans(aConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start())); + + // Look for existing cache + QM_TRY_INSPECT(const auto& maybeCacheId, + db::StorageGetCacheId(*aConn, mNamespace, mArgs.key())); + + if (maybeCacheId.isSome()) { + mCacheId = maybeCacheId.ref(); + MOZ_DIAGNOSTIC_ASSERT(mCacheId != INVALID_CACHE_ID); + return NS_OK; + } + + QM_TRY_UNWRAP(mCacheId, db::CreateCacheId(*aConn)); + + QM_TRY(MOZ_TO_RESULT( + db::StoragePutCache(*aConn, mNamespace, mArgs.key(), mCacheId))); + + QM_TRY(MOZ_TO_RESULT(trans.Commit())); + + MOZ_DIAGNOSTIC_ASSERT(mCacheId != INVALID_CACHE_ID); + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + MOZ_DIAGNOSTIC_ASSERT(aRv.Failed() || mCacheId != INVALID_CACHE_ID); + aListener->OnOpComplete(std::move(aRv), + StorageOpenResult(nullptr, nullptr, mNamespace), + mCacheId); + } + + private: + const Namespace mNamespace; + const StorageOpenArgs mArgs; + CacheId mCacheId; +}; + +// ---------------------------------------------------------------------------- + +class Manager::StorageDeleteAction final : public Manager::BaseAction { + public: + StorageDeleteAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + Namespace aNamespace, const StorageDeleteArgs& aArgs) + : BaseAction(std::move(aManager), aListenerId), + mNamespace(aNamespace), + mArgs(aArgs), + mCacheDeleted(false), + mCacheId(INVALID_CACHE_ID) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + mozStorageTransaction trans(aConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(trans.Start())); + + QM_TRY_INSPECT(const auto& maybeCacheId, + db::StorageGetCacheId(*aConn, mNamespace, mArgs.key())); + + if (maybeCacheId.isNothing()) { + mCacheDeleted = false; + return NS_OK; + } + mCacheId = maybeCacheId.ref(); + + // Don't delete the removing padding size here, we'll delete it on + // DeleteOrphanedCacheAction. + QM_TRY( + MOZ_TO_RESULT(db::StorageForgetCache(*aConn, mNamespace, mArgs.key()))); + + QM_TRY(MOZ_TO_RESULT(trans.Commit())); + + mCacheDeleted = true; + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + if (mCacheDeleted) { + // If content is referencing this cache, mark it orphaned to be + // deleted later. + if (!mManager->SetCacheIdOrphanedIfRefed(mCacheId)) { + // no outstanding references, delete immediately + const auto pinnedContext = + SafeRefPtr{mManager->mContext, AcquireStrongRefFromRawPtr{}}; + + if (pinnedContext->IsCanceled()) { + pinnedContext->NoteOrphanedData(); + } else { + pinnedContext->CancelForCacheId(mCacheId); + pinnedContext->Dispatch(MakeSafeRefPtr<DeleteOrphanedCacheAction>( + mManager.clonePtr(), mCacheId)); + } + } + } + + aListener->OnOpComplete(std::move(aRv), StorageDeleteResult(mCacheDeleted)); + } + + private: + const Namespace mNamespace; + const StorageDeleteArgs mArgs; + bool mCacheDeleted; + CacheId mCacheId; +}; + +// ---------------------------------------------------------------------------- + +class Manager::StorageKeysAction final : public Manager::BaseAction { + public: + StorageKeysAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + Namespace aNamespace) + : BaseAction(std::move(aManager), aListenerId), mNamespace(aNamespace) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + QM_TRY_UNWRAP(mKeys, db::StorageGetKeys(*aConn, mNamespace)); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + if (aRv.Failed()) { + mKeys.Clear(); + } + aListener->OnOpComplete(std::move(aRv), StorageKeysResult(mKeys)); + } + + private: + const Namespace mNamespace; + nsTArray<nsString> mKeys; +}; + +// ---------------------------------------------------------------------------- + +class Manager::OpenStreamAction final : public Manager::BaseAction { + public: + OpenStreamAction(SafeRefPtr<Manager> aManager, ListenerId aListenerId, + InputStreamResolver&& aResolver, const nsID& aBodyId) + : BaseAction(std::move(aManager), aListenerId), + mResolver(std::move(aResolver)), + mBodyId(aBodyId) {} + + virtual nsresult RunSyncWithDBOnTarget( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aDBDir, + mozIStorageConnection* aConn) override { + MOZ_DIAGNOSTIC_ASSERT(aDBDir); + + QM_TRY_UNWRAP(mBodyStream, BodyOpen(aDirectoryMetadata, *aDBDir, mBodyId)); + + return NS_OK; + } + + virtual void Complete(Listener* aListener, ErrorResult&& aRv) override { + if (aRv.Failed()) { + // Ignore the reason for fail and just pass a null input stream to let it + // fail. + aRv.SuppressException(); + mResolver(nullptr); + } else { + mResolver(std::move(mBodyStream)); + } + + mResolver = nullptr; + } + + private: + InputStreamResolver mResolver; + const nsID mBodyId; + nsCOMPtr<nsIInputStream> mBodyStream; +}; + +// ---------------------------------------------------------------------------- + +// static +Manager::ListenerId Manager::sNextListenerId = 0; + +void Manager::Listener::OnOpComplete(ErrorResult&& aRv, + const CacheOpResult& aResult) { + OnOpComplete(std::move(aRv), aResult, INVALID_CACHE_ID, Nothing()); +} + +void Manager::Listener::OnOpComplete(ErrorResult&& aRv, + const CacheOpResult& aResult, + CacheId aOpenedCacheId) { + OnOpComplete(std::move(aRv), aResult, aOpenedCacheId, Nothing()); +} + +void Manager::Listener::OnOpComplete(ErrorResult&& aRv, + const CacheOpResult& aResult, + const SavedResponse& aSavedResponse, + StreamList& aStreamList) { + AutoTArray<SavedResponse, 1> responseList; + responseList.AppendElement(aSavedResponse); + OnOpComplete( + std::move(aRv), aResult, INVALID_CACHE_ID, + Some(StreamInfo{responseList, nsTArray<SavedRequest>(), aStreamList})); +} + +void Manager::Listener::OnOpComplete( + ErrorResult&& aRv, const CacheOpResult& aResult, + const nsTArray<SavedResponse>& aSavedResponseList, + StreamList& aStreamList) { + OnOpComplete(std::move(aRv), aResult, INVALID_CACHE_ID, + Some(StreamInfo{aSavedResponseList, nsTArray<SavedRequest>(), + aStreamList})); +} + +void Manager::Listener::OnOpComplete( + ErrorResult&& aRv, const CacheOpResult& aResult, + const nsTArray<SavedRequest>& aSavedRequestList, StreamList& aStreamList) { + OnOpComplete(std::move(aRv), aResult, INVALID_CACHE_ID, + Some(StreamInfo{nsTArray<SavedResponse>(), aSavedRequestList, + aStreamList})); +} + +// static +Result<SafeRefPtr<Manager>, nsresult> Manager::AcquireCreateIfNonExistent( + const SafeRefPtr<ManagerId>& aManagerId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + return Factory::AcquireCreateIfNonExistent(aManagerId); +} + +// static +void Manager::InitiateShutdown() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + Factory::AbortAll(); + + Factory::ShutdownAll(); +} + +// static +bool Manager::IsShutdownAllComplete() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + return Factory::IsShutdownAllComplete(); +} + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +void Manager::RecordMayNotDeleteCSCP(int32_t aCacheStreamControlParentId) { + Factory::RecordMayNotDeleteCSCP(aCacheStreamControlParentId); +} + +void Manager::RecordHaveDeletedCSCP(int32_t aCacheStreamControlParentId) { + Factory::RecordHaveDeletedCSCP(aCacheStreamControlParentId); +} +#endif + +// static +nsCString Manager::GetShutdownStatus() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + return Factory::GetShutdownStatus(); +} + +// static +void Manager::Abort(const Client::DirectoryLockIdTable& aDirectoryLockIds) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + Factory::Abort(aDirectoryLockIds); +} + +// static +void Manager::AbortAll() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + Factory::AbortAll(); +} + +void Manager::RemoveListener(Listener* aListener) { + NS_ASSERT_OWNINGTHREAD(Manager); + // There may not be a listener here in the case where an actor is killed + // before it can perform any actual async requests on Manager. + mListeners.RemoveElement(aListener, ListenerEntryListenerComparator()); + MOZ_ASSERT( + !mListeners.Contains(aListener, ListenerEntryListenerComparator())); + MaybeAllowContextToClose(); +} + +void Manager::RemoveContext(Context& aContext) { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(mContext); + MOZ_DIAGNOSTIC_ASSERT(mContext == &aContext); + + // Whether the Context destruction was triggered from the Manager going + // idle or the underlying storage being invalidated, we should know we + // are closing before the Context is destroyed. + MOZ_DIAGNOSTIC_ASSERT(mState == Closing); + + // Before forgetting the Context, check to see if we have any outstanding + // cache or body objects waiting for deletion. If so, note that we've + // orphaned data so it will be cleaned up on the next open. + if (std::any_of( + mCacheIdRefs.cbegin(), mCacheIdRefs.cend(), + [](const auto& cacheIdRef) { return cacheIdRef.mOrphaned; }) || + std::any_of(mBodyIdRefs.cbegin(), mBodyIdRefs.cend(), + [](const auto& bodyIdRef) { return bodyIdRef.mOrphaned; })) { + aContext.NoteOrphanedData(); + } + + mContext = nullptr; + + // Once the context is gone, we can immediately remove ourself from the + // Factory list. We don't need to block shutdown by staying in the list + // any more. + Factory::Remove(*this); +} + +void Manager::NoteClosing() { + NS_ASSERT_OWNINGTHREAD(Manager); + // This can be called more than once legitimately through different paths. + mState = Closing; +} + +Manager::State Manager::GetState() const { + NS_ASSERT_OWNINGTHREAD(Manager); + return mState; +} + +void Manager::AddRefCacheId(CacheId aCacheId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mCacheIdRefs.end(); + const auto foundIt = + std::find_if(mCacheIdRefs.begin(), end, MatchByCacheId(aCacheId)); + if (foundIt != end) { + foundIt->mCount += 1; + return; + } + + mCacheIdRefs.AppendElement(CacheIdRefCounter{aCacheId, 1, false}); +} + +void Manager::ReleaseCacheId(CacheId aCacheId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mCacheIdRefs.end(); + const auto foundIt = + std::find_if(mCacheIdRefs.begin(), end, MatchByCacheId(aCacheId)); + if (foundIt != end) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + const uint32_t oldRef = foundIt->mCount; +#endif + foundIt->mCount -= 1; + MOZ_DIAGNOSTIC_ASSERT(foundIt->mCount < oldRef); + if (foundIt->mCount == 0) { + const bool orphaned = foundIt->mOrphaned; + mCacheIdRefs.RemoveElementAt(foundIt); + const auto pinnedContext = + SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + // If the context is already gone, then orphan flag should have been + // set in RemoveContext(). + if (orphaned && pinnedContext) { + if (pinnedContext->IsCanceled()) { + pinnedContext->NoteOrphanedData(); + } else { + pinnedContext->CancelForCacheId(aCacheId); + pinnedContext->Dispatch(MakeSafeRefPtr<DeleteOrphanedCacheAction>( + SafeRefPtrFromThis(), aCacheId)); + } + } + } + MaybeAllowContextToClose(); + return; + } + + MOZ_ASSERT_UNREACHABLE("Attempt to release CacheId that is not referenced!"); +} + +void Manager::AddRefBodyId(const nsID& aBodyId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mBodyIdRefs.end(); + const auto foundIt = + std::find_if(mBodyIdRefs.begin(), end, MatchByBodyId(aBodyId)); + if (foundIt != end) { + foundIt->mCount += 1; + return; + } + + mBodyIdRefs.AppendElement(BodyIdRefCounter{aBodyId, 1, false}); +} + +void Manager::ReleaseBodyId(const nsID& aBodyId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mBodyIdRefs.end(); + const auto foundIt = + std::find_if(mBodyIdRefs.begin(), end, MatchByBodyId(aBodyId)); + if (foundIt != end) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + const uint32_t oldRef = foundIt->mCount; +#endif + foundIt->mCount -= 1; + MOZ_DIAGNOSTIC_ASSERT(foundIt->mCount < oldRef); + if (foundIt->mCount < 1) { + const bool orphaned = foundIt->mOrphaned; + mBodyIdRefs.RemoveElementAt(foundIt); + const auto pinnedContext = + SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + // If the context is already gone, then orphan flag should have been + // set in RemoveContext(). + if (orphaned && pinnedContext) { + if (pinnedContext->IsCanceled()) { + pinnedContext->NoteOrphanedData(); + } else { + pinnedContext->Dispatch( + MakeSafeRefPtr<DeleteOrphanedBodyAction>(aBodyId)); + } + } + } + MaybeAllowContextToClose(); + return; + } + + MOZ_ASSERT_UNREACHABLE("Attempt to release BodyId that is not referenced!"); +} + +const ManagerId& Manager::GetManagerId() const { return *mManagerId; } + +void Manager::AddStreamList(StreamList& aStreamList) { + NS_ASSERT_OWNINGTHREAD(Manager); + mStreamLists.AppendElement(WrapNotNullUnchecked(&aStreamList)); +} + +void Manager::RemoveStreamList(StreamList& aStreamList) { + NS_ASSERT_OWNINGTHREAD(Manager); + mStreamLists.RemoveElement(&aStreamList); +} + +void Manager::ExecuteCacheOp(Listener* aListener, CacheId aCacheId, + const CacheOpArgs& aOpArgs) { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(aListener); + MOZ_DIAGNOSTIC_ASSERT(aOpArgs.type() != CacheOpArgs::TCachePutAllArgs); + + if (NS_WARN_IF(mState == Closing)) { + aListener->OnOpComplete(ErrorResult(NS_ERROR_FAILURE), void_t()); + return; + } + + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + MOZ_DIAGNOSTIC_ASSERT(!pinnedContext->IsCanceled()); + + auto action = [this, aListener, aCacheId, &aOpArgs, + &pinnedContext]() -> SafeRefPtr<Action> { + const ListenerId listenerId = SaveListener(aListener); + + if (CacheOpArgs::TCacheDeleteArgs == aOpArgs.type()) { + return MakeSafeRefPtr<CacheDeleteAction>(SafeRefPtrFromThis(), listenerId, + aCacheId, + aOpArgs.get_CacheDeleteArgs()); + } + + auto streamList = MakeSafeRefPtr<StreamList>(SafeRefPtrFromThis(), + pinnedContext.clonePtr()); + + switch (aOpArgs.type()) { + case CacheOpArgs::TCacheMatchArgs: + return MakeSafeRefPtr<CacheMatchAction>( + SafeRefPtrFromThis(), listenerId, aCacheId, + aOpArgs.get_CacheMatchArgs(), std::move(streamList)); + case CacheOpArgs::TCacheMatchAllArgs: + return MakeSafeRefPtr<CacheMatchAllAction>( + SafeRefPtrFromThis(), listenerId, aCacheId, + aOpArgs.get_CacheMatchAllArgs(), std::move(streamList)); + case CacheOpArgs::TCacheKeysArgs: + return MakeSafeRefPtr<CacheKeysAction>( + SafeRefPtrFromThis(), listenerId, aCacheId, + aOpArgs.get_CacheKeysArgs(), std::move(streamList)); + default: + MOZ_CRASH("Unknown Cache operation!"); + } + }(); + + pinnedContext->Dispatch(std::move(action)); +} + +void Manager::ExecuteStorageOp(Listener* aListener, Namespace aNamespace, + const CacheOpArgs& aOpArgs) { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(aListener); + + if (NS_WARN_IF(mState == Closing)) { + aListener->OnOpComplete(ErrorResult(NS_ERROR_FAILURE), void_t()); + return; + } + + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + MOZ_DIAGNOSTIC_ASSERT(!pinnedContext->IsCanceled()); + + auto action = [this, aListener, aNamespace, &aOpArgs, + &pinnedContext]() -> SafeRefPtr<Action> { + const ListenerId listenerId = SaveListener(aListener); + + switch (aOpArgs.type()) { + case CacheOpArgs::TStorageMatchArgs: + return MakeSafeRefPtr<StorageMatchAction>( + SafeRefPtrFromThis(), listenerId, aNamespace, + aOpArgs.get_StorageMatchArgs(), + MakeSafeRefPtr<StreamList>(SafeRefPtrFromThis(), + pinnedContext.clonePtr())); + case CacheOpArgs::TStorageHasArgs: + return MakeSafeRefPtr<StorageHasAction>(SafeRefPtrFromThis(), + listenerId, aNamespace, + aOpArgs.get_StorageHasArgs()); + case CacheOpArgs::TStorageOpenArgs: + return MakeSafeRefPtr<StorageOpenAction>(SafeRefPtrFromThis(), + listenerId, aNamespace, + aOpArgs.get_StorageOpenArgs()); + case CacheOpArgs::TStorageDeleteArgs: + return MakeSafeRefPtr<StorageDeleteAction>( + SafeRefPtrFromThis(), listenerId, aNamespace, + aOpArgs.get_StorageDeleteArgs()); + case CacheOpArgs::TStorageKeysArgs: + return MakeSafeRefPtr<StorageKeysAction>(SafeRefPtrFromThis(), + listenerId, aNamespace); + default: + MOZ_CRASH("Unknown CacheStorage operation!"); + } + }(); + + pinnedContext->Dispatch(std::move(action)); +} + +void Manager::ExecuteOpenStream(Listener* aListener, + InputStreamResolver&& aResolver, + const nsID& aBodyId) { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(aListener); + MOZ_DIAGNOSTIC_ASSERT(aResolver); + + if (NS_WARN_IF(mState == Closing)) { + aResolver(nullptr); + return; + } + + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + MOZ_DIAGNOSTIC_ASSERT(!pinnedContext->IsCanceled()); + + // We save the listener simply to track the existence of the caller here. + // Our returned value will really be passed to the resolver when the + // operation completes. In the future we should remove the Listener + // mechanism in favor of std::function or MozPromise. + ListenerId listenerId = SaveListener(aListener); + + pinnedContext->Dispatch(MakeSafeRefPtr<OpenStreamAction>( + SafeRefPtrFromThis(), listenerId, std::move(aResolver), aBodyId)); +} + +void Manager::ExecutePutAll( + Listener* aListener, CacheId aCacheId, + const nsTArray<CacheRequestResponse>& aPutList, + const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreamList, + const nsTArray<nsCOMPtr<nsIInputStream>>& aResponseStreamList) { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(aListener); + + if (NS_WARN_IF(mState == Closing)) { + aListener->OnOpComplete(ErrorResult(NS_ERROR_FAILURE), CachePutAllResult()); + return; + } + + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + MOZ_DIAGNOSTIC_ASSERT(!pinnedContext->IsCanceled()); + + ListenerId listenerId = SaveListener(aListener); + pinnedContext->Dispatch(MakeSafeRefPtr<CachePutAllAction>( + SafeRefPtrFromThis(), listenerId, aCacheId, aPutList, aRequestStreamList, + aResponseStreamList)); +} + +Manager::Manager(SafeRefPtr<ManagerId> aManagerId, nsIThread* aIOThread, + const ConstructorGuard&) + : mManagerId(std::move(aManagerId)), + mIOThread(aIOThread), + mContext(nullptr), + mShuttingDown(false), + mState(Open) { + MOZ_DIAGNOSTIC_ASSERT(mManagerId); + MOZ_DIAGNOSTIC_ASSERT(mIOThread); +} + +Manager::~Manager() { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(mState == Closing); + MOZ_DIAGNOSTIC_ASSERT(!mContext); + + nsCOMPtr<nsIThread> ioThread; + mIOThread.swap(ioThread); + + // Don't spin the event loop in the destructor waiting for the thread to + // shutdown. Defer this to the main thread, instead. + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(NewRunnableMethod( + "nsIThread::AsyncShutdown", ioThread, &nsIThread::AsyncShutdown))); +} + +void Manager::Init(Maybe<Manager&> aOldManager) { + NS_ASSERT_OWNINGTHREAD(Manager); + + // Create the context immediately. Since there can at most be one Context + // per Manager now, this lets us cleanly call Factory::Remove() once the + // Context goes away. + SafeRefPtr<Context> ref = Context::Create( + SafeRefPtrFromThis(), mIOThread->SerialEventTarget(), + MakeSafeRefPtr<SetupAction>(), + aOldManager ? SomeRef(*aOldManager->mContext) : Nothing()); + mContext = ref.unsafeGetRawPtr(); +} + +void Manager::Shutdown() { + NS_ASSERT_OWNINGTHREAD(Manager); + + // Ignore duplicate attempts to shutdown. This can occur when we start + // a browser initiated shutdown and then run ~Manager() which also + // calls Shutdown(). + if (mShuttingDown) { + return; + } + + mShuttingDown = true; + + // Note that we are closing to prevent any new requests from coming in and + // creating a new Context. We must ensure all Contexts and IO operations are + // complete before shutdown proceeds. + NoteClosing(); + + // If there is a context, then cancel and only note that we are done after + // its cleaned up. + if (mContext) { + const auto pinnedContext = + SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + pinnedContext->CancelAll(); + return; + } +} + +Maybe<DirectoryLock&> Manager::MaybeDirectoryLockRef() const { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(mContext); + + return mContext->MaybeDirectoryLockRef(); +} + +void Manager::Abort() { + NS_ASSERT_OWNINGTHREAD(Manager); + MOZ_DIAGNOSTIC_ASSERT(mContext); + + // Note that we are closing to prevent any new requests from coming in and + // creating a new Context. We must ensure all Contexts and IO operations are + // complete before origin clear proceeds. + NoteClosing(); + + // Cancel and only note that we are done after the context is cleaned up. + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + pinnedContext->CancelAll(); +} + +Manager::ListenerId Manager::SaveListener(Listener* aListener) { + NS_ASSERT_OWNINGTHREAD(Manager); + + // Once a Listener is added, we keep a reference to it until its + // removed. Since the same Listener might make multiple requests, + // ensure we only have a single reference in our list. + ListenerList::index_type index = + mListeners.IndexOf(aListener, 0, ListenerEntryListenerComparator()); + if (index != ListenerList::NoIndex) { + return mListeners[index].mId; + } + + ListenerId id = sNextListenerId; + sNextListenerId += 1; + + mListeners.AppendElement(ListenerEntry(id, aListener)); + return id; +} + +Manager::Listener* Manager::GetListener(ListenerId aListenerId) const { + NS_ASSERT_OWNINGTHREAD(Manager); + ListenerList::index_type index = + mListeners.IndexOf(aListenerId, 0, ListenerEntryIdComparator()); + if (index != ListenerList::NoIndex) { + return mListeners[index].mListener; + } + + // This can legitimately happen if the actor is deleted while a request is + // in process. For example, the child process OOMs. + return nullptr; +} + +bool Manager::SetCacheIdOrphanedIfRefed(CacheId aCacheId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mCacheIdRefs.end(); + const auto foundIt = + std::find_if(mCacheIdRefs.begin(), end, MatchByCacheId(aCacheId)); + if (foundIt != end) { + MOZ_DIAGNOSTIC_ASSERT(foundIt->mCount > 0); + MOZ_DIAGNOSTIC_ASSERT(!foundIt->mOrphaned); + foundIt->mOrphaned = true; + return true; + } + + return false; +} + +// TODO: provide way to set body non-orphaned if its added back to a cache (bug +// 1110479) + +bool Manager::SetBodyIdOrphanedIfRefed(const nsID& aBodyId) { + NS_ASSERT_OWNINGTHREAD(Manager); + + const auto end = mBodyIdRefs.end(); + const auto foundIt = + std::find_if(mBodyIdRefs.begin(), end, MatchByBodyId(aBodyId)); + if (foundIt != end) { + MOZ_DIAGNOSTIC_ASSERT(foundIt->mCount > 0); + MOZ_DIAGNOSTIC_ASSERT(!foundIt->mOrphaned); + foundIt->mOrphaned = true; + return true; + } + + return false; +} + +void Manager::NoteOrphanedBodyIdList(const nsTArray<nsID>& aDeletedBodyIdList) { + NS_ASSERT_OWNINGTHREAD(Manager); + + // XXX TransformIfIntoNewArray might be generalized to allow specifying the + // type of nsTArray to create, so that it can create an AutoTArray as well; an + // TransformIf (without AbortOnErr) might be added, which could be used here. + DeleteOrphanedBodyAction::DeletedBodyIdList deleteNowList; + deleteNowList.SetCapacity(aDeletedBodyIdList.Length()); + + std::copy_if(aDeletedBodyIdList.cbegin(), aDeletedBodyIdList.cend(), + MakeBackInserter(deleteNowList), + [this](const auto& deletedBodyId) { + return !SetBodyIdOrphanedIfRefed(deletedBodyId); + }); + + // TODO: note that we need to check these bodies for staleness on startup (bug + // 1110446) + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + if (!deleteNowList.IsEmpty() && pinnedContext && + !pinnedContext->IsCanceled()) { + pinnedContext->Dispatch( + MakeSafeRefPtr<DeleteOrphanedBodyAction>(std::move(deleteNowList))); + } +} + +void Manager::MaybeAllowContextToClose() { + NS_ASSERT_OWNINGTHREAD(Manager); + + // If we have an active context, but we have no more users of the Manager, + // then let it shut itself down. We must wait for all possible users of + // Cache state information to complete before doing this. Once we allow + // the Context to close we may not reliably get notified of storage + // invalidation. + const auto pinnedContext = SafeRefPtr{mContext, AcquireStrongRefFromRawPtr{}}; + if (pinnedContext && mListeners.IsEmpty() && mCacheIdRefs.IsEmpty() && + mBodyIdRefs.IsEmpty()) { + // Mark this Manager as invalid so that it won't get used again. We don't + // want to start any new operations once we allow the Context to close since + // it may race with the underlying storage getting invalidated. + NoteClosing(); + + pinnedContext->AllowToClose(); + } +} + +} // namespace mozilla::dom::cache |