gecko-dev/dom/quota/DirectoryLockImpl.cpp
Jan Varga 8f2571576a Bug 1749504 - Unregister invalidated directory locks when they become unblocked; r=dom-storage-reviewers,jstutte
Invalidated pending directory locks can be unregistered immediately when they
become unblocked. There's no need to wait for the last release because they
were never acquired.

Changes done in this patch:
- moved the unregistration into own Unregister method
- added the directory lock unregistration to NotifyOpenListener as well
- enabled the disabled test for ShutdownStorage

Differential Revision: https://phabricator.services.mozilla.com/D185692
2023-09-13 16:41:58 +00:00

413 lines
12 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "DirectoryLockImpl.h"
#include "mozilla/ReverseIterator.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/QuotaManager.h"
namespace mozilla::dom::quota {
DirectoryLockImpl::DirectoryLockImpl(
MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
const Nullable<PersistenceType>& aPersistenceType,
const nsACString& aSuffix, const nsACString& aGroup,
const OriginScope& aOriginScope, const nsACString& aStorageOrigin,
bool aIsPrivate, const Nullable<Client::Type>& aClientType,
const bool aExclusive, const bool aInternal,
const ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag)
: mQuotaManager(std::move(aQuotaManager)),
mPersistenceType(aPersistenceType),
mSuffix(aSuffix),
mGroup(aGroup),
mOriginScope(aOriginScope),
mStorageOrigin(aStorageOrigin),
mClientType(aClientType),
mId(mQuotaManager->GenerateDirectoryLockId()),
mIsPrivate(aIsPrivate),
mExclusive(aExclusive),
mInternal(aInternal),
mShouldUpdateLockIdTable(aShouldUpdateLockIdTableFlag ==
ShouldUpdateLockIdTableFlag::Yes),
mRegistered(false) {
AssertIsOnOwningThread();
MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty());
MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull());
MOZ_ASSERT_IF(!aInternal,
aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID);
MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty());
MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin());
MOZ_ASSERT_IF(!aInternal, !aStorageOrigin.IsEmpty());
MOZ_ASSERT_IF(!aInternal && !aIsPrivate,
aOriginScope.GetOrigin() == aStorageOrigin);
MOZ_ASSERT_IF(!aInternal && aIsPrivate,
aOriginScope.GetOrigin() != aStorageOrigin);
MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull());
MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax());
}
DirectoryLockImpl::~DirectoryLockImpl() {
AssertIsOnOwningThread();
MOZ_ASSERT_IF(!mRegistered, mBlocking.IsEmpty());
if (mRegistered) {
Unregister();
}
}
#ifdef DEBUG
void DirectoryLockImpl::AssertIsOnOwningThread() const {
mQuotaManager->AssertIsOnOwningThread();
}
#endif // DEBUG
bool DirectoryLockImpl::Overlaps(const DirectoryLockImpl& aLock) const {
AssertIsOnOwningThread();
// If the persistence types don't overlap, the op can proceed.
if (!aLock.mPersistenceType.IsNull() && !mPersistenceType.IsNull() &&
aLock.mPersistenceType.Value() != mPersistenceType.Value()) {
return false;
}
// If the origin scopes don't overlap, the op can proceed.
bool match = aLock.mOriginScope.Matches(mOriginScope);
if (!match) {
return false;
}
// If the client types don't overlap, the op can proceed.
if (!aLock.mClientType.IsNull() && !mClientType.IsNull() &&
aLock.mClientType.Value() != mClientType.Value()) {
return false;
}
// Otherwise, when all attributes overlap (persistence type, origin scope and
// client type) the op must wait.
return true;
}
bool DirectoryLockImpl::MustWaitFor(const DirectoryLockImpl& aLock) const {
AssertIsOnOwningThread();
// Waiting is never required if the ops in comparison represent shared locks.
if (!aLock.mExclusive && !mExclusive) {
return false;
}
// Wait if the ops overlap.
return Overlaps(aLock);
}
void DirectoryLockImpl::NotifyOpenListener() {
AssertIsOnOwningThread();
if (mInvalidated) {
if (mOpenListener) {
(*mOpenListener)->DirectoryLockFailed();
} else {
mAcquirePromiseHolder.Reject(NS_ERROR_FAILURE, __func__);
}
} else {
#ifdef DEBUG
mAcquired.Flip();
#endif
if (mOpenListener) {
(*mOpenListener)
->DirectoryLockAcquired(static_cast<UniversalDirectoryLock*>(this));
} else {
mAcquirePromiseHolder.Resolve(true, __func__);
}
}
if (mOpenListener) {
mOpenListener.destroy();
} else {
MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty());
}
mQuotaManager->RemovePendingDirectoryLock(*this);
mPending.Flip();
if (mInvalidated) {
Unregister();
}
}
void DirectoryLockImpl::Invalidate() {
AssertIsOnOwningThread();
mInvalidated.EnsureFlipped();
if (mInvalidateCallback) {
MOZ_ALWAYS_SUCCEEDS(GetCurrentSerialEventTarget()->Dispatch(
NS_NewRunnableFunction("DirectoryLockImpl::Invalidate",
[invalidateCallback = mInvalidateCallback]() {
invalidateCallback();
}),
NS_DISPATCH_NORMAL));
}
}
void DirectoryLockImpl::Unregister() {
AssertIsOnOwningThread();
MOZ_ASSERT(mRegistered);
// We must call UnregisterDirectoryLock before unblocking other locks because
// UnregisterDirectoryLock also updates the origin last access time and the
// access flag (if the last lock for given origin is unregistered). One of the
// blocked locks could be requested by the clear/reset operation which stores
// cached information about origins in storage.sqlite. So if the access flag
// is not updated before unblocking the lock for reset/clear, we might store
// invalid information which can lead to omitting origin initialization during
// next temporary storage initialization.
mQuotaManager->UnregisterDirectoryLock(*this);
MOZ_ASSERT(!mRegistered);
for (NotNull<RefPtr<DirectoryLockImpl>> blockingLock : mBlocking) {
blockingLock->MaybeUnblock(*this);
}
mBlocking.Clear();
}
void DirectoryLockImpl::Acquire(RefPtr<OpenDirectoryListener> aOpenListener) {
AssertIsOnOwningThread();
MOZ_ASSERT(aOpenListener);
mOpenListener.init(WrapNotNullUnchecked(std::move(aOpenListener)));
AcquireInternal();
}
RefPtr<BoolPromise> DirectoryLockImpl::Acquire() {
AssertIsOnOwningThread();
RefPtr<BoolPromise> result = mAcquirePromiseHolder.Ensure(__func__);
AcquireInternal();
return result;
}
void DirectoryLockImpl::AcquireInternal() {
AssertIsOnOwningThread();
mQuotaManager->AddPendingDirectoryLock(*this);
// See if this lock needs to wait.
bool blocked = false;
// XXX It is probably unnecessary to iterate this in reverse order.
for (DirectoryLockImpl* const existingLock :
Reversed(mQuotaManager->mDirectoryLocks)) {
if (MustWaitFor(*existingLock)) {
existingLock->AddBlockingLock(*this);
AddBlockedOnLock(*existingLock);
blocked = true;
}
}
mQuotaManager->RegisterDirectoryLock(*this);
// Otherwise, notify the open listener immediately.
if (!blocked) {
NotifyOpenListener();
return;
}
if (!mExclusive || !mInternal) {
return;
}
// All the locks that block this new exclusive internal lock need to be
// invalidated. We also need to notify clients to abort operations for them.
QuotaManager::DirectoryLockIdTableArray lockIds;
lockIds.SetLength(Client::TypeMax());
const auto& blockedOnLocks = GetBlockedOnLocks();
MOZ_ASSERT(!blockedOnLocks.IsEmpty());
for (DirectoryLockImpl* blockedOnLock : blockedOnLocks) {
if (!blockedOnLock->IsInternal()) {
blockedOnLock->Invalidate();
// Clients don't have to handle pending locks. Invalidation is sufficient
// in that case (once a lock is ready and the listener needs to be
// notified, we will call DirectoryLockFailed instead of
// DirectoryLockAcquired which should release any remaining references to
// the lock).
if (!blockedOnLock->IsPending()) {
lockIds[blockedOnLock->ClientType()].Put(blockedOnLock->Id());
}
}
}
mQuotaManager->AbortOperationsForLocks(lockIds);
}
void DirectoryLockImpl::AcquireImmediately() {
AssertIsOnOwningThread();
#ifdef DEBUG
for (const DirectoryLockImpl* const existingLock :
mQuotaManager->mDirectoryLocks) {
MOZ_ASSERT(!MustWaitFor(*existingLock));
}
#endif
mQuotaManager->RegisterDirectoryLock(*this);
}
#ifdef DEBUG
void DirectoryLockImpl::AssertIsAcquiredExclusively() {
AssertIsOnOwningThread();
MOZ_ASSERT(mBlockedOn.IsEmpty());
MOZ_ASSERT(mExclusive);
MOZ_ASSERT(mInternal);
MOZ_ASSERT(mRegistered);
MOZ_ASSERT(!mInvalidated);
MOZ_ASSERT(mAcquired);
bool found = false;
for (const DirectoryLockImpl* const existingLock :
mQuotaManager->mDirectoryLocks) {
if (existingLock == this) {
MOZ_ASSERT(!found);
found = true;
} else if (existingLock->mAcquired) {
MOZ_ASSERT(false);
}
}
MOZ_ASSERT(found);
}
#endif
void DirectoryLockImpl::OnInvalidate(std::function<void()>&& aCallback) {
mInvalidateCallback = std::move(aCallback);
}
RefPtr<ClientDirectoryLock> DirectoryLockImpl::SpecializeForClient(
PersistenceType aPersistenceType,
const quota::OriginMetadata& aOriginMetadata,
Client::Type aClientType) const {
AssertIsOnOwningThread();
MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID);
MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty());
MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
MOZ_ASSERT(aClientType < Client::TypeMax());
MOZ_ASSERT(!mOpenListener);
MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty());
MOZ_ASSERT(mBlockedOn.IsEmpty());
if (NS_WARN_IF(mExclusive)) {
return nullptr;
}
RefPtr<DirectoryLockImpl> lock = Create(
mQuotaManager, Nullable<PersistenceType>(aPersistenceType),
aOriginMetadata.mSuffix, aOriginMetadata.mGroup,
OriginScope::FromOrigin(aOriginMetadata.mOrigin),
aOriginMetadata.mStorageOrigin, aOriginMetadata.mIsPrivate,
Nullable<Client::Type>(aClientType),
/* aExclusive */ false, mInternal, ShouldUpdateLockIdTableFlag::Yes);
if (NS_WARN_IF(!Overlaps(*lock))) {
return nullptr;
}
#ifdef DEBUG
for (DirectoryLockImpl* const existingLock :
Reversed(mQuotaManager->mDirectoryLocks)) {
if (existingLock != this && !existingLock->MustWaitFor(*this)) {
MOZ_ASSERT(!existingLock->MustWaitFor(*lock));
}
}
#endif
for (const auto& blockedLock : mBlocking) {
if (blockedLock->MustWaitFor(*lock)) {
lock->AddBlockingLock(*blockedLock);
blockedLock->AddBlockedOnLock(*lock);
}
}
mQuotaManager->RegisterDirectoryLock(*lock);
if (mInvalidated) {
lock->Invalidate();
}
return lock;
}
void DirectoryLockImpl::Log() const {
AssertIsOnOwningThread();
if (!QM_LOG_TEST()) {
return;
}
QM_LOG(("DirectoryLockImpl [%p]", this));
nsCString persistenceType;
if (mPersistenceType.IsNull()) {
persistenceType.AssignLiteral("null");
} else {
persistenceType.Assign(PersistenceTypeToString(mPersistenceType.Value()));
}
QM_LOG((" mPersistenceType: %s", persistenceType.get()));
QM_LOG((" mGroup: %s", mGroup.get()));
nsCString originScope;
if (mOriginScope.IsOrigin()) {
originScope.AssignLiteral("origin:");
originScope.Append(mOriginScope.GetOrigin());
} else if (mOriginScope.IsPrefix()) {
originScope.AssignLiteral("prefix:");
originScope.Append(mOriginScope.GetOriginNoSuffix());
} else if (mOriginScope.IsPattern()) {
originScope.AssignLiteral("pattern:");
// Can't call GetJSONPattern since it only works on the main thread.
} else {
MOZ_ASSERT(mOriginScope.IsNull());
originScope.AssignLiteral("null");
}
QM_LOG((" mOriginScope: %s", originScope.get()));
const auto clientType = mClientType.IsNull()
? nsAutoCString{"null"_ns}
: Client::TypeToText(mClientType.Value());
QM_LOG((" mClientType: %s", clientType.get()));
nsCString blockedOnString;
for (auto blockedOn : mBlockedOn) {
blockedOnString.Append(
nsPrintfCString(" [%p]", static_cast<void*>(blockedOn)));
}
QM_LOG((" mBlockedOn:%s", blockedOnString.get()));
QM_LOG((" mExclusive: %d", mExclusive));
QM_LOG((" mInternal: %d", mInternal));
QM_LOG((" mInvalidated: %d", static_cast<bool>(mInvalidated)));
for (auto blockedOn : mBlockedOn) {
blockedOn->Log();
}
}
} // namespace mozilla::dom::quota