gecko-dev/dom/cache/Manager.cpp
Tom Tung c337197339 Bug 1402581 - P1: Don't delete the body file and decrease the padding size when the transaction fails. r=bkelly
--HG--
extra : rebase_source : b6b9b6688973eec2058eb55218988dfea54db083
extra : amend_source : 1446cdf0fe948705bb23687902b8e0f483304622
2017-10-03 18:36:44 +08:00

2149 lines
65 KiB
C++

/* -*- 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/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/cache/QuotaClient.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"
namespace mozilla {
namespace dom {
namespace cache {
namespace {
// 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 QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
MOZ_DIAGNOSTIC_ASSERT(aDBDir);
nsresult rv = BodyCreateDir(aDBDir);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
// executes in its own transaction
rv = db::CreateOrMigrateSchema(aConn);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
// 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(aQuotaInfo)) {
NS_WARNING("Cache not shutdown cleanly! Cleaning up stale data...");
mozStorageTransaction trans(aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
// Clean up orphaned Cache objects
AutoTArray<CacheId, 8> orphanedCacheIdList;
nsresult rv = db::FindOrphanedCacheIds(aConn, orphanedCacheIdList);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
int64_t overallDeletedPaddingSize = 0;
for (uint32_t i = 0; i < orphanedCacheIdList.Length(); ++i) {
AutoTArray<nsID, 16> deletedBodyIdList;
int64_t deletedPaddingSize = 0;
rv = db::DeleteCacheId(aConn, orphanedCacheIdList[i], deletedBodyIdList,
&deletedPaddingSize);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = BodyDeleteFiles(aQuotaInfo, aDBDir, deletedBodyIdList);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (deletedPaddingSize > 0) {
DecreaseUsageForQuotaInfo(aQuotaInfo, deletedPaddingSize);
}
MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - deletedPaddingSize >=
overallDeletedPaddingSize);
overallDeletedPaddingSize += deletedPaddingSize;
}
// Clean up orphaned body objects
AutoTArray<nsID, 64> knownBodyIdList;
rv = db::GetKnownBodyIds(aConn, knownBodyIdList);
rv = BodyDeleteOrphanedFiles(aQuotaInfo, aDBDir, knownBodyIdList);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
// Commit() explicitly here, because we want to ensure the padding file
// has the correct content.
rv = MaybeUpdatePaddingFile(aDBDir, aConn, /* aIncreaceSize */ 0,
overallDeletedPaddingSize,
[&trans]() mutable { return trans.Commit(); });
// We'll restore padding file below, so just warn here if failure happens.
Unused << NS_WARN_IF(NS_FAILED(rv));
}
if (DirectoryPaddingFileExists(aDBDir, DirPaddingFile::TMP_FILE) ||
!DirectoryPaddingFileExists(aDBDir, DirPaddingFile::FILE)) {
rv = RestorePaddingFile(aDBDir, aConn);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
}
return rv;
}
};
// ----------------------------------------------------------------------------
// 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:
explicit DeleteOrphanedBodyAction(const nsTArray<nsID>& aDeletedBodyIdList)
: mDeletedBodyIdList(aDeletedBodyIdList)
{ }
explicit DeleteOrphanedBodyAction(const nsID& aBodyId)
{
mDeletedBodyIdList.AppendElement(aBodyId);
}
virtual void
RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo, Data*) override
{
MOZ_DIAGNOSTIC_ASSERT(aResolver);
MOZ_DIAGNOSTIC_ASSERT(aQuotaInfo.mDir);
// Note that since DeleteOrphanedBodyAction isn't used while the context is
// being initialized, we don't need to check for cancellation here.
nsCOMPtr<nsIFile> dbDir;
nsresult rv = aQuotaInfo.mDir->Clone(getter_AddRefs(dbDir));
if (NS_WARN_IF(NS_FAILED(rv))) {
aResolver->Resolve(rv);
return;
}
rv = dbDir->Append(NS_LITERAL_STRING("cache"));
if (NS_WARN_IF(NS_FAILED(rv))) {
aResolver->Resolve(rv);
return;
}
rv = BodyDeleteFiles(aQuotaInfo, dbDir, mDeletedBodyIdList);
Unused << NS_WARN_IF(NS_FAILED(rv));
aResolver->Resolve(rv);
}
private:
nsTArray<nsID> mDeletedBodyIdList;
};
bool IsHeadRequest(const CacheRequest& aRequest, const CacheQueryParams& aParams)
{
return !aParams.ignoreMethod() && aRequest.method().LowerCaseEqualsLiteral("head");
}
bool IsHeadRequest(const CacheRequestOrVoid& aRequest, const CacheQueryParams& aParams)
{
if (aRequest.type() == CacheRequestOrVoid::TCacheRequest) {
return !aParams.ignoreMethod() &&
aRequest.get_CacheRequest().method().LowerCaseEqualsLiteral("head");
}
return false;
}
} // 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 nsresult
GetOrCreate(ManagerId* aManagerId, Manager** aManagerOut)
{
mozilla::ipc::AssertIsOnBackgroundThread();
// Ensure there is a factory instance. This forces the Get() call
// below to use the same factory.
nsresult rv = MaybeCreateInstance();
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
RefPtr<Manager> ref = Get(aManagerId);
if (!ref) {
// TODO: replace this with a thread pool (bug 1119864)
nsCOMPtr<nsIThread> ioThread;
rv = NS_NewNamedThread("DOMCacheThread", getter_AddRefs(ioThread));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
ref = new Manager(aManagerId, ioThread);
// 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.
RefPtr<Manager> oldManager = Get(aManagerId, Closing);
ref->Init(oldManager);
MOZ_ASSERT(!sFactory->mManagerList.Contains(ref));
sFactory->mManagerList.AppendElement(ref);
}
ref.forget(aManagerOut);
return NS_OK;
}
static already_AddRefed<Manager>
Get(ManagerId* aManagerId, State aState = Open)
{
mozilla::ipc::AssertIsOnBackgroundThread();
nsresult rv = MaybeCreateInstance();
if (NS_WARN_IF(NS_FAILED(rv))) { return 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.
ManagerList::BackwardIterator iter(sFactory->mManagerList);
while (iter.HasMore()) {
RefPtr<Manager> manager = iter.GetNext();
if (aState == manager->GetState() && *manager->mManagerId == *aManagerId) {
return manager.forget();
}
}
return nullptr;
}
static void
Remove(Manager* aManager)
{
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_DIAGNOSTIC_ASSERT(aManager);
MOZ_DIAGNOSTIC_ASSERT(sFactory);
MOZ_ALWAYS_TRUE(sFactory->mManagerList.RemoveElement(aManager));
// clean up the factory singleton if there are no more managers
MaybeDestroyInstance();
}
static void
Abort(const nsACString& aOrigin)
{
mozilla::ipc::AssertIsOnBackgroundThread();
if (!sFactory) {
return;
}
MOZ_DIAGNOSTIC_ASSERT(!sFactory->mManagerList.IsEmpty());
{
ManagerList::ForwardIterator iter(sFactory->mManagerList);
while (iter.HasMore()) {
RefPtr<Manager> manager = iter.GetNext();
if (aOrigin.IsVoid() ||
manager->mManagerId->QuotaOrigin() == aOrigin) {
manager->Abort();
}
}
}
}
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->mInSyncShutdown);
sFactory->mInSyncShutdown = true;
ManagerList::ForwardIterator iter(sFactory->mManagerList);
while (iter.HasMore()) {
RefPtr<Manager> manager = iter.GetNext();
manager->Shutdown();
}
}
MaybeDestroyInstance();
}
static bool
IsShutdownAllComplete()
{
mozilla::ipc::AssertIsOnBackgroundThread();
return !sFactory;
}
private:
Factory()
: mInSyncShutdown(false)
{
MOZ_COUNT_CTOR(cache::Manager::Factory);
}
~Factory()
{
MOZ_COUNT_DTOR(cache::Manager::Factory);
MOZ_DIAGNOSTIC_ASSERT(mManagerList.IsEmpty());
MOZ_DIAGNOSTIC_ASSERT(!mInSyncShutdown);
}
static nsresult
MaybeCreateInstance()
{
mozilla::ipc::AssertIsOnBackgroundThread();
if (!sFactory) {
// Be clear about what we are locking. sFactory is bg thread only, so
// we don't need to lock it here. Just protect sFactoryShutdown and
// sBackgroundThread.
{
StaticMutexAutoLock lock(sMutex);
if (sFactoryShutdown) {
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
}
}
// 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 shutting down. We need to be careful not to delete ourself
// synchronously during shutdown.
if (!sFactory->mManagerList.IsEmpty() || sFactory->mInSyncShutdown) {
return;
}
sFactory = nullptr;
}
// Singleton created on demand and deleted when last Manager is cleared
// in Remove().
// PBackground thread only.
static StaticAutoPtr<Factory> sFactory;
// protects following static attribute
static StaticMutex sMutex;
// Indicate if shutdown has occurred to block re-creation of sFactory.
// Must hold sMutex to access.
static bool sFactoryShutdown;
// 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.
typedef nsTObserverArray<Manager*> ManagerList;
ManagerList mManagerList;
// This flag is set when we are looping through the list and calling
// Shutdown() on each Manager. We need to be careful not to synchronously
// trigger the deletion of the factory while still executing this loop.
bool mInSyncShutdown;
};
// static
StaticAutoPtr<Manager::Factory> Manager::Factory::sFactory;
// static
StaticMutex Manager::Factory::sMutex;
// static
bool Manager::Factory::sFactoryShutdown = false;
// ----------------------------------------------------------------------------
// 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(Manager* aManager, ListenerId aListenerId)
: SyncDBAction(DBAction::Existing)
, mManager(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;
}
RefPtr<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(Manager* aManager, CacheId aCacheId)
: SyncDBAction(DBAction::Existing)
, mManager(aManager)
, mCacheId(aCacheId)
, mDeletedPaddingSize(0)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
mQuotaInfo.emplace(aQuotaInfo);
mozStorageTransaction trans(aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
nsresult rv = db::DeleteCacheId(aConn, mCacheId, mDeletedBodyIdList,
&mDeletedPaddingSize);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = MaybeUpdatePaddingFile(aDBDir, aConn, /* aIncreaceSize */ 0,
mDeletedPaddingSize,
[&trans]() mutable { return trans.Commit(); });
Unused << NS_WARN_IF(NS_FAILED(rv));
return rv;
}
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)) {
mDeletedBodyIdList.Clear();
mDeletedPaddingSize = 0;
}
mManager->NoteOrphanedBodyIdList(mDeletedBodyIdList);
if (mDeletedPaddingSize > 0) {
DecreaseUsageForQuotaInfo(mQuotaInfo.ref(), mDeletedPaddingSize);
}
// ensure we release the manager on the initiating thread
mManager = nullptr;
}
private:
RefPtr<Manager> mManager;
const CacheId mCacheId;
nsTArray<nsID> mDeletedBodyIdList;
Maybe<QuotaInfo> mQuotaInfo;
// Track any pad amount associated with orphaned entries.
int64_t mDeletedPaddingSize;
};
// ----------------------------------------------------------------------------
class Manager::CacheMatchAction final : public Manager::BaseAction
{
public:
CacheMatchAction(Manager* aManager, ListenerId aListenerId,
CacheId aCacheId, const CacheMatchArgs& aArgs,
StreamList* aStreamList)
: BaseAction(aManager, aListenerId)
, mCacheId(aCacheId)
, mArgs(aArgs)
, mStreamList(aStreamList)
, mFoundResponse(false)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
nsresult rv = db::CacheMatch(aConn, mCacheId, mArgs.request(),
mArgs.params(), &mFoundResponse, &mResponse);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (!mFoundResponse || !mResponse.mHasBodyId
|| IsHeadRequest(mArgs.request(), mArgs.params())) {
mResponse.mHasBodyId = false;
return rv;
}
nsCOMPtr<nsIInputStream> stream;
if (mArgs.openMode() == OpenMode::Eager) {
rv = BodyOpen(aQuotaInfo, aDBDir, mResponse.mBodyId, getter_AddRefs(stream));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (NS_WARN_IF(!stream)) { return NS_ERROR_FILE_NOT_FOUND; }
}
mStreamList->Add(mResponse.mBodyId, Move(stream));
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
if (!mFoundResponse) {
aListener->OnOpComplete(Move(aRv), CacheMatchResult(void_t()));
} else {
mStreamList->Activate(mCacheId);
aListener->OnOpComplete(Move(aRv), CacheMatchResult(void_t()), mResponse,
mStreamList);
}
mStreamList = nullptr;
}
virtual bool MatchesCacheId(CacheId aCacheId) const override
{
return aCacheId == mCacheId;
}
private:
const CacheId mCacheId;
const CacheMatchArgs mArgs;
RefPtr<StreamList> mStreamList;
bool mFoundResponse;
SavedResponse mResponse;
};
// ----------------------------------------------------------------------------
class Manager::CacheMatchAllAction final : public Manager::BaseAction
{
public:
CacheMatchAllAction(Manager* aManager, ListenerId aListenerId,
CacheId aCacheId, const CacheMatchAllArgs& aArgs,
StreamList* aStreamList)
: BaseAction(aManager, aListenerId)
, mCacheId(aCacheId)
, mArgs(aArgs)
, mStreamList(aStreamList)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
nsresult rv = db::CacheMatchAll(aConn, mCacheId, mArgs.requestOrVoid(),
mArgs.params(), mSavedResponses);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
for (uint32_t i = 0; i < mSavedResponses.Length(); ++i) {
if (!mSavedResponses[i].mHasBodyId
|| IsHeadRequest(mArgs.requestOrVoid(), mArgs.params())) {
mSavedResponses[i].mHasBodyId = false;
continue;
}
nsCOMPtr<nsIInputStream> stream;
if (mArgs.openMode() == OpenMode::Eager) {
rv = BodyOpen(aQuotaInfo, aDBDir, mSavedResponses[i].mBodyId,
getter_AddRefs(stream));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (NS_WARN_IF(!stream)) { return NS_ERROR_FILE_NOT_FOUND; }
}
mStreamList->Add(mSavedResponses[i].mBodyId, Move(stream));
}
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
mStreamList->Activate(mCacheId);
aListener->OnOpComplete(Move(aRv), CacheMatchAllResult(), mSavedResponses,
mStreamList);
mStreamList = nullptr;
}
virtual bool MatchesCacheId(CacheId aCacheId) const override
{
return aCacheId == mCacheId;
}
private:
const CacheId mCacheId;
const CacheMatchAllArgs mArgs;
RefPtr<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(Manager* aManager, ListenerId aListenerId,
CacheId aCacheId,
const nsTArray<CacheRequestResponse>& aPutList,
const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreamList,
const nsTArray<nsCOMPtr<nsIInputStream>>& aResponseStreamList)
: DBAction(DBAction::Existing)
, mManager(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() { }
virtual void
RunWithDBOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo,
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 = GetCurrentThreadSerialEventTarget();
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 = aResolver;
mDBDir = aDBDir;
mConn = aConn;
mQuotaInfo.emplace(aQuotaInfo);
// File bodies are streamed to disk via asynchronous copying. Start
// this copying now. Each copy will eventually result in a call
// to OnAsyncCopyComplete().
nsresult rv = NS_OK;
for (uint32_t i = 0; i < mList.Length(); ++i) {
rv = StartStreamCopy(aQuotaInfo, mList[i], RequestStream,
&mExpectedAsyncCopyCompletions);
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
rv = StartStreamCopy(aQuotaInfo, mList[i], ResponseStream,
&mExpectedAsyncCopyCompletions);
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
}
// 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);
nsresult rv = NS_OK;
for (uint32_t i = 0; i < mList.Length(); ++i) {
Entry& e = mList[i];
if (e.mRequestStream) {
rv = BodyFinalizeWrite(mDBDir, e.mRequestBodyId);
if (NS_WARN_IF(NS_FAILED(rv))) {
DoResolve(rv);
return;
}
}
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.
rv = BodyMaybeUpdatePaddingSize(mQuotaInfo.ref(), mDBDir,
e.mResponseBodyId,
e.mResponse.paddingInfo(),
&e.mResponse.paddingSize());
if (NS_WARN_IF(NS_FAILED(rv))) {
DoResolve(rv);
return;
}
MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - e.mResponse.paddingSize() >=
mUpdatedPaddingSize);
mUpdatedPaddingSize += e.mResponse.paddingSize();
}
rv = BodyFinalizeWrite(mDBDir, e.mResponseBodyId);
if (NS_WARN_IF(NS_FAILED(rv))) {
DoResolve(rv);
return;
}
}
int64_t deletedPaddingSize = 0;
rv = db::CachePut(mConn, mCacheId, e.mRequest,
e.mRequestStream ? &e.mRequestBodyId : nullptr,
e.mResponse,
e.mResponseStream ? &e.mResponseBodyId : nullptr,
mDeletedBodyIdList, &deletedPaddingSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
DoResolve(rv);
return;
}
MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - mDeletedPaddingSize >=
deletedPaddingSize);
mDeletedPaddingSize += deletedPaddingSize;
}
// Update padding file when it's necessary
rv = MaybeUpdatePaddingFile(mDBDir, mConn, mUpdatedPaddingSize,
mDeletedPaddingSize,
[&trans]() mutable { return trans.Commit(); });
Unused << NS_WARN_IF(NS_FAILED(rv));
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) {
DecreaseUsageForQuotaInfo(mQuotaInfo.ref(), 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 QuotaInfo& aQuotaInfo, Entry& aEntry,
StreamId aStreamId, uint32_t* aCopyCountOut)
{
MOZ_ASSERT(mTarget->IsOnCurrentThread());
MOZ_DIAGNOSTIC_ASSERT(aCopyCountOut);
if (IsCanceled()) {
return NS_ERROR_ABORT;
}
nsCOMPtr<nsIInputStream> source;
nsID* bodyId;
if (aStreamId == RequestStream) {
source = aEntry.mRequestStream;
bodyId = &aEntry.mRequestBodyId;
} else {
MOZ_DIAGNOSTIC_ASSERT(aStreamId == ResponseStream);
source = aEntry.mResponseStream;
bodyId = &aEntry.mResponseBodyId;
}
if (!source) {
return NS_OK;
}
nsCOMPtr<nsISupports> copyContext;
nsresult rv = BodyStartWriteStream(aQuotaInfo, mDBDir, source, this,
AsyncCopyCompleteFunc, bodyId,
getter_AddRefs(copyContext));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
mBodyIdWrittenList.AppendElement(*bodyId);
if (copyContext) {
MutexAutoLock lock(mMutex);
mCopyContextList.AppendElement(copyContext);
}
*aCopyCountOut += 1;
return rv;
}
void
CancelAllStreamCopying()
{
// May occur on either owning thread or target thread
MutexAutoLock lock(mMutex);
for (uint32_t i = 0; i < mCopyContextList.Length(); ++i) {
BodyCancelWrite(mDBDir, 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(mQuotaInfo.ref(), mDBDir, mBodyIdWrittenList);
if (mUpdatedPaddingSize > 0) {
DecreaseUsageForQuotaInfo(mQuotaInfo.ref(), 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.
RefPtr<Action::Resolver> resolver;
mResolver.swap(resolver);
resolver->Resolve(aRv);
}
// initiating thread only
RefPtr<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
RefPtr<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;
nsTArray<nsCOMPtr<nsISupports>> mCopyContextList;
Maybe<QuotaInfo> mQuotaInfo;
// 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(Manager* aManager, ListenerId aListenerId,
CacheId aCacheId, const CacheDeleteArgs& aArgs)
: BaseAction(aManager, aListenerId)
, mCacheId(aCacheId)
, mArgs(aArgs)
, mSuccess(false)
, mDeletedPaddingSize(0)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
mQuotaInfo.emplace(aQuotaInfo);
mozStorageTransaction trans(aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
nsresult rv = db::CacheDelete(aConn, mCacheId, mArgs.request(),
mArgs.params(), mDeletedBodyIdList,
&mDeletedPaddingSize, &mSuccess);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = MaybeUpdatePaddingFile(aDBDir, aConn, /* aIncreaceSize */ 0,
mDeletedPaddingSize,
[&trans]() mutable { return trans.Commit(); });
if (NS_WARN_IF(NS_FAILED(rv))) {
mSuccess = false;
return rv;
}
return rv;
}
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()) {
mDeletedBodyIdList.Clear();
mDeletedPaddingSize = 0;
}
mManager->NoteOrphanedBodyIdList(mDeletedBodyIdList);
if (mDeletedPaddingSize > 0) {
DecreaseUsageForQuotaInfo(mQuotaInfo.ref(), mDeletedPaddingSize);
}
aListener->OnOpComplete(Move(aRv), CacheDeleteResult(mSuccess));
}
virtual bool MatchesCacheId(CacheId aCacheId) const override
{
return aCacheId == mCacheId;
}
private:
const CacheId mCacheId;
const CacheDeleteArgs mArgs;
bool mSuccess;
nsTArray<nsID> mDeletedBodyIdList;
Maybe<QuotaInfo> mQuotaInfo;
// Track any pad amount associated with deleted entries.
int64_t mDeletedPaddingSize;
};
// ----------------------------------------------------------------------------
class Manager::CacheKeysAction final : public Manager::BaseAction
{
public:
CacheKeysAction(Manager* aManager, ListenerId aListenerId,
CacheId aCacheId, const CacheKeysArgs& aArgs,
StreamList* aStreamList)
: BaseAction(aManager, aListenerId)
, mCacheId(aCacheId)
, mArgs(aArgs)
, mStreamList(aStreamList)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
nsresult rv = db::CacheKeys(aConn, mCacheId, mArgs.requestOrVoid(),
mArgs.params(), mSavedRequests);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
for (uint32_t i = 0; i < mSavedRequests.Length(); ++i) {
if (!mSavedRequests[i].mHasBodyId
|| IsHeadRequest(mArgs.requestOrVoid(), mArgs.params())) {
mSavedRequests[i].mHasBodyId = false;
continue;
}
nsCOMPtr<nsIInputStream> stream;
if (mArgs.openMode() == OpenMode::Eager) {
rv = BodyOpen(aQuotaInfo, aDBDir, mSavedRequests[i].mBodyId,
getter_AddRefs(stream));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (NS_WARN_IF(!stream)) { return NS_ERROR_FILE_NOT_FOUND; }
}
mStreamList->Add(mSavedRequests[i].mBodyId, Move(stream));
}
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
mStreamList->Activate(mCacheId);
aListener->OnOpComplete(Move(aRv), CacheKeysResult(), mSavedRequests,
mStreamList);
mStreamList = nullptr;
}
virtual bool MatchesCacheId(CacheId aCacheId) const override
{
return aCacheId == mCacheId;
}
private:
const CacheId mCacheId;
const CacheKeysArgs mArgs;
RefPtr<StreamList> mStreamList;
nsTArray<SavedRequest> mSavedRequests;
};
// ----------------------------------------------------------------------------
class Manager::StorageMatchAction final : public Manager::BaseAction
{
public:
StorageMatchAction(Manager* aManager, ListenerId aListenerId,
Namespace aNamespace,
const StorageMatchArgs& aArgs,
StreamList* aStreamList)
: BaseAction(aManager, aListenerId)
, mNamespace(aNamespace)
, mArgs(aArgs)
, mStreamList(aStreamList)
, mFoundResponse(false)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
nsresult rv = db::StorageMatch(aConn, mNamespace, mArgs.request(),
mArgs.params(), &mFoundResponse,
&mSavedResponse);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (!mFoundResponse || !mSavedResponse.mHasBodyId
|| IsHeadRequest(mArgs.request(), mArgs.params())) {
mSavedResponse.mHasBodyId = false;
return rv;
}
nsCOMPtr<nsIInputStream> stream;
if (mArgs.openMode() == OpenMode::Eager) {
rv = BodyOpen(aQuotaInfo, aDBDir, mSavedResponse.mBodyId,
getter_AddRefs(stream));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (NS_WARN_IF(!stream)) { return NS_ERROR_FILE_NOT_FOUND; }
}
mStreamList->Add(mSavedResponse.mBodyId, Move(stream));
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
if (!mFoundResponse) {
aListener->OnOpComplete(Move(aRv), StorageMatchResult(void_t()));
} else {
mStreamList->Activate(mSavedResponse.mCacheId);
aListener->OnOpComplete(Move(aRv), StorageMatchResult(void_t()), mSavedResponse,
mStreamList);
}
mStreamList = nullptr;
}
private:
const Namespace mNamespace;
const StorageMatchArgs mArgs;
RefPtr<StreamList> mStreamList;
bool mFoundResponse;
SavedResponse mSavedResponse;
};
// ----------------------------------------------------------------------------
class Manager::StorageHasAction final : public Manager::BaseAction
{
public:
StorageHasAction(Manager* aManager, ListenerId aListenerId,
Namespace aNamespace, const StorageHasArgs& aArgs)
: BaseAction(aManager, aListenerId)
, mNamespace(aNamespace)
, mArgs(aArgs)
, mCacheFound(false)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
CacheId cacheId;
return db::StorageGetCacheId(aConn, mNamespace, mArgs.key(),
&mCacheFound, &cacheId);
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
aListener->OnOpComplete(Move(aRv), StorageHasResult(mCacheFound));
}
private:
const Namespace mNamespace;
const StorageHasArgs mArgs;
bool mCacheFound;
};
// ----------------------------------------------------------------------------
class Manager::StorageOpenAction final : public Manager::BaseAction
{
public:
StorageOpenAction(Manager* aManager, ListenerId aListenerId,
Namespace aNamespace, const StorageOpenArgs& aArgs)
: BaseAction(aManager, aListenerId)
, mNamespace(aNamespace)
, mArgs(aArgs)
, mCacheId(INVALID_CACHE_ID)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
// Cache does not exist, create it instead
mozStorageTransaction trans(aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
// Look for existing cache
bool cacheFound;
nsresult rv = db::StorageGetCacheId(aConn, mNamespace, mArgs.key(),
&cacheFound, &mCacheId);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (cacheFound) {
MOZ_DIAGNOSTIC_ASSERT(mCacheId != INVALID_CACHE_ID);
return rv;
}
rv = db::CreateCacheId(aConn, &mCacheId);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = db::StoragePutCache(aConn, mNamespace, mArgs.key(), mCacheId);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = trans.Commit();
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
MOZ_DIAGNOSTIC_ASSERT(mCacheId != INVALID_CACHE_ID);
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
MOZ_DIAGNOSTIC_ASSERT(aRv.Failed() || mCacheId != INVALID_CACHE_ID);
aListener->OnOpComplete(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(Manager* aManager, ListenerId aListenerId,
Namespace aNamespace, const StorageDeleteArgs& aArgs)
: BaseAction(aManager, aListenerId)
, mNamespace(aNamespace)
, mArgs(aArgs)
, mCacheDeleted(false)
, mCacheId(INVALID_CACHE_ID)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
mozStorageTransaction trans(aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
bool exists;
nsresult rv = db::StorageGetCacheId(aConn, mNamespace, mArgs.key(),
&exists, &mCacheId);
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (!exists) {
mCacheDeleted = false;
return NS_OK;
}
// Don't delete the removing padding size here, we'll delete it on
// DeleteOrphanedCacheAction.
rv = db::StorageForgetCache(aConn, mNamespace, mArgs.key());
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
rv = trans.Commit();
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
mCacheDeleted = true;
return rv;
}
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
RefPtr<Context> context = mManager->mContext;
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
context->CancelForCacheId(mCacheId);
RefPtr<Action> action =
new DeleteOrphanedCacheAction(mManager, mCacheId);
context->Dispatch(action);
}
}
}
aListener->OnOpComplete(Move(aRv), StorageDeleteResult(mCacheDeleted));
}
private:
const Namespace mNamespace;
const StorageDeleteArgs mArgs;
bool mCacheDeleted;
CacheId mCacheId;
};
// ----------------------------------------------------------------------------
class Manager::StorageKeysAction final : public Manager::BaseAction
{
public:
StorageKeysAction(Manager* aManager, ListenerId aListenerId,
Namespace aNamespace)
: BaseAction(aManager, aListenerId)
, mNamespace(aNamespace)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
return db::StorageGetKeys(aConn, mNamespace, mKeys);
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
if (aRv.Failed()) {
mKeys.Clear();
}
aListener->OnOpComplete(Move(aRv), StorageKeysResult(mKeys));
}
private:
const Namespace mNamespace;
nsTArray<nsString> mKeys;
};
// ----------------------------------------------------------------------------
class Manager::OpenStreamAction final : public Manager::BaseAction
{
public:
OpenStreamAction(Manager* aManager, ListenerId aListenerId,
InputStreamResolver&& aResolver, const nsID& aBodyId)
: BaseAction(aManager, aListenerId)
, mResolver(Move(aResolver))
, mBodyId(aBodyId)
{ }
virtual nsresult
RunSyncWithDBOnTarget(const QuotaInfo& aQuotaInfo, nsIFile* aDBDir,
mozIStorageConnection* aConn) override
{
nsresult rv = BodyOpen(aQuotaInfo, aDBDir, mBodyId,
getter_AddRefs(mBodyStream));
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
if (NS_WARN_IF(!mBodyStream)) { return NS_ERROR_FILE_NOT_FOUND; }
return rv;
}
virtual void
Complete(Listener* aListener, ErrorResult&& aRv) override
{
mResolver(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(Move(aRv), aResult, INVALID_CACHE_ID, nsTArray<SavedResponse>(),
nsTArray<SavedRequest>(), nullptr);
}
void
Manager::Listener::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult,
CacheId aOpenedCacheId)
{
OnOpComplete(Move(aRv), aResult, aOpenedCacheId, nsTArray<SavedResponse>(),
nsTArray<SavedRequest>(), nullptr);
}
void
Manager::Listener::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult,
const SavedResponse& aSavedResponse,
StreamList* aStreamList)
{
AutoTArray<SavedResponse, 1> responseList;
responseList.AppendElement(aSavedResponse);
OnOpComplete(Move(aRv), aResult, INVALID_CACHE_ID, responseList,
nsTArray<SavedRequest>(), aStreamList);
}
void
Manager::Listener::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult,
const nsTArray<SavedResponse>& aSavedResponseList,
StreamList* aStreamList)
{
OnOpComplete(Move(aRv), aResult, INVALID_CACHE_ID, aSavedResponseList,
nsTArray<SavedRequest>(), aStreamList);
}
void
Manager::Listener::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult,
const nsTArray<SavedRequest>& aSavedRequestList,
StreamList* aStreamList)
{
OnOpComplete(Move(aRv), aResult, INVALID_CACHE_ID, nsTArray<SavedResponse>(),
aSavedRequestList, aStreamList);
}
// static
nsresult
Manager::GetOrCreate(ManagerId* aManagerId, Manager** aManagerOut)
{
mozilla::ipc::AssertIsOnBackgroundThread();
return Factory::GetOrCreate(aManagerId, aManagerOut);
}
// static
already_AddRefed<Manager>
Manager::Get(ManagerId* aManagerId)
{
mozilla::ipc::AssertIsOnBackgroundThread();
return Factory::Get(aManagerId);
}
// static
void
Manager::ShutdownAll()
{
mozilla::ipc::AssertIsOnBackgroundThread();
Factory::ShutdownAll();
if (!mozilla::SpinEventLoopUntil([]() {
return Factory::IsShutdownAllComplete();
})) {
NS_WARNING("Something bad happened!");
}
}
// static
void
Manager::Abort(const nsACString& aOrigin)
{
mozilla::ipc::AssertIsOnBackgroundThread();
Factory::Abort(aOrigin);
}
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.
for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
if (mCacheIdRefs[i].mOrphaned) {
aContext->NoteOrphanedData();
break;
}
}
for (uint32_t i = 0; i < mBodyIdRefs.Length(); ++i) {
if (mBodyIdRefs[i].mOrphaned) {
aContext->NoteOrphanedData();
break;
}
}
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);
for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
if (mCacheIdRefs[i].mCacheId == aCacheId) {
mCacheIdRefs[i].mCount += 1;
return;
}
}
CacheIdRefCounter* entry = mCacheIdRefs.AppendElement();
entry->mCacheId = aCacheId;
entry->mCount = 1;
entry->mOrphaned = false;
}
void
Manager::ReleaseCacheId(CacheId aCacheId)
{
NS_ASSERT_OWNINGTHREAD(Manager);
for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
if (mCacheIdRefs[i].mCacheId == aCacheId) {
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
uint32_t oldRef = mCacheIdRefs[i].mCount;
#endif
mCacheIdRefs[i].mCount -= 1;
MOZ_DIAGNOSTIC_ASSERT(mCacheIdRefs[i].mCount < oldRef);
if (mCacheIdRefs[i].mCount == 0) {
bool orphaned = mCacheIdRefs[i].mOrphaned;
mCacheIdRefs.RemoveElementAt(i);
RefPtr<Context> context = mContext;
// If the context is already gone, then orphan flag should have been
// set in RemoveContext().
if (orphaned && context) {
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
context->CancelForCacheId(aCacheId);
RefPtr<Action> action = new DeleteOrphanedCacheAction(this,
aCacheId);
context->Dispatch(action);
}
}
}
MaybeAllowContextToClose();
return;
}
}
MOZ_ASSERT_UNREACHABLE("Attempt to release CacheId that is not referenced!");
}
void
Manager::AddRefBodyId(const nsID& aBodyId)
{
NS_ASSERT_OWNINGTHREAD(Manager);
for (uint32_t i = 0; i < mBodyIdRefs.Length(); ++i) {
if (mBodyIdRefs[i].mBodyId == aBodyId) {
mBodyIdRefs[i].mCount += 1;
return;
}
}
BodyIdRefCounter* entry = mBodyIdRefs.AppendElement();
entry->mBodyId = aBodyId;
entry->mCount = 1;
entry->mOrphaned = false;
}
void
Manager::ReleaseBodyId(const nsID& aBodyId)
{
NS_ASSERT_OWNINGTHREAD(Manager);
for (uint32_t i = 0; i < mBodyIdRefs.Length(); ++i) {
if (mBodyIdRefs[i].mBodyId == aBodyId) {
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
uint32_t oldRef = mBodyIdRefs[i].mCount;
#endif
mBodyIdRefs[i].mCount -= 1;
MOZ_DIAGNOSTIC_ASSERT(mBodyIdRefs[i].mCount < oldRef);
if (mBodyIdRefs[i].mCount < 1) {
bool orphaned = mBodyIdRefs[i].mOrphaned;
mBodyIdRefs.RemoveElementAt(i);
RefPtr<Context> context = mContext;
// If the context is already gone, then orphan flag should have been
// set in RemoveContext().
if (orphaned && context) {
if (context->IsCanceled()) {
context->NoteOrphanedData();
} else {
RefPtr<Action> action = new DeleteOrphanedBodyAction(aBodyId);
context->Dispatch(action);
}
}
}
MaybeAllowContextToClose();
return;
}
}
MOZ_ASSERT_UNREACHABLE("Attempt to release BodyId that is not referenced!");
}
already_AddRefed<ManagerId>
Manager::GetManagerId() const
{
RefPtr<ManagerId> ref = mManagerId;
return ref.forget();
}
void
Manager::AddStreamList(StreamList* aStreamList)
{
NS_ASSERT_OWNINGTHREAD(Manager);
MOZ_DIAGNOSTIC_ASSERT(aStreamList);
mStreamLists.AppendElement(aStreamList);
}
void
Manager::RemoveStreamList(StreamList* aStreamList)
{
NS_ASSERT_OWNINGTHREAD(Manager);
MOZ_DIAGNOSTIC_ASSERT(aStreamList);
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;
}
RefPtr<Context> context = mContext;
MOZ_DIAGNOSTIC_ASSERT(!context->IsCanceled());
RefPtr<StreamList> streamList = new StreamList(this, context);
ListenerId listenerId = SaveListener(aListener);
RefPtr<Action> action;
switch(aOpArgs.type()) {
case CacheOpArgs::TCacheMatchArgs:
action = new CacheMatchAction(this, listenerId, aCacheId,
aOpArgs.get_CacheMatchArgs(), streamList);
break;
case CacheOpArgs::TCacheMatchAllArgs:
action = new CacheMatchAllAction(this, listenerId, aCacheId,
aOpArgs.get_CacheMatchAllArgs(),
streamList);
break;
case CacheOpArgs::TCacheDeleteArgs:
action = new CacheDeleteAction(this, listenerId, aCacheId,
aOpArgs.get_CacheDeleteArgs());
break;
case CacheOpArgs::TCacheKeysArgs:
action = new CacheKeysAction(this, listenerId, aCacheId,
aOpArgs.get_CacheKeysArgs(), streamList);
break;
default:
MOZ_CRASH("Unknown Cache operation!");
}
context->Dispatch(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;
}
RefPtr<Context> context = mContext;
MOZ_DIAGNOSTIC_ASSERT(!context->IsCanceled());
RefPtr<StreamList> streamList = new StreamList(this, context);
ListenerId listenerId = SaveListener(aListener);
RefPtr<Action> action;
switch(aOpArgs.type()) {
case CacheOpArgs::TStorageMatchArgs:
action = new StorageMatchAction(this, listenerId, aNamespace,
aOpArgs.get_StorageMatchArgs(),
streamList);
break;
case CacheOpArgs::TStorageHasArgs:
action = new StorageHasAction(this, listenerId, aNamespace,
aOpArgs.get_StorageHasArgs());
break;
case CacheOpArgs::TStorageOpenArgs:
action = new StorageOpenAction(this, listenerId, aNamespace,
aOpArgs.get_StorageOpenArgs());
break;
case CacheOpArgs::TStorageDeleteArgs:
action = new StorageDeleteAction(this, listenerId, aNamespace,
aOpArgs.get_StorageDeleteArgs());
break;
case CacheOpArgs::TStorageKeysArgs:
action = new StorageKeysAction(this, listenerId, aNamespace);
break;
default:
MOZ_CRASH("Unknown CacheStorage operation!");
}
context->Dispatch(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;
}
RefPtr<Context> context = mContext;
MOZ_DIAGNOSTIC_ASSERT(!context->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);
RefPtr<Action> action =
new OpenStreamAction(this, listenerId, Move(aResolver), aBodyId);
context->Dispatch(action);
}
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;
}
RefPtr<Context> context = mContext;
MOZ_DIAGNOSTIC_ASSERT(!context->IsCanceled());
ListenerId listenerId = SaveListener(aListener);
RefPtr<Action> action = new CachePutAllAction(this, listenerId, aCacheId,
aPutList, aRequestStreamList,
aResponseStreamList);
context->Dispatch(action);
}
Manager::Manager(ManagerId* aManagerId, nsIThread* aIOThread)
: mManagerId(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::Shutdown",
ioThread, &nsIThread::Shutdown)));
}
void
Manager::Init(Manager* aOldManager)
{
NS_ASSERT_OWNINGTHREAD(Manager);
RefPtr<Context> oldContext;
if (aOldManager) {
oldContext = aOldManager->mContext;
}
// 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.
RefPtr<Action> setupAction = new SetupAction();
RefPtr<Context> ref = Context::Create(this, mIOThread->SerialEventTarget(), setupAction,
oldContext);
mContext = ref;
}
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) {
RefPtr<Context> context = mContext;
context->CancelAll();
return;
}
}
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.
RefPtr<Context> context = mContext;
context->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);
for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
if (mCacheIdRefs[i].mCacheId == aCacheId) {
MOZ_DIAGNOSTIC_ASSERT(mCacheIdRefs[i].mCount > 0);
MOZ_DIAGNOSTIC_ASSERT(!mCacheIdRefs[i].mOrphaned);
mCacheIdRefs[i].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);
for (uint32_t i = 0; i < mBodyIdRefs.Length(); ++i) {
if (mBodyIdRefs[i].mBodyId == aBodyId) {
MOZ_DIAGNOSTIC_ASSERT(mBodyIdRefs[i].mCount > 0);
MOZ_DIAGNOSTIC_ASSERT(!mBodyIdRefs[i].mOrphaned);
mBodyIdRefs[i].mOrphaned = true;
return true;
}
}
return false;
}
void
Manager::NoteOrphanedBodyIdList(const nsTArray<nsID>& aDeletedBodyIdList)
{
NS_ASSERT_OWNINGTHREAD(Manager);
AutoTArray<nsID, 64> deleteNowList;
deleteNowList.SetCapacity(aDeletedBodyIdList.Length());
for (uint32_t i = 0; i < aDeletedBodyIdList.Length(); ++i) {
if (!SetBodyIdOrphanedIfRefed(aDeletedBodyIdList[i])) {
deleteNowList.AppendElement(aDeletedBodyIdList[i]);
}
}
// TODO: note that we need to check these bodies for staleness on startup (bug 1110446)
RefPtr<Context> context = mContext;
if (!deleteNowList.IsEmpty() && context && !context->IsCanceled()) {
RefPtr<Action> action = new DeleteOrphanedBodyAction(deleteNowList);
context->Dispatch(action);
}
}
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.
RefPtr<Context> context = mContext;
if (context && 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();
context->AllowToClose();
}
}
} // namespace cache
} // namespace dom
} // namespace mozilla