diff options
Diffstat (limited to 'netwerk/cache2/CacheEntry.cpp')
-rw-r--r-- | netwerk/cache2/CacheEntry.cpp | 1943 |
1 files changed, 1943 insertions, 0 deletions
diff --git a/netwerk/cache2/CacheEntry.cpp b/netwerk/cache2/CacheEntry.cpp new file mode 100644 index 0000000000..9b82dce25f --- /dev/null +++ b/netwerk/cache2/CacheEntry.cpp @@ -0,0 +1,1943 @@ +/* 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 <algorithm> +#include <math.h> + +#include "CacheEntry.h" + +#include "CacheFileUtils.h" +#include "CacheIndex.h" +#include "CacheLog.h" +#include "CacheObserver.h" +#include "CacheStorageService.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Telemetry.h" +#include "mozilla/psm/TransportSecurityInfo.h" +#include "nsComponentManagerUtils.h" +#include "nsIAsyncOutputStream.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheStorage.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISeekableStream.h" +#include "nsISizeOf.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla::net { + +static uint32_t const ENTRY_WANTED = nsICacheEntryOpenCallback::ENTRY_WANTED; +static uint32_t const RECHECK_AFTER_WRITE_FINISHED = + nsICacheEntryOpenCallback::RECHECK_AFTER_WRITE_FINISHED; +static uint32_t const ENTRY_NEEDS_REVALIDATION = + nsICacheEntryOpenCallback::ENTRY_NEEDS_REVALIDATION; +static uint32_t const ENTRY_NOT_WANTED = + nsICacheEntryOpenCallback::ENTRY_NOT_WANTED; + +NS_IMPL_ISUPPORTS(CacheEntryHandle, nsICacheEntry) + +// CacheEntryHandle + +CacheEntryHandle::CacheEntryHandle(CacheEntry* aEntry) : mEntry(aEntry) { +#ifdef DEBUG + if (!mEntry->HandlesCount()) { + // CacheEntry.mHandlesCount must go from zero to one only under + // the service lock. Can access CacheStorageService::Self() w/o a check + // since CacheEntry hrefs it. + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + } +#endif + + mEntry->AddHandleRef(); + + LOG(("New CacheEntryHandle %p for entry %p", this, aEntry)); +} + +NS_IMETHODIMP CacheEntryHandle::Dismiss() { + LOG(("CacheEntryHandle::Dismiss %p", this)); + + if (mClosed.compareExchange(false, true)) { + mEntry->OnHandleClosed(this); + return NS_OK; + } + + LOG((" already dropped")); + return NS_ERROR_UNEXPECTED; +} + +CacheEntryHandle::~CacheEntryHandle() { + mEntry->ReleaseHandleRef(); + Dismiss(); + + LOG(("CacheEntryHandle::~CacheEntryHandle %p", this)); +} + +// CacheEntry::Callback + +CacheEntry::Callback::Callback(CacheEntry* aEntry, + nsICacheEntryOpenCallback* aCallback, + bool aReadOnly, bool aCheckOnAnyThread, + bool aSecret) + : mEntry(aEntry), + mCallback(aCallback), + mTarget(GetCurrentEventTarget()), + mReadOnly(aReadOnly), + mRevalidating(false), + mCheckOnAnyThread(aCheckOnAnyThread), + mRecheckAfterWrite(false), + mNotWanted(false), + mSecret(aSecret), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::Callback(CacheEntry* aEntry, + bool aDoomWhenFoundInPinStatus) + : mEntry(aEntry), + mReadOnly(false), + mRevalidating(false), + mCheckOnAnyThread(true), + mRecheckAfterWrite(false), + mNotWanted(false), + mSecret(false), + mDoomWhenFoundPinned(aDoomWhenFoundInPinStatus), + mDoomWhenFoundNonPinned(!aDoomWhenFoundInPinStatus) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::Callback(CacheEntry::Callback const& aThat) + : mEntry(aThat.mEntry), + mCallback(aThat.mCallback), + mTarget(aThat.mTarget), + mReadOnly(aThat.mReadOnly), + mRevalidating(aThat.mRevalidating), + mCheckOnAnyThread(aThat.mCheckOnAnyThread), + mRecheckAfterWrite(aThat.mRecheckAfterWrite), + mNotWanted(aThat.mNotWanted), + mSecret(aThat.mSecret), + mDoomWhenFoundPinned(aThat.mDoomWhenFoundPinned), + mDoomWhenFoundNonPinned(aThat.mDoomWhenFoundNonPinned) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::~Callback() { + ProxyRelease("CacheEntry::Callback::mCallback", mCallback, mTarget); + + mEntry->ReleaseHandleRef(); + MOZ_COUNT_DTOR(CacheEntry::Callback); +} + +// We have locks on both this and aEntry +void CacheEntry::Callback::ExchangeEntry(CacheEntry* aEntry) { + aEntry->mLock.AssertCurrentThreadOwns(); + mEntry->mLock.AssertCurrentThreadOwns(); + if (mEntry == aEntry) return; + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(aEntry->HandlesCount()); + aEntry->AddHandleRef(); + mEntry->ReleaseHandleRef(); + mEntry = aEntry; +} + +// This is called on entries in another entry's mCallback array, under the lock +// of that other entry. No other threads can access this entry at this time. +bool CacheEntry::Callback::DeferDoom(bool* aDoom) const + MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_ASSERT(mEntry->mPinningKnown); + + if (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) || + MOZ_UNLIKELY(mDoomWhenFoundPinned)) { + *aDoom = + (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) && + MOZ_LIKELY(!mEntry->mPinned)) || + (MOZ_UNLIKELY(mDoomWhenFoundPinned) && MOZ_UNLIKELY(mEntry->mPinned)); + + return true; + } + + return false; +} + +nsresult CacheEntry::Callback::OnCheckThread(bool* aOnCheckThread) const { + if (!mCheckOnAnyThread) { + // Check we are on the target + return mTarget->IsOnCurrentThread(aOnCheckThread); + } + + // We can invoke check anywhere + *aOnCheckThread = true; + return NS_OK; +} + +nsresult CacheEntry::Callback::OnAvailThread(bool* aOnAvailThread) const { + return mTarget->IsOnCurrentThread(aOnAvailThread); +} + +// CacheEntry + +NS_IMPL_ISUPPORTS(CacheEntry, nsIRunnable, CacheFileListener) + +/* static */ +uint64_t CacheEntry::GetNextId() { + static Atomic<uint64_t, Relaxed> id(0); + return ++id; +} + +CacheEntry::CacheEntry(const nsACString& aStorageID, const nsACString& aURI, + const nsACString& aEnhanceID, bool aUseDisk, + bool aSkipSizeCheck, bool aPin) + : mURI(aURI), + mEnhanceID(aEnhanceID), + mStorageID(aStorageID), + mUseDisk(aUseDisk), + mSkipSizeCheck(aSkipSizeCheck), + mPinned(aPin), + mSecurityInfoLoaded(false), + mPreventCallbacks(false), + mHasData(false), + mPinningKnown(false), + mCacheEntryId(GetNextId()) { + LOG(("CacheEntry::CacheEntry [this=%p]", this)); + + mService = CacheStorageService::Self(); + + CacheStorageService::Self()->RecordMemoryOnlyEntry(this, !aUseDisk, + true /* overwrite */); +} + +CacheEntry::~CacheEntry() { LOG(("CacheEntry::~CacheEntry [this=%p]", this)); } + +char const* CacheEntry::StateString(uint32_t aState) { + switch (aState) { + case NOTLOADED: + return "NOTLOADED"; + case LOADING: + return "LOADING"; + case EMPTY: + return "EMPTY"; + case WRITING: + return "WRITING"; + case READY: + return "READY"; + case REVALIDATING: + return "REVALIDATING"; + } + + return "?"; +} + +nsresult CacheEntry::HashingKeyWithStorage(nsACString& aResult) const { + return HashingKey(mStorageID, mEnhanceID, mURI, aResult); +} + +nsresult CacheEntry::HashingKey(nsACString& aResult) const { + return HashingKey(""_ns, mEnhanceID, mURI, aResult); +} + +// static +nsresult CacheEntry::HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, nsIURI* aURI, + nsACString& aResult) { + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return HashingKey(aStorageID, aEnhanceID, spec, aResult); +} + +// static +nsresult CacheEntry::HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, + const nsACString& aURISpec, + nsACString& aResult) { + /** + * This key is used to salt hash that is a base for disk file name. + * Changing it will cause we will not be able to find files on disk. + */ + + aResult.Assign(aStorageID); + + if (!aEnhanceID.IsEmpty()) { + CacheFileUtils::AppendTagWithValue(aResult, '~', aEnhanceID); + } + + // Appending directly + aResult.Append(':'); + aResult.Append(aURISpec); + + return NS_OK; +} + +void CacheEntry::AsyncOpen(nsICacheEntryOpenCallback* aCallback, + uint32_t aFlags) { + bool readonly = aFlags & nsICacheStorage::OPEN_READONLY; + bool bypassIfBusy = aFlags & nsICacheStorage::OPEN_BYPASS_IF_BUSY; + bool truncate = aFlags & nsICacheStorage::OPEN_TRUNCATE; + bool priority = aFlags & nsICacheStorage::OPEN_PRIORITY; + bool multithread = aFlags & nsICacheStorage::CHECK_MULTITHREADED; + bool secret = aFlags & nsICacheStorage::OPEN_SECRETLY; + + if (MOZ_LOG_TEST(gCache2Log, LogLevel::Debug)) { + MutexAutoLock lock(mLock); + LOG(("CacheEntry::AsyncOpen [this=%p, state=%s, flags=%d, callback=%p]", + this, StateString(mState), aFlags, aCallback)); + } +#ifdef DEBUG + { + // yes, if logging is on in DEBUG we'll take the lock twice in a row + MutexAutoLock lock(mLock); + MOZ_ASSERT(!readonly || !truncate, "Bad flags combination"); + MOZ_ASSERT(!(truncate && mState > LOADING), + "Must not call truncate on already loaded entry"); + } +#endif + + Callback callback(this, aCallback, readonly, multithread, secret); + + if (!Open(callback, truncate, priority, bypassIfBusy)) { + // We get here when the callback wants to bypass cache when it's busy. + LOG((" writing or revalidating, callback wants to bypass cache")); + callback.mNotWanted = true; + InvokeAvailableCallback(callback); + } +} + +bool CacheEntry::Open(Callback& aCallback, bool aTruncate, bool aPriority, + bool aBypassIfBusy) { + mozilla::MutexAutoLock lock(mLock); + + // Check state under the lock + if (aBypassIfBusy && (mState == WRITING || mState == REVALIDATING)) { + return false; + } + + RememberCallback(aCallback); + + // Load() opens the lock + if (Load(aTruncate, aPriority)) { + // Loading is in progress... + return true; + } + + InvokeCallbacks(); + + return true; +} + +bool CacheEntry::Load(bool aTruncate, bool aPriority) { + LOG(("CacheEntry::Load [this=%p, trunc=%d]", this, aTruncate)); + + mLock.AssertCurrentThreadOwns(); + + if (mState > LOADING) { + LOG((" already loaded")); + return false; + } + + if (mState == LOADING) { + LOG((" already loading")); + return true; + } + + mState = LOADING; + + MOZ_ASSERT(!mFile); + + nsresult rv; + + nsAutoCString fileKey; + rv = HashingKeyWithStorage(fileKey); + + bool reportMiss = false; + + // Check the index under two conditions for two states and take appropriate + // action: + // 1. When this is a disk entry and not told to truncate, check there is a + // disk file. + // If not, set the 'truncate' flag to true so that this entry will open + // instantly as a new one. + // 2. When this is a memory-only entry, check there is a disk file. + // If there is or could be, doom that file. + if ((!aTruncate || !mUseDisk) && NS_SUCCEEDED(rv)) { + // Check the index right now to know we have or have not the entry + // as soon as possible. + CacheIndex::EntryStatus status; + if (NS_SUCCEEDED(CacheIndex::HasEntry(fileKey, &status))) { + switch (status) { + case CacheIndex::DOES_NOT_EXIST: + // Doesn't apply to memory-only entries, Load() is called only once + // for them and never again for their session lifetime. + if (!aTruncate && mUseDisk) { + LOG( + (" entry doesn't exist according information from the index, " + "truncating")); + reportMiss = true; + aTruncate = true; + } + break; + case CacheIndex::EXISTS: + case CacheIndex::DO_NOT_KNOW: + if (!mUseDisk) { + LOG( + (" entry open as memory-only, but there is a file, status=%d, " + "dooming it", + status)); + CacheFileIOManager::DoomFileByKey(fileKey, nullptr); + } + break; + } + } + } + + mFile = new CacheFile(); + + BackgroundOp(Ops::REGISTER); + + bool directLoad = aTruncate || !mUseDisk; + if (directLoad) { + // mLoadStart will be used to calculate telemetry of life-time of this + // entry. Low resulution is then enough. + mLoadStart = TimeStamp::NowLoRes(); + mPinningKnown = true; + } else { + mLoadStart = TimeStamp::Now(); + } + + { + mozilla::MutexAutoUnlock unlock(mLock); + + if (reportMiss) { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::MISS, mLoadStart); + } + + LOG((" performing load, file=%p", mFile.get())); + if (NS_SUCCEEDED(rv)) { + rv = mFile->Init(fileKey, aTruncate, !mUseDisk, mSkipSizeCheck, aPriority, + mPinned, directLoad ? nullptr : this); + } + + if (NS_FAILED(rv)) { + mFileStatus = rv; + AsyncDoom(nullptr); + return false; + } + } + + if (directLoad) { + // Just fake the load has already been done as "new". + mFileStatus = NS_OK; + mState = EMPTY; + } + + return mState == LOADING; +} + +NS_IMETHODIMP CacheEntry::OnFileReady(nsresult aResult, bool aIsNew) { + LOG(("CacheEntry::OnFileReady [this=%p, rv=0x%08" PRIx32 ", new=%d]", this, + static_cast<uint32_t>(aResult), aIsNew)); + + MOZ_ASSERT(!mLoadStart.IsNull()); + + if (NS_SUCCEEDED(aResult)) { + if (aIsNew) { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::MISS, mLoadStart); + } else { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::HIT, mLoadStart); + } + } + + // OnFileReady, that is the only code that can transit from LOADING + // to any follow-on state and can only be invoked ones on an entry. + // Until this moment there is no consumer that could manipulate + // the entry state. + + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState == LOADING); + + mState = (aIsNew || NS_FAILED(aResult)) ? EMPTY : READY; + + mFileStatus = aResult; + + mPinned = mFile->IsPinned(); + + mPinningKnown = true; + LOG((" pinning=%d", (bool)mPinned)); + + if (mState == READY) { + mHasData = true; + + uint32_t frecency; + mFile->GetFrecency(&frecency); + // mFrecency is held in a double to increase computance precision. + // It is ok to persist frecency only as a uint32 with some math involved. + mFrecency = INT2FRECENCY(frecency); + } + + InvokeCallbacks(); + + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::OnFileDoomed(nsresult aResult) { + if (mDoomCallback) { + RefPtr<DoomCallbackRunnable> event = + new DoomCallbackRunnable(this, aResult); + NS_DispatchToMainThread(event); + } + + return NS_OK; +} + +already_AddRefed<CacheEntryHandle> CacheEntry::ReopenTruncated( + bool aMemoryOnly, nsICacheEntryOpenCallback* aCallback) { + LOG(("CacheEntry::ReopenTruncated [this=%p]", this)); + + mLock.AssertCurrentThreadOwns(); + + // Hold callbacks invocation, AddStorageEntry would invoke from doom + // prematurly + mPreventCallbacks = true; + + RefPtr<CacheEntryHandle> handle; + RefPtr<CacheEntry> newEntry; + { + if (mPinned) { + MOZ_ASSERT(mUseDisk); + // We want to pin even no-store entries (the case we recreate a disk entry + // as a memory-only entry.) + aMemoryOnly = false; + } + + mozilla::MutexAutoUnlock unlock(mLock); + + // The following call dooms this entry (calls DoomAlreadyRemoved on us) + nsresult rv = CacheStorageService::Self()->AddStorageEntry( + GetStorageID(), GetURI(), GetEnhanceID(), mUseDisk && !aMemoryOnly, + mSkipSizeCheck, mPinned, + nsICacheStorage::OPEN_TRUNCATE, // truncate existing (this one) + getter_AddRefs(handle)); + + if (NS_SUCCEEDED(rv)) { + newEntry = handle->Entry(); + LOG((" exchanged entry %p by entry %p, rv=0x%08" PRIx32, this, + newEntry.get(), static_cast<uint32_t>(rv))); + newEntry->AsyncOpen(aCallback, nsICacheStorage::OPEN_TRUNCATE); + } else { + LOG((" exchanged of entry %p failed, rv=0x%08" PRIx32, this, + static_cast<uint32_t>(rv))); + AsyncDoom(nullptr); + } + } + + mPreventCallbacks = false; + + if (!newEntry) return nullptr; + + newEntry->TransferCallbacks(*this); + mCallbacks.Clear(); + + // Must return a new write handle, since the consumer is expected to + // write to this newly recreated entry. The |handle| is only a common + // reference counter and doesn't revert entry state back when write + // fails and also doesn't update the entry frecency. Not updating + // frecency causes entries to not be purged from our memory pools. + RefPtr<CacheEntryHandle> writeHandle = newEntry->NewWriteHandle(); + return writeHandle.forget(); +} + +void CacheEntry::TransferCallbacks(CacheEntry& aFromEntry) { + mozilla::MutexAutoLock lock(mLock); + aFromEntry.mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::TransferCallbacks [entry=%p, from=%p]", this, &aFromEntry)); + + if (!mCallbacks.Length()) { + mCallbacks.SwapElements(aFromEntry.mCallbacks); + } else { + mCallbacks.AppendElements(aFromEntry.mCallbacks); + } + + uint32_t callbacksLength = mCallbacks.Length(); + if (callbacksLength) { + // Carry the entry reference (unfortunately, needs to be done manually...) + for (uint32_t i = 0; i < callbacksLength; ++i) { + mCallbacks[i].ExchangeEntry(this); + } + + BackgroundOp(Ops::CALLBACKS, true); + } +} + +void CacheEntry::RememberCallback(Callback& aCallback) { + mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::RememberCallback [this=%p, cb=%p, state=%s]", this, + aCallback.mCallback.get(), StateString(mState))); + + mCallbacks.AppendElement(aCallback); +} + +void CacheEntry::InvokeCallbacksLock() { + mozilla::MutexAutoLock lock(mLock); + InvokeCallbacks(); +} + +void CacheEntry::InvokeCallbacks() { + mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::InvokeCallbacks BEGIN [this=%p]", this)); + + // Invoke first all r/w callbacks, then all r/o callbacks. + if (InvokeCallbacks(false)) InvokeCallbacks(true); + + LOG(("CacheEntry::InvokeCallbacks END [this=%p]", this)); +} + +bool CacheEntry::InvokeCallbacks(bool aReadOnly) { + mLock.AssertCurrentThreadOwns(); + + RefPtr<CacheEntryHandle> recreatedHandle; + + uint32_t i = 0; + while (i < mCallbacks.Length()) { + if (mPreventCallbacks) { + LOG((" callbacks prevented!")); + return false; + } + + if (!mIsDoomed && (mState == WRITING || mState == REVALIDATING)) { + LOG((" entry is being written/revalidated")); + return false; + } + + bool recreate; + if (mCallbacks[i].DeferDoom(&recreate)) { + mCallbacks.RemoveElementAt(i); + if (!recreate) { + continue; + } + + LOG((" defer doom marker callback hit positive, recreating")); + recreatedHandle = ReopenTruncated(!mUseDisk, nullptr); + break; + } + + if (mCallbacks[i].mReadOnly != aReadOnly) { + // Callback is not r/w or r/o, go to another one in line + ++i; + continue; + } + + bool onCheckThread; + nsresult rv = mCallbacks[i].OnCheckThread(&onCheckThread); + + if (NS_SUCCEEDED(rv) && !onCheckThread) { + // Redispatch to the target thread + rv = mCallbacks[i].mTarget->Dispatch( + NewRunnableMethod("net::CacheEntry::InvokeCallbacksLock", this, + &CacheEntry::InvokeCallbacksLock), + nsIEventTarget::DISPATCH_NORMAL); + if (NS_SUCCEEDED(rv)) { + LOG((" re-dispatching to target thread")); + return false; + } + } + + Callback callback = mCallbacks[i]; + mCallbacks.RemoveElementAt(i); + + if (NS_SUCCEEDED(rv) && !InvokeCallback(callback)) { + // Callback didn't fire, put it back and go to another one in line. + // Only reason InvokeCallback returns false is that onCacheEntryCheck + // returns RECHECK_AFTER_WRITE_FINISHED. If we would stop the loop, other + // readers or potential writers would be unnecessarily kept from being + // invoked. + size_t pos = std::min(mCallbacks.Length(), static_cast<size_t>(i)); + mCallbacks.InsertElementAt(pos, callback); + ++i; + } + } + + if (recreatedHandle) { + // Must be released outside of the lock, enters InvokeCallback on the new + // entry + mozilla::MutexAutoUnlock unlock(mLock); + recreatedHandle = nullptr; + } + + return true; +} + +bool CacheEntry::InvokeCallback(Callback& aCallback) { + mLock.AssertCurrentThreadOwns(); + LOG(("CacheEntry::InvokeCallback [this=%p, state=%s, cb=%p]", this, + StateString(mState), aCallback.mCallback.get())); + + // When this entry is doomed we want to notify the callback any time + if (!mIsDoomed) { + // When we are here, the entry must be loaded from disk + MOZ_ASSERT(mState > LOADING); + + if (mState == WRITING || mState == REVALIDATING) { + // Prevent invoking other callbacks since one of them is now writing + // or revalidating this entry. No consumers should get this entry + // until metadata are filled with values downloaded from the server + // or the entry revalidated and output stream has been opened. + LOG((" entry is being written/revalidated, callback bypassed")); + return false; + } + + // mRecheckAfterWrite flag already set means the callback has already passed + // the onCacheEntryCheck call. Until the current write is not finished this + // callback will be bypassed. + if (!aCallback.mRecheckAfterWrite) { + if (!aCallback.mReadOnly) { + if (mState == EMPTY) { + // Advance to writing state, we expect to invoke the callback and let + // it fill content of this entry. Must set and check the state here + // to prevent more then one + mState = WRITING; + LOG((" advancing to WRITING state")); + } + + if (!aCallback.mCallback) { + // We can be given no callback only in case of recreate, it is ok + // to advance to WRITING state since the caller of recreate is + // expected to write this entry now. + return true; + } + } + + if (mState == READY) { + // Metadata present, validate the entry + uint32_t checkResult; + { + // mayhemer: TODO check and solve any potential races of concurent + // OnCacheEntryCheck + mozilla::MutexAutoUnlock unlock(mLock); + + RefPtr<CacheEntryHandle> handle = NewHandle(); + + nsresult rv = + aCallback.mCallback->OnCacheEntryCheck(handle, &checkResult); + LOG((" OnCacheEntryCheck: rv=0x%08" PRIx32 ", result=%" PRId32, + static_cast<uint32_t>(rv), static_cast<uint32_t>(checkResult))); + + if (NS_FAILED(rv)) checkResult = ENTRY_NOT_WANTED; + } + + aCallback.mRevalidating = checkResult == ENTRY_NEEDS_REVALIDATION; + + switch (checkResult) { + case ENTRY_WANTED: + // Nothing more to do here, the consumer is responsible to handle + // the result of OnCacheEntryCheck it self. + // Proceed to callback... + break; + + case RECHECK_AFTER_WRITE_FINISHED: + LOG( + (" consumer will check on the entry again after write is " + "done")); + // The consumer wants the entry to complete first. + aCallback.mRecheckAfterWrite = true; + break; + + case ENTRY_NEEDS_REVALIDATION: + LOG((" will be holding callbacks until entry is revalidated")); + // State is READY now and from that state entry cannot transit to + // any other state then REVALIDATING for which cocurrency is not an + // issue. Potentially no need to lock here. + mState = REVALIDATING; + break; + + case ENTRY_NOT_WANTED: + LOG((" consumer not interested in the entry")); + // Do not give this entry to the consumer, it is not interested in + // us. + aCallback.mNotWanted = true; + break; + } + } + } + } + + if (aCallback.mCallback) { + if (!mIsDoomed && aCallback.mRecheckAfterWrite) { + // If we don't have data and the callback wants a complete entry, + // don't invoke now. + bool bypass = !mHasData; + if (!bypass && NS_SUCCEEDED(mFileStatus)) { + int64_t _unused; + bypass = !mFile->DataSize(&_unused); + } + + if (bypass) { + LOG((" bypassing, entry data still being written")); + return false; + } + + // Entry is complete now, do the check+avail call again + aCallback.mRecheckAfterWrite = false; + return InvokeCallback(aCallback); + } + + mozilla::MutexAutoUnlock unlock(mLock); + InvokeAvailableCallback(aCallback); + } + + return true; +} + +void CacheEntry::InvokeAvailableCallback(Callback const& aCallback) { + nsresult rv; + uint32_t state; + { + mozilla::MutexAutoLock lock(mLock); + state = mState; + LOG( + ("CacheEntry::InvokeAvailableCallback [this=%p, state=%s, cb=%p, " + "r/o=%d, " + "n/w=%d]", + this, StateString(mState), aCallback.mCallback.get(), + aCallback.mReadOnly, aCallback.mNotWanted)); + + // When we are here, the entry must be loaded from disk + MOZ_ASSERT(state > LOADING || mIsDoomed); + } + + bool onAvailThread; + rv = aCallback.OnAvailThread(&onAvailThread); + if (NS_FAILED(rv)) { + LOG((" target thread dead?")); + return; + } + + if (!onAvailThread) { + // Dispatch to the right thread + RefPtr<AvailableCallbackRunnable> event = + new AvailableCallbackRunnable(this, aCallback); + + rv = aCallback.mTarget->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + LOG((" redispatched, (rv = 0x%08" PRIx32 ")", static_cast<uint32_t>(rv))); + return; + } + + if (mIsDoomed || aCallback.mNotWanted) { + LOG( + (" doomed or not wanted, notifying OCEA with " + "NS_ERROR_CACHE_KEY_NOT_FOUND")); + aCallback.mCallback->OnCacheEntryAvailable(nullptr, false, + NS_ERROR_CACHE_KEY_NOT_FOUND); + return; + } + + if (state == READY) { + LOG((" ready/has-meta, notifying OCEA with entry and NS_OK")); + + if (!aCallback.mSecret) { + mozilla::MutexAutoLock lock(mLock); + BackgroundOp(Ops::FRECENCYUPDATE); + } + + OnFetched(aCallback); + + RefPtr<CacheEntryHandle> handle = NewHandle(); + aCallback.mCallback->OnCacheEntryAvailable(handle, false, NS_OK); + return; + } + + // R/O callbacks may do revalidation, let them fall through + if (aCallback.mReadOnly && !aCallback.mRevalidating) { + LOG( + (" r/o and not ready, notifying OCEA with " + "NS_ERROR_CACHE_KEY_NOT_FOUND")); + aCallback.mCallback->OnCacheEntryAvailable(nullptr, false, + NS_ERROR_CACHE_KEY_NOT_FOUND); + return; + } + + // This is a new or potentially non-valid entry and needs to be fetched first. + // The CacheEntryHandle blocks other consumers until the channel + // either releases the entry or marks metadata as filled or whole entry valid, + // i.e. until MetaDataReady() or SetValid() on the entry is called + // respectively. + + // Consumer will be responsible to fill or validate the entry metadata and + // data. + + OnFetched(aCallback); + + RefPtr<CacheEntryHandle> handle = NewWriteHandle(); + rv = aCallback.mCallback->OnCacheEntryAvailable(handle, state == WRITING, + NS_OK); + + if (NS_FAILED(rv)) { + LOG((" writing/revalidating failed (0x%08" PRIx32 ")", + static_cast<uint32_t>(rv))); + + // Consumer given a new entry failed to take care of the entry. + OnHandleClosed(handle); + return; + } + + LOG((" writing/revalidating")); +} + +void CacheEntry::OnFetched(Callback const& aCallback) { + if (NS_SUCCEEDED(mFileStatus) && !aCallback.mSecret) { + // Let the last-fetched and fetch-count properties be updated. + mFile->OnFetched(); + } +} + +CacheEntryHandle* CacheEntry::NewHandle() { return new CacheEntryHandle(this); } + +CacheEntryHandle* CacheEntry::NewWriteHandle() { + mozilla::MutexAutoLock lock(mLock); + + // Ignore the OPEN_SECRETLY flag on purpose here, which should actually be + // used only along with OPEN_READONLY, but there is no need to enforce that. + BackgroundOp(Ops::FRECENCYUPDATE); + + return (mWriter = NewHandle()); +} + +void CacheEntry::OnHandleClosed(CacheEntryHandle const* aHandle) { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::OnHandleClosed [this=%p, state=%s, handle=%p]", this, + StateString(mState), aHandle)); + + if (mIsDoomed && NS_SUCCEEDED(mFileStatus) && + // Note: mHandlesCount is dropped before this method is called + (mHandlesCount == 0 || + (mHandlesCount == 1 && mWriter && mWriter != aHandle))) { + // This entry is no longer referenced from outside and is doomed. + // We can do this also when there is just reference from the writer, + // no one else could ever reach the written data. + // Tell the file to kill the handle, i.e. bypass any I/O operations + // on it except removing the file. + mFile->Kill(); + } + + if (mWriter != aHandle) { + LOG((" not the writer")); + return; + } + + if (mOutputStream) { + LOG((" abandoning phantom output stream")); + // No one took our internal output stream, so there are no data + // and output stream has to be open symultaneously with input stream + // on this entry again. + mHasData = false; + // This asynchronously ends up invoking callbacks on this entry + // through OnOutputClosed() call. + mOutputStream->Close(); + mOutputStream = nullptr; + } else { + // We must always redispatch, otherwise there is a risk of stack + // overflow. This code can recurse deeply. It won't execute sooner + // than we release mLock. + BackgroundOp(Ops::CALLBACKS, true); + } + + mWriter = nullptr; + + if (mState == WRITING) { + LOG((" reverting to state EMPTY - write failed")); + mState = EMPTY; + } else if (mState == REVALIDATING) { + LOG((" reverting to state READY - reval failed")); + mState = READY; + } + + if (mState == READY && !mHasData) { + // We may get to this state when following steps happen: + // 1. a new entry is given to a consumer + // 2. the consumer calls MetaDataReady(), we transit to READY + // 3. abandons the entry w/o opening the output stream, mHasData left false + // + // In this case any following consumer will get a ready entry (with + // metadata) but in state like the entry data write was still happening (was + // in progress) and will indefinitely wait for the entry data or even the + // entry itself when RECHECK_AFTER_WRITE is returned from onCacheEntryCheck. + LOG( + (" we are in READY state, pretend we have data regardless it" + " has actully been never touched")); + mHasData = true; + } +} + +void CacheEntry::OnOutputClosed() { + // Called when the file's output stream is closed. Invoke any callbacks + // waiting for complete entry. + + mozilla::MutexAutoLock lock(mLock); + InvokeCallbacks(); +} + +bool CacheEntry::IsReferenced() const { + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + + // Increasing this counter from 0 to non-null and this check both happen only + // under the service lock. + return mHandlesCount > 0; +} + +bool CacheEntry::IsFileDoomed() { + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->IsDoomed(); + } + + return false; +} + +uint32_t CacheEntry::GetMetadataMemoryConsumption() { + NS_ENSURE_SUCCESS(mFileStatus, 0); + + uint32_t size; + if (NS_FAILED(mFile->ElementsSize(&size))) return 0; + + return size; +} + +// nsICacheEntry + +nsresult CacheEntry::GetPersistent(bool* aPersistToDisk) { + // No need to sync when only reading. + // When consumer needs to be consistent with state of the memory storage + // entries table, then let it use GetUseDisk getter that must be called under + // the service lock. + *aPersistToDisk = mUseDisk; + return NS_OK; +} + +nsresult CacheEntry::GetKey(nsACString& aKey) { + aKey.Assign(mURI); + return NS_OK; +} + +nsresult CacheEntry::GetCacheEntryId(uint64_t* aCacheEntryId) { + *aCacheEntryId = mCacheEntryId; + return NS_OK; +} + +nsresult CacheEntry::GetFetchCount(uint32_t* aFetchCount) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetFetchCount(aFetchCount); +} + +nsresult CacheEntry::GetLastFetched(uint32_t* aLastFetched) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastFetched(aLastFetched); +} + +nsresult CacheEntry::GetLastModified(uint32_t* aLastModified) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastModified(aLastModified); +} + +nsresult CacheEntry::GetExpirationTime(uint32_t* aExpirationTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetExpirationTime(aExpirationTime); +} + +nsresult CacheEntry::GetOnStartTime(uint64_t* aTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + return mFile->GetOnStartTime(aTime); +} + +nsresult CacheEntry::GetOnStopTime(uint64_t* aTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + return mFile->GetOnStopTime(aTime); +} + +nsresult CacheEntry::SetNetworkTimes(uint64_t aOnStartTime, + uint64_t aOnStopTime) { + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->SetNetworkTimes(aOnStartTime, aOnStopTime); + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::SetContentType(uint8_t aContentType) { + NS_ENSURE_ARG_MAX(aContentType, nsICacheEntry::CONTENT_TYPE_LAST - 1); + + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->SetContentType(aContentType); + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::GetIsForcedValid(bool* aIsForcedValid) { + NS_ENSURE_ARG(aIsForcedValid); + +#ifdef DEBUG + { + mozilla::MutexAutoLock lock(mLock); + MOZ_ASSERT(mState > LOADING); + } +#endif + if (mPinned) { + *aIsForcedValid = true; + return NS_OK; + } + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + *aIsForcedValid = + CacheStorageService::Self()->IsForcedValidEntry(mStorageID, key); + LOG(("CacheEntry::GetIsForcedValid [this=%p, IsForcedValid=%d]", this, + *aIsForcedValid)); + + return NS_OK; +} + +nsresult CacheEntry::ForceValidFor(uint32_t aSecondsToTheFuture) { + LOG(("CacheEntry::ForceValidFor [this=%p, aSecondsToTheFuture=%d]", this, + aSecondsToTheFuture)); + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + CacheStorageService::Self()->ForceEntryValidFor(mStorageID, key, + aSecondsToTheFuture); + + return NS_OK; +} + +nsresult CacheEntry::MarkForcedValidUse() { + LOG(("CacheEntry::MarkForcedValidUse [this=%p, ]", this)); + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + CacheStorageService::Self()->MarkForcedValidEntryUse(mStorageID, key); + return NS_OK; +} + +nsresult CacheEntry::SetExpirationTime(uint32_t aExpirationTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsresult rv = mFile->SetExpirationTime(aExpirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + // Aligned assignment, thus atomic. + mSortingExpirationTime = aExpirationTime; + return NS_OK; +} + +nsresult CacheEntry::OpenInputStream(int64_t offset, nsIInputStream** _retval) { + LOG(("CacheEntry::OpenInputStream [this=%p]", this)); + return OpenInputStreamInternal(offset, nullptr, _retval); +} + +nsresult CacheEntry::OpenAlternativeInputStream(const nsACString& type, + nsIInputStream** _retval) { + LOG(("CacheEntry::OpenAlternativeInputStream [this=%p, type=%s]", this, + PromiseFlatCString(type).get())); + return OpenInputStreamInternal(0, PromiseFlatCString(type).get(), _retval); +} + +nsresult CacheEntry::OpenInputStreamInternal(int64_t offset, + const char* aAltDataType, + nsIInputStream** _retval) { + LOG(("CacheEntry::OpenInputStreamInternal [this=%p]", this)); + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsresult rv; + + RefPtr<CacheEntryHandle> selfHandle = NewHandle(); + + nsCOMPtr<nsIInputStream> stream; + if (aAltDataType) { + rv = mFile->OpenAlternativeInputStream(selfHandle, aAltDataType, + getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + // Failure of this method may be legal when the alternative data requested + // is not avaialble or of a different type. Console error logs are + // ensured by CacheFile::OpenAlternativeInputStream. + return rv; + } + } else { + rv = mFile->OpenInputStream(selfHandle, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(stream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData) { + // So far output stream on this new entry not opened, do it now. + LOG((" creating phantom output stream")); + rv = OpenOutputStreamInternal(0, getter_AddRefs(mOutputStream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + stream.forget(_retval); + return NS_OK; +} + +nsresult CacheEntry::OpenOutputStream(int64_t offset, int64_t predictedSize, + nsIOutputStream** _retval) { + LOG(("CacheEntry::OpenOutputStream [this=%p]", this)); + + nsresult rv; + + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState > EMPTY); + + if (mFile->EntryWouldExceedLimit(0, predictedSize, false)) { + LOG((" entry would exceed size limit")); + return NS_ERROR_FILE_TOO_BIG; + } + + if (mOutputStream && !mIsDoomed) { + LOG((" giving phantom output stream")); + mOutputStream.forget(_retval); + } else { + rv = OpenOutputStreamInternal(offset, _retval); + if (NS_FAILED(rv)) return rv; + } + + // Entry considered ready when writer opens output stream. + if (mState < READY) mState = READY; + + // Invoke any pending readers now. + InvokeCallbacks(); + + return NS_OK; +} + +nsresult CacheEntry::OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + LOG(("CacheEntry::OpenAlternativeOutputStream [this=%p, type=%s]", this, + PromiseFlatCString(type).get())); + + nsresult rv; + + if (type.IsEmpty()) { + // The empty string is reserved to mean no alt-data available. + return NS_ERROR_INVALID_ARG; + } + + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData || mState < READY || mOutputStream || mIsDoomed) { + LOG((" entry not in state to write alt-data")); + return NS_ERROR_NOT_AVAILABLE; + } + + if (mFile->EntryWouldExceedLimit(0, predictedSize, true)) { + LOG((" entry would exceed size limit")); + return NS_ERROR_FILE_TOO_BIG; + } + + nsCOMPtr<nsIAsyncOutputStream> stream; + rv = mFile->OpenAlternativeOutputStream( + nullptr, PromiseFlatCString(type).get(), getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + stream.swap(*_retval); + return NS_OK; +} + +nsresult CacheEntry::OpenOutputStreamInternal(int64_t offset, + nsIOutputStream** _retval) { + LOG(("CacheEntry::OpenOutputStreamInternal [this=%p]", this)); + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + mLock.AssertCurrentThreadOwns(); + + if (mIsDoomed) { + LOG((" doomed...")); + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT(mState > LOADING); + + nsresult rv; + + // No need to sync on mUseDisk here, we don't need to be consistent + // with content of the memory storage entries hash table. + if (!mUseDisk) { + rv = mFile->SetMemoryOnly(); + NS_ENSURE_SUCCESS(rv, rv); + } + + RefPtr<CacheOutputCloseListener> listener = + new CacheOutputCloseListener(this); + + nsCOMPtr<nsIOutputStream> stream; + rv = mFile->OpenOutputStream(listener, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(stream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + NS_ENSURE_SUCCESS(rv, rv); + + // Prevent opening output stream again. + mHasData = true; + + stream.swap(*_retval); + return NS_OK; +} + +nsresult CacheEntry::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + { + mozilla::MutexAutoLock lock(mLock); + if (mSecurityInfoLoaded) { + *aSecurityInfo = do_AddRef(mSecurityInfo).take(); + return NS_OK; + } + } + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsCString info; + nsresult rv = mFile->GetElement("security-info", getter_Copies(info)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsITransportSecurityInfo> securityInfo; + if (!info.IsVoid()) { + rv = mozilla::psm::TransportSecurityInfo::Read( + info, getter_AddRefs(securityInfo)); + NS_ENSURE_SUCCESS(rv, rv); + } + if (!securityInfo) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo.swap(securityInfo); + mSecurityInfoLoaded = true; + + *aSecurityInfo = do_AddRef(mSecurityInfo).take(); + } + + return NS_OK; +} + +nsresult CacheEntry::SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo) { + nsresult rv; + + NS_ENSURE_SUCCESS(mFileStatus, mFileStatus); + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo = aSecurityInfo; + mSecurityInfoLoaded = true; + } + + nsCString info; + if (aSecurityInfo) { + rv = aSecurityInfo->ToString(info); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mFile->SetElement("security-info", info.Length() ? info.get() : nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheEntry::GetStorageDataSize(uint32_t* aStorageDataSize) { + NS_ENSURE_ARG(aStorageDataSize); + + int64_t dataSize; + nsresult rv = GetDataSize(&dataSize); + if (NS_FAILED(rv)) return rv; + + *aStorageDataSize = (uint32_t)std::min(int64_t(uint32_t(-1)), dataSize); + + return NS_OK; +} + +nsresult CacheEntry::AsyncDoom(nsICacheEntryDoomCallback* aCallback) { + LOG(("CacheEntry::AsyncDoom [this=%p]", this)); + + { + mozilla::MutexAutoLock lock(mLock); + + if (mIsDoomed || mDoomCallback) { + return NS_ERROR_IN_PROGRESS; // to aggregate have DOOMING state + } + + RemoveForcedValidity(); + + mIsDoomed = true; + mDoomCallback = aCallback; + } + + // This immediately removes the entry from the master hashtable and also + // immediately dooms the file. This way we make sure that any consumer + // after this point asking for the same entry won't get + // a) this entry + // b) a new entry with the same file + PurgeAndDoom(); + + return NS_OK; +} + +nsresult CacheEntry::GetMetaDataElement(const char* aKey, char** aRetval) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetElement(aKey, aRetval); +} + +nsresult CacheEntry::SetMetaDataElement(const char* aKey, const char* aValue) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->SetElement(aKey, aValue); +} + +nsresult CacheEntry::VisitMetaData(nsICacheEntryMetaDataVisitor* aVisitor) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->VisitMetaData(aVisitor); +} + +nsresult CacheEntry::MetaDataReady() { + mozilla::MutexAutoLock lock(mLock); + + LOG(("CacheEntry::MetaDataReady [this=%p, state=%s]", this, + StateString(mState))); + + MOZ_ASSERT(mState > EMPTY); + + if (mState == WRITING) mState = READY; + + InvokeCallbacks(); + + return NS_OK; +} + +nsresult CacheEntry::SetValid() { + nsCOMPtr<nsIOutputStream> outputStream; + + { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::SetValid [this=%p, state=%s]", this, + StateString(mState))); + + MOZ_ASSERT(mState > EMPTY); + + mState = READY; + mHasData = true; + + InvokeCallbacks(); + + outputStream.swap(mOutputStream); + } + + if (outputStream) { + LOG((" abandoning phantom output stream")); + outputStream->Close(); + } + + return NS_OK; +} + +nsresult CacheEntry::Recreate(bool aMemoryOnly, nsICacheEntry** _retval) { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::Recreate [this=%p, state=%s]", this, StateString(mState))); + + RefPtr<CacheEntryHandle> handle = ReopenTruncated(aMemoryOnly, nullptr); + if (handle) { + handle.forget(_retval); + return NS_OK; + } + + BackgroundOp(Ops::CALLBACKS, true); + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::GetDataSize(int64_t* aDataSize) { + LOG(("CacheEntry::GetDataSize [this=%p]", this)); + *aDataSize = 0; + + { + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData) { + LOG((" write in progress (no data)")); + return NS_ERROR_IN_PROGRESS; + } + } + + NS_ENSURE_SUCCESS(mFileStatus, mFileStatus); + + // mayhemer: TODO Problem with compression? + if (!mFile->DataSize(aDataSize)) { + LOG((" write in progress (stream active)")); + return NS_ERROR_IN_PROGRESS; + } + + LOG((" size=%" PRId64, *aDataSize)); + return NS_OK; +} + +nsresult CacheEntry::GetAltDataSize(int64_t* aDataSize) { + LOG(("CacheEntry::GetAltDataSize [this=%p]", this)); + if (NS_FAILED(mFileStatus)) { + return mFileStatus; + } + return mFile->GetAltDataSize(aDataSize); +} + +nsresult CacheEntry::GetAltDataType(nsACString& aType) { + LOG(("CacheEntry::GetAltDataType [this=%p]", this)); + if (NS_FAILED(mFileStatus)) { + return mFileStatus; + } + return mFile->GetAltDataType(aType); +} + +nsresult CacheEntry::MarkValid() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::MaybeMarkValid() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::HasWriteAccess(bool aWriteAllowed, bool* aWriteAccess) { + *aWriteAccess = aWriteAllowed; + return NS_OK; +} + +nsresult CacheEntry::Close() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::GetDiskStorageSizeInKB(uint32_t* aDiskStorageSize) { + if (NS_FAILED(mFileStatus)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mFile->GetDiskStorageSizeInKB(aDiskStorageSize); +} + +nsresult CacheEntry::GetLoadContextInfo(nsILoadContextInfo** aInfo) { + nsCOMPtr<nsILoadContextInfo> info = CacheFileUtils::ParseKey(mStorageID); + if (!info) { + return NS_ERROR_FAILURE; + } + + info.forget(aInfo); + + return NS_OK; +} + +// nsIRunnable + +NS_IMETHODIMP CacheEntry::Run() { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + mozilla::MutexAutoLock lock(mLock); + + BackgroundOp(mBackgroundOperations.Grab()); + return NS_OK; +} + +// Management methods + +double CacheEntry::GetFrecency() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mFrecency; +} + +uint32_t CacheEntry::GetExpirationTime() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mSortingExpirationTime; +} + +bool CacheEntry::IsRegistered() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mRegistration == REGISTERED; +} + +bool CacheEntry::CanRegister() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mRegistration == NEVERREGISTERED; +} + +void CacheEntry::SetRegistered(bool aRegistered) { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + if (aRegistered) { + MOZ_ASSERT(mRegistration == NEVERREGISTERED); + mRegistration = REGISTERED; + } else { + MOZ_ASSERT(mRegistration == REGISTERED); + mRegistration = DEREGISTERED; + } +} + +bool CacheEntry::DeferOrBypassRemovalOnPinStatus(bool aPinned) { + LOG(("CacheEntry::DeferOrBypassRemovalOnPinStatus [this=%p]", this)); + + mozilla::MutexAutoLock lock(mLock); + if (mPinningKnown) { + LOG((" pinned=%d, caller=%d", (bool)mPinned, aPinned)); + // Bypass when the pin status of this entry doesn't match the pin status + // caller wants to remove + return mPinned != aPinned; + } + + LOG((" pinning unknown, caller=%d", aPinned)); + // Oterwise, remember to doom after the status is determined for any + // callback opening the entry after this point... + Callback c(this, aPinned); + RememberCallback(c); + // ...and always bypass + return true; +} + +bool CacheEntry::Purge(uint32_t aWhat) { + LOG(("CacheEntry::Purge [this=%p, what=%d]", this, aWhat)); + + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + switch (aWhat) { + case PURGE_DATA_ONLY_DISK_BACKED: + case PURGE_WHOLE_ONLY_DISK_BACKED: + // This is an in-memory only entry, don't purge it + if (!mUseDisk) { + LOG((" not using disk")); + return false; + } + } + + { + mozilla::MutexAutoLock lock(mLock); + + if (mState == WRITING || mState == LOADING || mFrecency == 0) { + // In-progress (write or load) entries should (at least for consistency + // and from the logical point of view) stay in memory. Zero-frecency + // entries are those which have never been given to any consumer, those + // are actually very fresh and should not go just because frecency had not + // been set so far. + LOG((" state=%s, frecency=%1.10f", StateString(mState), mFrecency)); + return false; + } + } + + if (NS_SUCCEEDED(mFileStatus) && mFile->IsWriteInProgress()) { + // The file is used when there are open streams or chunks/metadata still + // waiting for write. In this case, this entry cannot be purged, + // otherwise reopenned entry would may not even find the data on disk - + // CacheFile is not shared and cannot be left orphan when its job is not + // done, hence keep the whole entry. + LOG((" file still under use")); + return false; + } + + switch (aWhat) { + case PURGE_WHOLE_ONLY_DISK_BACKED: + case PURGE_WHOLE: { + if (!CacheStorageService::Self()->RemoveEntry(this, true)) { + LOG((" not purging, still referenced")); + return false; + } + + CacheStorageService::Self()->UnregisterEntry(this); + + // Entry removed it self from control arrays, return true + return true; + } + + case PURGE_DATA_ONLY_DISK_BACKED: { + NS_ENSURE_SUCCESS(mFileStatus, false); + + mFile->ThrowMemoryCachedData(); + + // Entry has been left in control arrays, return false (not purged) + return false; + } + } + + LOG((" ?")); + return false; +} + +void CacheEntry::PurgeAndDoom() { + LOG(("CacheEntry::PurgeAndDoom [this=%p]", this)); + + CacheStorageService::Self()->RemoveEntry(this); + DoomAlreadyRemoved(); +} + +void CacheEntry::DoomAlreadyRemoved() { + LOG(("CacheEntry::DoomAlreadyRemoved [this=%p]", this)); + + mozilla::MutexAutoLock lock(mLock); + + RemoveForcedValidity(); + + mIsDoomed = true; + + // Pretend pinning is know. This entry is now doomed for good, so don't + // bother with defering doom because of unknown pinning state any more. + mPinningKnown = true; + + // This schedules dooming of the file, dooming is ensured to happen + // sooner than demand to open the same file made after this point + // so that we don't get this file for any newer opened entry(s). + DoomFile(); + + // Must force post here since may be indirectly called from + // InvokeCallbacks of this entry and we don't want reentrancy here. + BackgroundOp(Ops::CALLBACKS, true); + // Process immediately when on the management thread. + BackgroundOp(Ops::UNREGISTER); +} + +void CacheEntry::DoomFile() { + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + if (NS_SUCCEEDED(mFileStatus)) { + if (mHandlesCount == 0 || (mHandlesCount == 1 && mWriter)) { + // We kill the file also when there is just reference from the writer, + // no one else could ever reach the written data. Obvisouly also + // when there is no reference at all (should we ever end up here + // in that case.) + // Tell the file to kill the handle, i.e. bypass any I/O operations + // on it except removing the file. + mFile->Kill(); + } + + // Always calls the callback asynchronously. + rv = mFile->Doom(mDoomCallback ? this : nullptr); + if (NS_SUCCEEDED(rv)) { + LOG((" file doomed")); + return; + } + + if (NS_ERROR_FILE_NOT_FOUND == rv) { + // File is set to be just memory-only, notify the callbacks + // and pretend dooming has succeeded. From point of view of + // the entry it actually did - the data is gone and cannot be + // reused. + rv = NS_OK; + } + } + + // Always posts to the main thread. + OnFileDoomed(rv); +} + +void CacheEntry::RemoveForcedValidity() { + mLock.AssertCurrentThreadOwns(); + + nsresult rv; + + if (mIsDoomed) { + return; + } + + nsAutoCString entryKey; + rv = HashingKey(entryKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + CacheStorageService::Self()->RemoveEntryForceValid(mStorageID, entryKey); +} + +void CacheEntry::BackgroundOp(uint32_t aOperations, bool aForceAsync) { + mLock.AssertCurrentThreadOwns(); + + if (!CacheStorageService::IsOnManagementThread() || aForceAsync) { + if (mBackgroundOperations.Set(aOperations)) { + CacheStorageService::Self()->Dispatch(this); + } + + LOG(("CacheEntry::BackgroundOp this=%p dipatch of %x", this, aOperations)); + return; + } + + { + mozilla::MutexAutoUnlock unlock(mLock); + + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + if (aOperations & Ops::FRECENCYUPDATE) { + ++mUseCount; + +#ifndef M_LN2 +# define M_LN2 0.69314718055994530942 +#endif + + // Half-life is dynamic, in seconds. + static double half_life = CacheObserver::HalfLifeSeconds(); + // Must convert from seconds to milliseconds since PR_Now() gives usecs. + static double const decay = + (M_LN2 / half_life) / static_cast<double>(PR_USEC_PER_SEC); + + double now_decay = static_cast<double>(PR_Now()) * decay; + + if (mFrecency == 0) { + mFrecency = now_decay; + } else { + // TODO: when C++11 enabled, use std::log1p(n) which is equal to log(n + + // 1) but more precise. + mFrecency = log(exp(mFrecency - now_decay) + 1) + now_decay; + } + LOG(("CacheEntry FRECENCYUPDATE [this=%p, frecency=%1.10f]", this, + mFrecency)); + + // Because CacheFile::Set*() are not thread-safe to use (uses + // WeakReference that is not thread-safe) we must post to the main + // thread... + NS_DispatchToMainThread( + NewRunnableMethod<double>("net::CacheEntry::StoreFrecency", this, + &CacheEntry::StoreFrecency, mFrecency)); + } + + if (aOperations & Ops::REGISTER) { + LOG(("CacheEntry REGISTER [this=%p]", this)); + + CacheStorageService::Self()->RegisterEntry(this); + } + + if (aOperations & Ops::UNREGISTER) { + LOG(("CacheEntry UNREGISTER [this=%p]", this)); + + CacheStorageService::Self()->UnregisterEntry(this); + } + } // unlock + + if (aOperations & Ops::CALLBACKS) { + LOG(("CacheEntry CALLBACKS (invoke) [this=%p]", this)); + + InvokeCallbacks(); + } +} + +void CacheEntry::StoreFrecency(double aFrecency) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(mFileStatus)) { + mFile->SetFrecency(FRECENCY2INT(aFrecency)); + } +} + +// CacheOutputCloseListener + +CacheOutputCloseListener::CacheOutputCloseListener(CacheEntry* aEntry) + : Runnable("net::CacheOutputCloseListener"), mEntry(aEntry) {} + +void CacheOutputCloseListener::OnOutputClosed() { + // We need this class and to redispatch since this callback is invoked + // under the file's lock and to do the job we need to enter the entry's + // lock too. That would lead to potential deadlocks. + + if (NS_IsMainThread()) { + // If we're already on the main thread, dispatch to the main thread instead + // of the sts. Always dispatching to the sts can cause problems late in + // shutdown, when threadpools may no longer be available (bug 1806332). + // + // This may also avoid some unnecessary thread-hops when invoking callbacks, + // which can require that they be called on the main thread. + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(do_AddRef(this))); + return; + } + + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_DIAGNOSTIC_ASSERT(sts); + MOZ_ALWAYS_SUCCEEDS(sts->Dispatch(do_AddRef(this))); +} + +NS_IMETHODIMP CacheOutputCloseListener::Run() { + mEntry->OnOutputClosed(); + return NS_OK; +} + +// Memory reporting + +size_t CacheEntry::SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) { + size_t n = 0; + + MutexAutoLock lock(mLock); + n += mCallbacks.ShallowSizeOfExcludingThis(mallocSizeOf); + if (mFile) { + n += mFile->SizeOfIncludingThis(mallocSizeOf); + } + + n += mURI.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mEnhanceID.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mStorageID.SizeOfExcludingThisIfUnshared(mallocSizeOf); + + // mDoomCallback is an arbitrary class that is probably reported elsewhere. + // mOutputStream is reported in mFile. + // mWriter is one of many handles we create, but (intentionally) not keep + // any reference to, so those unfortunately cannot be reported. Handles are + // small, though. + // mSecurityInfo doesn't impl nsISizeOf. + + return n; +} + +size_t CacheEntry::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net |