/* 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 #include #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 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(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 event = new DoomCallbackRunnable(this, aResult); NS_DispatchToMainThread(event); } return NS_OK; } already_AddRefed 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 handle; RefPtr 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(rv))); newEntry->AsyncOpen(aCallback, nsICacheStorage::OPEN_TRUNCATE); } else { LOG((" exchanged of entry %p failed, rv=0x%08" PRIx32, this, static_cast(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 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 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(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 handle = NewHandle(); nsresult rv = aCallback.mCallback->OnCacheEntryCheck(handle, &checkResult); LOG((" OnCacheEntryCheck: rv=0x%08" PRIx32 ", result=%" PRId32, static_cast(rv), static_cast(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 event = new AvailableCallbackRunnable(this, aCallback); rv = aCallback.mTarget->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); LOG((" redispatched, (rv = 0x%08" PRIx32 ")", static_cast(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 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 handle = NewWriteHandle(); rv = aCallback.mCallback->OnCacheEntryAvailable(handle, state == WRITING, NS_OK); if (NS_FAILED(rv)) { LOG((" writing/revalidating failed (0x%08" PRIx32 ")", static_cast(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 selfHandle = NewHandle(); nsCOMPtr 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 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 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 listener = new CacheOutputCloseListener(this); nsCOMPtr stream; rv = mFile->OpenOutputStream(listener, getter_AddRefs(stream)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 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 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 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 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(PR_USEC_PER_SEC); double now_decay = static_cast(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("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 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