/* -*- 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 "ActorsParent.h"
#include <inttypes.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <iterator>
#include <
new>
#include <numeric>
#include <tuple>
#include <type_traits>
#include <utility>
#include "ActorsParentCommon.h"
#include "CrashAnnotations.h"
#include "DatabaseFileInfo.h"
#include "DatabaseFileManager.h"
#include "DatabaseFileManagerImpl.h"
#include "DBSchema.h"
#include "ErrorList.h"
#include "IDBCursorType.h"
#include "IDBObjectStore.h"
#include "IDBTransaction.h"
#include "IndexedDBCommon.h"
#include "IndexedDatabaseInlines.h"
#include "IndexedDatabaseManager.h"
#include "IndexedDBCipherKeyManager.h"
#include "KeyPath.h"
#include "MainThreadUtils.h"
#include "ProfilerHelpers.h"
#include "ReportInternalError.h"
#include "SafeRefPtr.h"
#include "SchemaUpgrades.h"
#include "chrome/common/ipc_channel.h"
#include "ipc/IPCMessageUtils.h"
#include "js/RootingAPI.h"
#include "js/StructuredClone.h"
#include "js/Value.h"
#include "jsapi.h"
#include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h"
#include "mozIStorageFunction.h"
#include "mozIStorageProgressHandler.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozIStorageValueArray.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/Algorithm.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/ArrayIterator.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/Casting.h"
#include "mozilla/CondVar.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/Maybe.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/RefCountType.h"
#include "mozilla/RefCounted.h"
#include "mozilla/RemoteLazyInputStreamParent.h"
#include "mozilla/RemoteLazyInputStreamStorage.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/SnappyCompressOutputStream.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/BlobImpl.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/IDBCursorBinding.h"
#include "mozilla/dom/IDBFactory.h"
#include "mozilla/dom/IPCBlob.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/dom/IndexedDatabase.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/dom/PContentParent.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/indexedDB/IDBResult.h"
#include "mozilla/dom/indexedDB/Key.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBCursor.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabase.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactory.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBRequest.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/quota/Assertions.h"
#include "mozilla/dom/quota/CachingDatabaseConnection.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/ClientDirectoryLock.h"
#include "mozilla/dom/quota/ClientImpl.h"
#include "mozilla/dom/quota/DebugOnlyMacro.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/DirectoryLockInlines.h"
#include "mozilla/dom/quota/DecryptingInputStream_impl.h"
#include "mozilla/dom/quota/EncryptingOutputStream_impl.h"
#include "mozilla/dom/quota/ErrorHandling.h"
#include "mozilla/dom/quota/FileStreams.h"
#include "mozilla/dom/quota/OriginScope.h"
#include "mozilla/dom/quota/PersistenceScope.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/QuotaObject.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/ThreadUtils.h"
#include "mozilla/dom/quota/UniversalDirectoryLock.h"
#include "mozilla/dom/quota/UsageInfo.h"
#include "mozilla/fallible.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/InputStreamParams.h"
#include "mozilla/ipc/PBackgroundParent.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/mozalloc.h"
#include "mozilla/storage/Variant.h"
#include "NotifyUtils.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsClassHashtable.h"
#include "nsContentUtils.h"
#include "nsTHashMap.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsEscape.h"
#include "nsHashKeys.h"
#include "nsIAsyncInputStream.h"
#include "nsID.h"
#include "nsIDUtils.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileProtocolHandler.h"
#include "nsIFileStreams.h"
#include "nsIFileURL.h"
#include "nsIInputStream.h"
#include "nsIOutputStream.h"
#include "nsIProtocolHandler.h"
#include "nsIRunnable.h"
#include "nsISupports.h"
#include "nsISupportsPriority.h"
#include "nsISupportsUtils.h"
#include "nsIThread.h"
#include "nsIThreadInternal.h"
#include "nsITimer.h"
#include "nsIURIMutator.h"
#include "nsIVariant.h"
#include "nsLiteralString.h"
#include "nsNetCID.h"
#include "nsPrintfCString.h"
#include "nsProxyRelease.h"
#include "nsServiceManagerUtils.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashSet.h"
#include "nsTHashtable.h"
#include "nsTLiteralString.h"
#include "nsTStringRepr.h"
#include "nsThreadPool.h"
#include "nsThreadUtils.h"
#include "nscore.h"
#include "prinrval.h"
#include "prio.h"
#include "prsystem.h"
#include "prthread.h"
#include "prtime.h"
#include "prtypes.h"
#include "snappy/snappy.h"
struct JSContext;
class JSObject;
template <
class T>
class nsPtrHashKey;
#define IDB_DEBUG_LOG(_args) \
MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), LogLevel::Debug, _args)
#if defined(MOZ_WIDGET_ANDROID)
# define IDB_MOBILE
#endif
// Helper macros to reduce assertion verbosity
// AUUF == ASSERT_UNREACHABLE_UNLESS_FUZZING
#ifdef DEBUG
# ifdef FUZZING
# define NS_AUUF_OR_WARN(...) NS_WARNING(__VA_ARGS__)
# else
# define NS_AUUF_OR_WARN(...) MOZ_ASSERT(
false, __VA_ARGS__)
# endif
# define NS_AUUF_OR_WARN_IF(cond) \
[](
bool aCond) { \
if (MOZ_UNLIKELY(aCond)) { \
NS_AUUF_OR_WARN(
#cond); \
} \
return aCond; \
}((cond))
#else
# define NS_AUUF_OR_WARN(...) \
do { \
}
while (
false)
# define NS_AUUF_OR_WARN_IF(cond)
static_cast<
bool>(cond)
#endif
namespace mozilla {
namespace dom::indexedDB {
using namespace mozilla::dom::quota;
using namespace mozilla::ipc;
using mozilla::dom::quota::Client;
namespace {
class ConnectionPool;
class Database;
struct DatabaseActorInfo;
class DatabaseFile;
class DatabaseLoggingInfo;
class DatabaseMaintenance;
class Factory;
class Maintenance;
class OpenDatabaseOp;
class TransactionBase;
class TransactionDatabaseOperationBase;
class VersionChangeTransaction;
template <
bool StatementHasIndexKeyBindings>
struct ValuePopulateResponseHelper;
/*******************************************************************************
* Constants
******************************************************************************/
const int32_t kStorageProgressGranularity = 1000;
// Changing the value here will override the page size of new databases only.
// A journal mode change and VACUUM are needed to change existing databases, so
// the best way to do that is to use the schema version upgrade mechanism.
const uint32_t kSQLitePageSizeOverride =
#ifdef IDB_MOBILE
2048;
#else
4096;
#endif
static_assert(kSQLitePageSizeOverride ==
/* mozStorage default */ 0 ||
(kSQLitePageSizeOverride % 2 == 0 &&
kSQLitePageSizeOverride >= 512 &&
kSQLitePageSizeOverride <= 65536),
"Must be 0 (disabled) or a power of 2 between 512 and 65536!");
// Set to -1 to use SQLite's default, 0 to disable, or some positive number to
// enforce a custom limit.
const int32_t kMaxWALPages = 5000;
// 20MB on desktop, 10MB on mobile.
// Set to some multiple of the page size to grow the database in larger chunks.
const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;
static_assert(kSQLiteGrowthIncrement >= 0 &&
kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
"Must be 0 (disabled) or a positive multiple of the page size!");
// The maximum number of threads that can be used for database activity at a
// single time. Please keep in sync with the constants in
// test_connection_idle_maintenance*.js tests
const uint32_t kMaxConnectionThreadCount = 20;
static_assert(kMaxConnectionThreadCount,
"Must have at least one thread!");
// The maximum number of threads to keep when idle. Until we switch to the STS
// pool, we can reduce the number of idle threads kept around thanks to the
// grace timeout.
const uint32_t kMaxIdleConnectionThreadCount = 1;
static_assert(kMaxConnectionThreadCount >= kMaxIdleConnectionThreadCount,
"Idle thread limit must be less than total thread limit!");
// The length of time that wanted idle threads will stay alive before being shut
// down.
const uint32_t kConnectionThreadMaxIdleMS = 30 * 1000;
// 30 seconds
// The length of time that excess idle threads will stay alive before being shut
// down.
const uint32_t kConnectionThreadGraceIdleMS = 500;
// 0.5 seconds
// The length of time that database connections will be held open after all
// transactions have completed before doing idle maintenance. Please keep in
// sync with the timeouts in test_connection_idle_maintenance*.js tests
const uint32_t kConnectionIdleMaintenanceMS = 2 * 1000;
// 2 seconds
// The length of time that database connections will be held open after all
// transactions and maintenance have completed.
const uint32_t kConnectionIdleCloseMS = 10 * 1000;
// 10 seconds
#define SAVEPOINT_CLAUSE
"SAVEPOINT sp;"_ns
// For efficiency reasons, kEncryptedStreamBlockSize must be a multiple of large
// 4k disk sectors.
static_assert(kEncryptedStreamBlockSize % 4096 == 0);
// Similarly, the file copy buffer size must be a multiple of the encrypted
// block size.
static_assert(kFileCopyBufferSize % kEncryptedStreamBlockSize == 0);
constexpr
auto kFileManagerDirectoryNameSuffix = u
".files"_ns;
constexpr
auto kSQLiteSuffix = u
".sqlite"_ns;
constexpr
auto kSQLiteJournalSuffix = u
".sqlite-journal"_ns;
constexpr
auto kSQLiteSHMSuffix = u
".sqlite-shm"_ns;
constexpr
auto kSQLiteWALSuffix = u
".sqlite-wal"_ns;
// The following constants define all names of binding parameters in statements,
// where they are bound by name. This should include all parameter names which
// are bound by name. Binding may be done by index when the statement definition
// and binding are done in the same local scope, and no other reasons prevent
// using the indexes (e.g. multiple statement variants with differing number or
// order of parameters). Neither the styles of specifying parameter names
// (literally vs. via these constants) nor the binding styles (by index vs. by
// name) should not be mixed for the same statement. The decision must be made
// for each statement based on the proximity of statement and binding calls.
constexpr
auto kStmtParamNameCurrentKey =
"current_key"_ns;
constexpr
auto kStmtParamNameRangeBound =
"range_bound"_ns;
constexpr
auto kStmtParamNameObjectStorePosition =
"object_store_position"_ns;
constexpr
auto kStmtParamNameLowerKey =
"lower_key"_ns;
constexpr
auto kStmtParamNameUpperKey =
"upper_key"_ns;
constexpr
auto kStmtParamNameKey =
"key"_ns;
constexpr
auto kStmtParamNameObjectStoreId =
"object_store_id"_ns;
constexpr
auto kStmtParamNameIndexId =
"index_id"_ns;
// TODO: Maybe the uses of kStmtParamNameId should be replaced by more
// specific constants such as kStmtParamNameObjectStoreId.
constexpr
auto kStmtParamNameId =
"id"_ns;
constexpr
auto kStmtParamNameValue =
"value"_ns;
constexpr
auto kStmtParamNameObjectDataKey =
"object_data_key"_ns;
constexpr
auto kStmtParamNameIndexDataValues =
"index_data_values"_ns;
constexpr
auto kStmtParamNameData =
"data"_ns;
constexpr
auto kStmtParamNameFileIds =
"file_ids"_ns;
constexpr
auto kStmtParamNameValueLocale =
"value_locale"_ns;
constexpr
auto kStmtParamNameLimit =
"limit"_ns;
// The following constants define some names of columns in tables, which are
// referred to in remote locations, e.g. in calls to
// GetBindingClauseForKeyRange.
constexpr
auto kColumnNameKey =
"key"_ns;
constexpr
auto kColumnNameValue =
"value"_ns;
constexpr
auto kColumnNameAliasSortKey =
"sort_column"_ns;
// SQL fragments used at multiple locations.
constexpr
auto kOpenLimit =
" LIMIT "_ns;
// The deletion marker file is created before RemoveDatabaseFilesAndDirectory
// begins deleting a database. It is removed as the last step of deletion. If a
// deletion marker file is found when initializing the origin, the deletion
// routine is run again to ensure that the database and all of its related files
// are removed. The primary goal of this mechanism is to avoid situations where
// a database has been partially deleted, leading to inconsistent state for the
// origin.
constexpr
auto kIdbDeletionMarkerFilePrefix = u
"idb-deleting-"_ns;
const uint32_t kDeleteTimeoutMs = 1000;
#ifdef DEBUG
const int32_t kDEBUGThreadPriority = nsISupportsPriority::PRIORITY_NORMAL;
const uint32_t kDEBUGThreadSleepMS = 0;
// Set to a non-zero number to enable debugging of transaction event targets.
// It will cause sleeping after every transaction runnable!
//
// This can be useful for discovering race conditions related to switching to
// another thread. Such races are usually avoided by using MozPromise or
// RunAfterProcessingCurrentEvent. Chaos mode doesn't always help with
// uncovering these issues, and only a precisely targeted sleep call can
// simulate the problem.
const uint32_t kDEBUGTransactionThreadSleepMS = 0;
// Make sure that we notice if we ever accidentally check in a non-zero value.
# ifdef MOZILLA_OFFICIAL
static_assert(kDEBUGTransactionThreadSleepMS == 0);
# endif
#endif
/*******************************************************************************
* Metadata classes
******************************************************************************/
// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullIndexMetadata {
IndexMetadata mCommonMetadata = {0, nsString(), KeyPath(0), nsCString(),
false,
false,
false};
FlippedOnce<
false> mDeleted;
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata)
private:
~FullIndexMetadata() =
default;
};
using IndexTable = nsTHashMap<nsUint64HashKey, SafeRefPtr<FullIndexMetadata>>;
// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullObjectStoreMetadata {
ObjectStoreMetadata mCommonMetadata;
IndexTable mIndexes;
// The auto increment ids are touched on both the background thread and the
// transaction I/O thread, and they must be kept in sync, so we need a mutex
// to protect them.
struct AutoIncrementIds {
int64_t next;
int64_t committed;
};
DataMutex<AutoIncrementIds> mAutoIncrementIds;
FlippedOnce<
false> mDeleted;
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata);
bool HasLiveIndexes()
const;
FullObjectStoreMetadata(ObjectStoreMetadata aCommonMetadata,
const AutoIncrementIds& aAutoIncrementIds)
: mCommonMetadata{std::move(aCommonMetadata)},
mAutoIncrementIds{AutoIncrementIds{aAutoIncrementIds},
"FullObjectStoreMetadata"} {}
private:
~FullObjectStoreMetadata() =
default;
};
using ObjectStoreTable =
nsTHashMap<nsUint64HashKey, SafeRefPtr<FullObjectStoreMetadata>>;
static_assert(
std::is_same_v<IndexOrObjectStoreId,
std::remove_cv_t<std::remove_reference_t<
decltype(std::declval<
const ObjectStoreGetParams&>()
.objectStoreId())>>>);
static_assert(
std::is_same_v<
IndexOrObjectStoreId,
std::remove_cv_t<std::remove_reference_t<
decltype(std::declval<
const IndexGetParams&>().objectStoreId())>>>);
struct FullDatabaseMetadata final : AtomicSafeRefCounted<FullDatabaseMetadata> {
DatabaseMetadata mCommonMetadata;
nsCString mDatabaseId;
nsString mFilePath;
ObjectStoreTable mObjectStores;
IndexOrObjectStoreId mNextObjectStoreId = 0;
IndexOrObjectStoreId mNextIndexId = 0;
public:
explicit FullDatabaseMetadata(
const DatabaseMetadata& aCommonMetadata)
: mCommonMetadata(aCommonMetadata) {
AssertIsOnBackgroundThread();
}
[[nodiscard]] SafeRefPtr<FullDatabaseMetadata> Duplicate()
const;
MOZ_DECLARE_REFCOUNTED_TYPENAME(FullDatabaseMetadata)
};
template <
class Enumerable>
auto MatchMetadataNameOrId(
const Enumerable& aEnumerable,
IndexOrObjectStoreId aId,
Maybe<
const nsAString&> aName = Nothing()) {
AssertIsOnBackgroundThread();
MOZ_ASSERT(aId);
const auto it = std::find_if(
aEnumerable.cbegin(), aEnumerable.cend(),
[aId, aName](
const auto& entry) {
MOZ_ASSERT(entry.GetKey() != 0);
const auto& value = entry.GetData();
MOZ_ASSERT(value);
return !value->mDeleted &&
(aId == value->mCommonMetadata.id() ||
(aName && *aName == value->mCommonMetadata.name()));
});
return ToMaybeRef(it != aEnumerable.cend() ? it->GetData().unsafeGetRawPtr()
: nullptr);
}
/*******************************************************************************
* SQLite functions
******************************************************************************/
// WARNING: the hash function used for the database name must not change.
// That's why this function exists separately from mozilla::HashString(), even
// though it is (at the time of writing) equivalent. See bug 780408 and bug
// 940315 for details.
uint32_t HashName(
const nsAString& aName) {
struct Helper {
static uint32_t RotateBitsLeft32(uint32_t aValue, uint8_t aBits) {
MOZ_ASSERT(aBits < 32);
return (aValue << aBits) | (aValue >> (32 - aBits));
}
};
static const uint32_t kGoldenRatioU32 = 0x9e3779b9u;
return std::accumulate(aName.BeginReading(), aName.EndReading(), uint32_t(0),
[](uint32_t hash, char16_t ch) {
return kGoldenRatioU32 *
(Helper::RotateBitsLeft32(hash, 5) ^ ch);
});
}
nsresult ClampResultCode(nsresult aResultCode) {
if (NS_SUCCEEDED(aResultCode) ||
NS_ERROR_GET_MODULE(aResultCode) == NS_ERROR_MODULE_DOM_INDEXEDDB) {
return aResultCode;
}
switch (aResultCode) {
case NS_ERROR_FILE_NO_DEVICE_SPACE:
return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
case NS_ERROR_STORAGE_CONSTRAINT:
return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
default:
#ifdef DEBUG
nsPrintfCString message(
"Converting non-IndexedDB error code (0x%" PRIX32
") to "
"NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR",
static_cast<uint32_t>(aResultCode));
NS_WARNING(message.get());
#else
;
#endif
}
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
Result<nsCOMPtr<nsIFileURL>, nsresult> GetDatabaseFileURL(
nsIFile& aDatabaseFile,
const int64_t aDirectoryLockId,
const Maybe<CipherKey>& aMaybeKey) {
MOZ_ASSERT(aDirectoryLockId >= -1);
QM_TRY_INSPECT(
const auto& protocolHandler,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIProtocolHandler>,
MOZ_SELECT_OVERLOAD(do_GetService),
NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX
"file"));
QM_TRY_INSPECT(
const auto& fileHandler,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFileProtocolHandler>,
MOZ_SELECT_OVERLOAD(do_QueryInterface),
protocolHandler));
QM_TRY_INSPECT(
const auto& mutator, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<nsIURIMutator>, fileHandler,
NewFileURIMutator, &aDatabaseFile));
// aDirectoryLockId should only be -1 when we are called
// - from DatabaseFileManager::InitDirectory when the temporary storage
// hasn't been initialized yet. At that time, the in-memory objects (e.g.
// OriginInfo) are only being created so it doesn't make sense to tunnel
// quota information to QuotaVFS to get corresponding QuotaObject instances
// for SQLite files.
// - from DeleteDatabaseOp::LoadPreviousVersion, since this might require
// temporarily exceeding the quota limit before the database can be
// deleted.
const nsCString directoryLockIdClause =
"&directoryLockId="_ns + IntToCString(aDirectoryLockId);
const auto keyClause = [&aMaybeKey] {
nsAutoCString keyClause;
if (aMaybeKey) {
keyClause.AssignLiteral(
"&key=");
for (uint8_t byte : IndexedDBCipherStrategy::SerializeKey(*aMaybeKey)) {
keyClause.AppendPrintf(
"%02x", byte);
}
}
return keyClause;
}();
QM_TRY_UNWRAP(
auto result, ([&mutator, &directoryLockIdClause, &keyClause] {
nsCOMPtr<nsIFileURL> result;
nsresult rv = NS_MutateURI(mutator)
.SetQuery(
"cache=private"_ns +
directoryLockIdClause + keyClause)
.Finalize(result);
return NS_SUCCEEDED(rv)
? Result<nsCOMPtr<nsIFileURL>, nsresult>{result}
: Err(rv);
}()));
return result;
}
nsLiteralCString GetDefaultSynchronousMode() {
return IndexedDatabaseManager::FullSynchronous() ?
"FULL"_ns :
"NORMAL"_ns;
}
nsresult SetDefaultPragmas(mozIStorageConnection& aConnection) {
MOZ_ASSERT(!NS_IsMainThread());
static constexpr
auto kBuiltInPragmas =
// We use foreign keys in DEBUG builds only because there is a performance
// cost to using them.
"PRAGMA foreign_keys = "
#ifdef DEBUG
"ON"
#else
"OFF"
#endif
";"
// The "INSERT OR REPLACE" statement doesn't fire the update trigger,
// instead it fires only the insert trigger. This confuses the update
// refcount function. This behavior changes with enabled recursive
// triggers, so the statement fires the delete trigger first and then the
// insert trigger.
"PRAGMA recursive_triggers = ON;"
// We aggressively truncate the database file when idle so don't bother
// overwriting the WAL with 0 during active periods.
"PRAGMA secure_delete = OFF;"_ns;
QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(kBuiltInPragmas)));
QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsAutoCString{
"PRAGMA synchronous = "_ns + GetDefaultSynchronousMode() +
";"_ns})));
#ifndef IDB_MOBILE
if (kSQLiteGrowthIncrement) {
// This is just an optimization so ignore the failure if the disk is
// currently too full.
QM_TRY(QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT(
aConnection.SetGrowthIncrement(kSQLiteGrowthIncrement,
""_ns)),
// Predicate.
IsSpecificError<NS_ERROR_FILE_TOO_BIG>,
// Fallback.
ErrToDefaultOk<>));
}
#endif // IDB_MOBILE
return NS_OK;
}
nsresult SetJournalMode(mozIStorageConnection& aConnection) {
MOZ_ASSERT(!NS_IsMainThread());
// Try enabling WAL mode. This can fail in various circumstances so we have to
// check the results here.
constexpr
auto journalModeQueryStart =
"PRAGMA journal_mode = "_ns;
constexpr
auto journalModeWAL =
"wal"_ns;
QM_TRY_INSPECT(
const auto& stmt,
CreateAndExecuteSingleStepStatement(
aConnection, journalModeQueryStart + journalModeWAL));
QM_TRY_INSPECT(
const auto& journalMode,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt, GetUTF8String, 0));
if (journalMode.Equals(journalModeWAL)) {
// WAL mode successfully enabled. Maybe set limits on its size here.
if (kMaxWALPages >= 0) {
QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
"PRAGMA wal_autocheckpoint = "_ns + IntToCString(kMaxWALPages))));
}
}
else {
NS_WARNING(
"Failed to set WAL mode, falling back to normal journal mode.");
#ifdef IDB_MOBILE
QM_TRY(MOZ_TO_RESULT(
aConnection.ExecuteSimpleSQL(journalModeQueryStart +
"truncate"_ns)));
#endif
}
return NS_OK;
}
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> OpenDatabase(
mozIStorageService& aStorageService, nsIFileURL& aFileURL,
const uint32_t aTelemetryId = 0) {
const nsAutoCString telemetryFilename =
aTelemetryId ?
"indexedDB-"_ns + IntToCString(aTelemetryId) +
NS_ConvertUTF16toUTF8(kSQLiteSuffix)
: nsAutoCString();
QM_TRY_UNWRAP(
auto connection,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, aStorageService,
OpenDatabaseWithFileURL, &aFileURL, telemetryFilename,
mozIStorageService::CONNECTION_INTERRUPTIBLE));
return WrapMovingNotNull(std::move(connection));
}
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
OpenDatabaseAndHandleBusy(mozIStorageService& aStorageService,
nsIFileURL& aFileURL,
const uint32_t aTelemetryId = 0) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!IsOnBackgroundThread());
using ConnectionType = Maybe<MovingNotNull<nsCOMPtr<mozIStorageConnection>>>;
QM_TRY_UNWRAP(
auto connection,
QM_OR_ELSE_WARN_IF(
// Expression
OpenDatabase(aStorageService, aFileURL, aTelemetryId)
.map([](
auto connection) -> ConnectionType {
return Some(std::move(connection));
}),
// Predicate.
IsSpecificError<NS_ERROR_STORAGE_BUSY>,
// Fallback.
ErrToDefaultOk<ConnectionType>));
if (connection.isNothing()) {
#ifdef DEBUG
{
nsCString path;
MOZ_ALWAYS_SUCCEEDS(aFileURL.GetFileName(path));
nsPrintfCString message(
"Received NS_ERROR_STORAGE_BUSY when attempting to open database "
"'%s', retrying for up to 10 seconds",
path.get());
NS_WARNING(message.get());
}
#endif
// Another thread must be checkpointing the WAL. Wait up to 10 seconds for
// that to complete.
const TimeStamp start = TimeStamp::NowLoRes();
do {
PR_Sleep(PR_MillisecondsToInterval(100));
QM_TRY_UNWRAP(connection,
QM_OR_ELSE_WARN_IF(
// Expression.
OpenDatabase(aStorageService, aFileURL, aTelemetryId)
.map([](
auto connection) -> ConnectionType {
return Some(std::move(connection));
}),
// Predicate.
([&start](nsresult aValue) {
return aValue == NS_ERROR_STORAGE_BUSY &&
TimeStamp::NowLoRes() - start <=
TimeDuration::FromSeconds(10);
}),
// Fallback.
ErrToDefaultOk<ConnectionType>));
}
while (connection.isNothing());
}
return connection.extract();
}
// Returns true if a given nsIFile exists and is a directory. Returns false if
// it doesn't exist. Returns an error if it exists, but is not a directory, or
// any other error occurs.
Result<
bool, nsresult> ExistsAsDirectory(nsIFile& aDirectory) {
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists));
if (exists) {
QM_TRY_INSPECT(
const bool& isDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory));
QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE));
}
return exists;
}
constexpr nsresult mapNoDeviceSpaceError(nsresult aRv) {
if (aRv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
// mozstorage translates SQLITE_FULL to
// NS_ERROR_FILE_NO_DEVICE_SPACE, which we know better as
// NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR.
return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
}
return aRv;
}
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
CreateStorageConnection(nsIFile& aDBFile, nsIFile& aFMDirectory,
const nsAString& aName,
const nsACString& aOrigin,
const int64_t aDirectoryLockId,
const uint32_t aTelemetryId,
const Maybe<CipherKey>& aMaybeKey) {
AssertIsOnIOThread();
MOZ_ASSERT(aDirectoryLockId >= -1);
AUTO_PROFILER_LABEL(
"CreateStorageConnection", DOM);
QM_TRY_INSPECT(
const auto& dbFileUrl,
GetDatabaseFileURL(aDBFile, aDirectoryLockId, aMaybeKey));
QM_TRY_INSPECT(
const auto& storageService,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
auto connection,
QM_OR_ELSE_WARN_IF(
// Expression.
OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId)
.map([](
auto connection) -> nsCOMPtr<mozIStorageConnection> {
return std::move(connection).unwrapBasePtr();
}),
// Predicate.
([&aName](nsresult aValue) {
// If we're just opening the database during origin initialization,
// then we don't want to erase any files. The failure here will fail
// origin initialization too.
return IsDatabaseCorruptionError(aValue) && !aName.IsVoid();
}),
// Fallback.
ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
if (!connection) {
// XXX Shouldn't we also update quota usage?
// Nuke the database file.
QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(
false)));
QM_TRY_INSPECT(
const bool& existsAsDirectory,
ExistsAsDirectory(aFMDirectory));
if (existsAsDirectory) {
QM_TRY(MOZ_TO_RESULT(aFMDirectory.Remove(
true)));
}
QM_TRY_UNWRAP(connection, OpenDatabaseAndHandleBusy(
*storageService, *dbFileUrl, aTelemetryId));
}
QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection)));
QM_TRY(MOZ_TO_RESULT(connection->EnableModule(
"filesystem"_ns)));
// Check to make sure that the database schema is correct.
QM_TRY_INSPECT(
const int32_t& schemaVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion));
// Unknown schema will fail origin initialization too.
QM_TRY(OkIf(schemaVersion || !aName.IsVoid()),
Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](
const auto&) {
IDB_WARNING(
"Unable to open IndexedDB database, schema is not set!");
});
QM_TRY(
OkIf(schemaVersion <= kSQLiteSchemaVersion),
Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](
const auto&) {
IDB_WARNING(
"Unable to open IndexedDB database, schema is too high!");
});
bool journalModeSet =
false;
if (schemaVersion != kSQLiteSchemaVersion) {
const bool newDatabase = !schemaVersion;
if (newDatabase) {
// Set the page size first.
const auto sqlitePageSizeOverride =
aMaybeKey ? 8192 : kSQLitePageSizeOverride;
if (sqlitePageSizeOverride) {
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString(
"PRAGMA page_size = %" PRIu32
";", sqlitePageSizeOverride))));
}
// We have to set the auto_vacuum mode before opening a transaction.
QM_TRY((MOZ_TO_RESULT_INVOKE_MEMBER(
connection, ExecuteSimpleSQL,
#ifdef IDB_MOBILE
// Turn on full auto_vacuum mode to reclaim disk space on
// mobile devices (at the cost of some COMMIT speed).
"PRAGMA auto_vacuum = FULL;"_ns
#else
// Turn on incremental auto_vacuum mode on desktop builds.
"PRAGMA auto_vacuum = INCREMENTAL;"_ns
#endif
)
.mapErr(mapNoDeviceSpaceError)));
QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection)));
journalModeSet =
true;
}
else {
#ifdef DEBUG
// Disable foreign key support while upgrading. This has to be done before
// starting a transaction.
MOZ_ALWAYS_SUCCEEDS(
connection->ExecuteSimpleSQL(
"PRAGMA foreign_keys = OFF;"_ns));
#endif
}
bool vacuumNeeded =
false;
mozStorageTransaction transaction(
connection.get(),
false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(transaction.Start()));
if (newDatabase) {
QM_TRY(MOZ_TO_RESULT(CreateTables(*connection)));
#ifdef DEBUG
{
QM_TRY_INSPECT(
const int32_t& schemaVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion),
QM_ASSERT_UNREACHABLE);
MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
}
#endif
// The parameter names are not used, parameters are bound by index only
// locally in the same function.
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
"INSERT INTO database (name, origin) "
"VALUES (:name, :origin)"_ns));
QM_TRY(MOZ_TO_RESULT(stmt->BindStringByIndex(0, aName)));
QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByIndex(1, aOrigin)));
QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
}
else {
QM_TRY_UNWRAP(vacuumNeeded, MaybeUpgradeSchema(*connection, schemaVersion,
aFMDirectory, aOrigin));
}
QM_TRY(MOZ_TO_RESULT_INVOKE_MEMBER(transaction, Commit)
.mapErr(mapNoDeviceSpaceError));
#ifdef DEBUG
if (!newDatabase) {
// Re-enable foreign key support after doing a foreign key check.
QM_TRY_INSPECT(
const bool& foreignKeyError,
CreateAndExecuteSingleStepStatement<
SingleStepResult::ReturnNullIfNoResult>(
*connection,
"PRAGMA foreign_key_check;"_ns),
QM_ASSERT_UNREACHABLE);
MOZ_ASSERT(!foreignKeyError,
"Database has inconsisistent foreign keys!");
MOZ_ALWAYS_SUCCEEDS(
connection->ExecuteSimpleSQL(
"PRAGMA foreign_keys = OFF;"_ns));
}
#endif
if (kSQLitePageSizeOverride && !newDatabase) {
QM_TRY_INSPECT(
const auto& stmt,
CreateAndExecuteSingleStepStatement(
*connection,
"PRAGMA page_size;"_ns));
QM_TRY_INSPECT(
const int32_t& pageSize,
MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0));
MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);
if (kSQLitePageSizeOverride != uint32_t(pageSize)) {
// We must not be in WAL journal mode to change the page size.
QM_TRY(MOZ_TO_RESULT(
connection->ExecuteSimpleSQL(
"PRAGMA journal_mode = DELETE;"_ns)));
QM_TRY_INSPECT(
const auto& stmt,
CreateAndExecuteSingleStepStatement(
*connection,
"PRAGMA journal_mode;"_ns));
QM_TRY_INSPECT(
const auto& journalMode,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt,
GetUTF8String, 0));
if (journalMode.EqualsLiteral(
"delete")) {
// Successfully set to rollback journal mode so changing the page size
// is possible with a VACUUM.
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString(
"PRAGMA page_size = %" PRIu32
";", kSQLitePageSizeOverride))));
// We will need to VACUUM in order to change the page size.
vacuumNeeded =
true;
}
else {
NS_WARNING(
"Failed to set journal_mode for database, unable to "
"change the page size!");
}
}
}
if (vacuumNeeded) {
QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(
"VACUUM;"_ns)));
}
if (newDatabase || vacuumNeeded) {
if (journalModeSet) {
// Make sure we checkpoint to get an accurate file size.
QM_TRY(MOZ_TO_RESULT(
connection->ExecuteSimpleSQL(
"PRAGMA wal_checkpoint(FULL);"_ns)));
}
QM_TRY_INSPECT(
const int64_t& fileSize,
MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize));
MOZ_ASSERT(fileSize > 0);
PRTime vacuumTime = PR_Now();
MOZ_ASSERT(vacuumTime);
// The parameter names are not used, parameters are bound by index only
// locally in the same function.
QM_TRY_INSPECT(
const auto& vacuumTimeStmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<mozIStorageStatement>,
connection, CreateStatement,
"UPDATE database "
"SET last_vacuum_time = :time"
", last_vacuum_size = :size;"_ns));
QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(0, vacuumTime)));
QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(1, fileSize)));
QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute()));
}
}
if (!journalModeSet) {
QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection)));
}
return WrapMovingNotNullUnchecked(std::move(connection));
}
nsCOMPtr<nsIFile> GetFileForPath(
const nsAString& aPath) {
MOZ_ASSERT(!aPath.IsEmpty());
QM_TRY_RETURN(QM_NewLocalFile(aPath), nullptr);
}
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(nsIFile& aDatabaseFile,
const int64_t aDirectoryLockId,
const uint32_t aTelemetryId,
const Maybe<CipherKey>& aMaybeKey) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!IsOnBackgroundThread());
MOZ_ASSERT(aDirectoryLockId >= 0);
AUTO_PROFILER_LABEL(
"GetStorageConnection", DOM);
QM_TRY_INSPECT(
const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, Exists));
QM_TRY(OkIf(exists), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
IDB_REPORT_INTERNAL_ERR_LAMBDA);
QM_TRY_INSPECT(
const auto& dbFileUrl,
GetDatabaseFileURL(aDatabaseFile, aDirectoryLockId, aMaybeKey));
QM_TRY_INSPECT(
const auto& storageService,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
MOZ_SELECT_OVERLOAD(do_GetService),
MOZ_STORAGE_SERVICE_CONTRACTID));
QM_TRY_UNWRAP(
nsCOMPtr<mozIStorageConnection> connection,
OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId));
QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection)));
QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection)));
return WrapMovingNotNullUnchecked(std::move(connection));
}
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(
const nsAString& aDatabaseFilePath,
const int64_t aDirectoryLockId,
const uint32_t aTelemetryId,
const Maybe<CipherKey>& aMaybeKey) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!IsOnBackgroundThread());
MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, kSQLiteSuffix));
MOZ_ASSERT(aDirectoryLockId >= 0);
nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath);
QM_TRY(OkIf(dbFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
IDB_REPORT_INTERNAL_ERR_LAMBDA);
return GetStorageConnection(*dbFile, aDirectoryLockId, aTelemetryId,
aMaybeKey);
}
/*******************************************************************************
* ConnectionPool declarations
******************************************************************************/
class DatabaseConnection final :
public CachingDatabaseConnection {
friend class ConnectionPool;
enum class CheckpointMode { Full, Restart, Truncate };
public:
class AutoSavepoint;
class UpdateRefcountFunction;
private:
InitializedOnce<
const NotNull<SafeRefPtr<DatabaseFileManager>>> mFileManager;
RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction;
RefPtr<QuotaObject> mQuotaObject;
RefPtr<QuotaObject> mJournalQuotaObject;
IDBTransaction::Durability mLastDurability;
bool mInReadTransaction;
bool mInWriteTransaction;
#ifdef DEBUG
uint32_t mDEBUGSavepointCount;
#endif
public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection)
UpdateRefcountFunction* GetUpdateRefcountFunction()
const {
AssertIsOnConnectionThread();
return mUpdateRefcountFunction;
}
nsresult BeginWriteTransaction(
const IDBTransaction::Durability aDurability);
nsresult CommitWriteTransaction();
void RollbackWriteTransaction();
void FinishWriteTransaction();
nsresult StartSavepoint();
nsresult ReleaseSavepoint();
nsresult RollbackSavepoint();
nsresult Checkpoint() {
AssertIsOnConnectionThread();
return CheckpointInternal(CheckpointMode::Full);
}
void DoIdleProcessing(
bool aNeedsCheckpoint,
const Atomic<
bool>& aInterrupted);
void Close();
nsresult DisableQuotaChecks();
void EnableQuotaChecks();
private:
DatabaseConnection(
MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection,
MovingNotNull<SafeRefPtr<DatabaseFileManager>> aFileManager);
~DatabaseConnection();
nsresult Init();
nsresult CheckpointInternal(CheckpointMode aMode);
Result<uint32_t, nsresult> GetFreelistCount(
CachedStatement& aCachedStatement);
/**
* On success, returns whether some pages were freed.
*/
Result<
bool, nsresult> ReclaimFreePagesWhileIdle(
CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement,
uint32_t aFreelistCount,
bool aNeedsCheckpoint,
const Atomic<
bool>& aInterrupted);
Result<int64_t, nsresult> GetFileSize(
const nsAString& aPath);
};
class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final {
DatabaseConnection* mConnection;
#ifdef DEBUG
const TransactionBase* mDEBUGTransaction;
#endif
public:
AutoSavepoint();
~AutoSavepoint();
nsresult Start(
const TransactionBase& aTransaction);
nsresult Commit();
};
class DatabaseConnection::UpdateRefcountFunction final
:
public mozIStorageFunction {
class FileInfoEntry;
enum class UpdateType { Increment, Decrement };
DatabaseConnection*
const mConnection;
DatabaseFileManager& mFileManager;
nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries;
nsTHashMap<nsUint64HashKey, NotNull<FileInfoEntry*>> mSavepointEntriesIndex;
nsTArray<int64_t> mJournalsToCreateBeforeCommit;
nsTArray<int64_t> mJournalsToRemoveAfterCommit;
nsTArray<int64_t> mJournalsToRemoveAfterAbort;
bool mInSavepoint;
public:
NS_DECL_ISUPPORTS_ONEVENTTARGET
NS_DECL_MOZISTORAGEFUNCTION
UpdateRefcountFunction(DatabaseConnection* aConnection,
DatabaseFileManager& aFileManager);
nsresult WillCommit();
void DidCommit();
void DidAbort();
void StartSavepoint();
void ReleaseSavepoint();
void RollbackSavepoint();
void Reset();
private:
~UpdateRefcountFunction() =
default;
nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex,
UpdateType aUpdateType);
nsresult CreateJournals();
nsresult RemoveJournals(
const nsTArray<int64_t>& aJournals);
};
class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final {
SafeRefPtr<DatabaseFileInfo> mFileInfo;
int32_t mDelta;
int32_t mSavepointDelta;
public:
explicit FileInfoEntry(SafeRefPtr<DatabaseFileInfo> aFileInfo)
: mFileInfo(std::move(aFileInfo)), mDelta(0), mSavepointDelta(0) {
MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
}
void IncDeltas(
bool aUpdateSavepointDelta) {
++mDelta;
if (aUpdateSavepointDelta) {
++mSavepointDelta;
}
}
void DecDeltas(
bool aUpdateSavepointDelta) {
--mDelta;
if (aUpdateSavepointDelta) {
--mSavepointDelta;
}
}
void DecBySavepointDelta() { mDelta -= mSavepointDelta; }
SafeRefPtr<DatabaseFileInfo> ReleaseFileInfo() {
return std::move(mFileInfo);
}
void MaybeUpdateDBRefs() {
if (mDelta) {
mFileInfo->UpdateDBRefs(mDelta);
}
}
int32_t Delta()
const {
return mDelta; }
int32_t SavepointDelta()
const {
return mSavepointDelta; }
~FileInfoEntry() {
MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
}
};
class ConnectionPool final {
public:
class FinishCallback;
private:
class ConnectionRunnable;
class CloseConnectionRunnable;
struct DatabaseInfo;
struct DatabaseCompleteCallback;
class FinishCallbackWrapper;
class IdleConnectionRunnable;
#ifdef DEBUG
class TransactionRunnable;
#endif
class TransactionInfo;
struct TransactionInfoPair;
struct IdleResource {
TimeStamp mIdleTime;
IdleResource(
const IdleResource& aOther) =
delete;
IdleResource(IdleResource&& aOther) noexcept
: IdleResource(aOther.mIdleTime) {}
IdleResource&
operator=(
const IdleResource& aOther) =
delete;
IdleResource&
operator=(IdleResource&& aOther) =
delete;
protected:
explicit IdleResource(
const TimeStamp& aIdleTime);
~IdleResource();
};
struct IdleDatabaseInfo final :
public IdleResource {
InitializedOnce<
const NotNull<DatabaseInfo*>> mDatabaseInfo;
public:
explicit IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo);
IdleDatabaseInfo(
const IdleDatabaseInfo& aOther) =
delete;
IdleDatabaseInfo(IdleDatabaseInfo&& aOther) noexcept
: IdleResource(std::move(aOther)),
mDatabaseInfo{std::move(aOther.mDatabaseInfo)} {
MOZ_ASSERT(mDatabaseInfo);
MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo);
}
IdleDatabaseInfo&
operator=(
const IdleDatabaseInfo& aOther) =
delete;
IdleDatabaseInfo&
operator=(IdleDatabaseInfo&& aOther) =
delete;
~IdleDatabaseInfo();
bool operator==(
const IdleDatabaseInfo& aOther)
const {
return *mDatabaseInfo == *aOther.mDatabaseInfo;
}
bool operator==(
const DatabaseInfo* aDatabaseInfo)
const {
return *mDatabaseInfo == aDatabaseInfo;
}
bool operator<(
const IdleDatabaseInfo& aOther)
const {
return mIdleTime < aOther.mIdleTime;
}
};
struct PerformingIdleMaintenanceDatabaseInfo {
const NotNull<DatabaseInfo*> mDatabaseInfo;
RefPtr<IdleConnectionRunnable> mIdleConnectionRunnable;
PerformingIdleMaintenanceDatabaseInfo(
DatabaseInfo& aDatabaseInfo,
RefPtr<IdleConnectionRunnable> aIdleConnectionRunnable);
PerformingIdleMaintenanceDatabaseInfo(
const PerformingIdleMaintenanceDatabaseInfo& aOther) =
delete;
PerformingIdleMaintenanceDatabaseInfo(
PerformingIdleMaintenanceDatabaseInfo&& aOther) noexcept
: mDatabaseInfo{aOther.mDatabaseInfo},
mIdleConnectionRunnable{std::move(aOther.mIdleConnectionRunnable)} {
MOZ_COUNT_CTOR(ConnectionPool::PerformingIdleMaintenanceDatabaseInfo);
}
PerformingIdleMaintenanceDatabaseInfo&
operator=(
const PerformingIdleMaintenanceDatabaseInfo& aOther) =
delete;
PerformingIdleMaintenanceDatabaseInfo&
operator=(
PerformingIdleMaintenanceDatabaseInfo&& aOther) =
delete;
~PerformingIdleMaintenanceDatabaseInfo();
bool operator==(
const DatabaseInfo* aDatabaseInfo)
const {
return mDatabaseInfo == aDatabaseInfo;
}
};
// This mutex guards mDatabases, see below.
Mutex mDatabasesMutex MOZ_UNANNOTATED;
nsCOMPtr<nsIThreadPool> mIOTarget;
nsTArray<IdleDatabaseInfo> mIdleDatabases;
nsTArray<PerformingIdleMaintenanceDatabaseInfo>
mDatabasesPerformingIdleMaintenance;
nsCOMPtr<nsITimer> mIdleTimer;
TimeStamp mTargetIdleTime;
// Only modifed on the owning thread, but read on multiple threads. Therefore
// all modifications and all reads off the owning thread must be protected by
// mDatabasesMutex.
nsClassHashtable<nsCStringHashKey, DatabaseInfo> mDatabases;
nsClassHashtable<nsUint64HashKey, TransactionInfo> mTransactions;
nsTArray<NotNull<TransactionInfo*>> mQueuedTransactions;
nsTArray<UniquePtr<DatabaseCompleteCallback>> mCompleteCallbacks;
uint64_t mNextTransactionId;
FlippedOnce<
false> mShutdownRequested;
FlippedOnce<
false> mShutdownComplete;
public:
ConnectionPool();
void AssertIsOnOwningThread()
const {
NS_ASSERT_OWNINGTHREAD(ConnectionPool);
}
Result<RefPtr<DatabaseConnection>, nsresult> GetOrCreateConnection(
const Database& aDatabase);
uint64_t Start(
const nsID& aBackgroundChildLoggingId,
const nsACString& aDatabaseId, int64_t aLoggingSerialNumber,
const nsTArray<nsString>& aObjectStoreNames,
bool aIsWriteTransaction,
TransactionDatabaseOperationBase* aTransactionOp);
void Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable);
void Finish(uint64_t aTransactionId, FinishCallback* aCallback);
void CloseDatabaseWhenIdle(
const nsACString& aDatabaseId) {
Unused << CloseDatabaseWhenIdleInternal(aDatabaseId);
}
void WaitForDatabaseToComplete(
const nsCString& aDatabaseId,
nsIRunnable* aCallback);
void Shutdown();
NS_INLINE_DECL_REFCOUNTING(ConnectionPool)
private:
~ConnectionPool();
static void IdleTimerCallback(nsITimer* aTimer,
void* aClosure);
static uint32_t SerialNumber() {
return ++sSerialNumber; }
static uint32_t sSerialNumber;
void Cleanup();
void AdjustIdleTimer();
void CancelIdleTimer();
void CloseIdleDatabases();
bool ScheduleTransaction(TransactionInfo& aTransactionInfo,
bool aFromQueuedTransactions);
void NoteFinishedTransaction(uint64_t aTransactionId);
void ScheduleQueuedTransactions();
void NoteIdleDatabase(DatabaseInfo& aDatabaseInfo);
void NoteClosedDatabase(DatabaseInfo& aDatabaseInfo);
bool MaybeFireCallback(DatabaseCompleteCallback* aCallback);
void PerformIdleDatabaseMaintenance(DatabaseInfo& aDatabaseInfo);
void CloseDatabase(DatabaseInfo& aDatabaseInfo)
const;
bool CloseDatabaseWhenIdleInternal(
const nsACString& aDatabaseId);
};
class ConnectionPool::ConnectionRunnable :
public Runnable {
protected:
DatabaseInfo& mDatabaseInfo;
nsCOMPtr<nsIEventTarget> mOwningEventTarget;
explicit ConnectionRunnable(DatabaseInfo& aDatabaseInfo);
~ConnectionRunnable() override =
default;
};
class ConnectionPool::IdleConnectionRunnable final :
public ConnectionRunnable {
const bool mNeedsCheckpoint;
Atomic<
bool> mInterrupted;
public:
IdleConnectionRunnable(DatabaseInfo& aDatabaseInfo,
bool aNeedsCheckpoint)
: ConnectionRunnable(aDatabaseInfo), mNeedsCheckpoint(aNeedsCheckpoint) {}
NS_INLINE_DECL_REFCOUNTING_INHERITED(IdleConnectionRunnable,
ConnectionRunnable)
void Interrupt() { mInterrupted =
true; }
private:
~IdleConnectionRunnable() override =
default;
NS_DECL_NSIRUNNABLE
};
class ConnectionPool::CloseConnectionRunnable final
:
public ConnectionRunnable {
public:
explicit CloseConnectionRunnable(DatabaseInfo& aDatabaseInfo)
: ConnectionRunnable(aDatabaseInfo) {}
NS_INLINE_DECL_REFCOUNTING_INHERITED(CloseConnectionRunnable,
ConnectionRunnable)
private:
~CloseConnectionRunnable() override =
default;
NS_DECL_NSIRUNNABLE
};
struct ConnectionPool::DatabaseInfo final {
friend class mozilla::DefaultDelete<DatabaseInfo>;
RefPtr<ConnectionPool> mConnectionPool;
const nsCString mDatabaseId;
RefPtr<DatabaseConnection> mConnection;
nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions;
nsTArray<NotNull<TransactionInfo*>> mTransactionsScheduledDuringClose;
nsTArray<NotNull<TransactionInfo*>> mScheduledWriteTransactions;
Maybe<TransactionInfo&> mRunningWriteTransaction;
RefPtr<TaskQueue> mEventTarget;
uint32_t mReadTransactionCount;
uint32_t mWriteTransactionCount;
bool mNeedsCheckpoint;
bool mIdle;
FlippedOnce<
false> mCloseOnIdle;
bool mClosing;
#ifdef DEBUG
nsISerialEventTarget* mDEBUGConnectionEventTarget;
#endif
DatabaseInfo(ConnectionPool* aConnectionPool,
const nsACString& aDatabaseId);
void AssertIsOnConnectionThread()
const {
MOZ_ASSERT(mDEBUGConnectionEventTarget);
MOZ_ASSERT(GetCurrentSerialEventTarget() == mDEBUGConnectionEventTarget);
}
uint64_t TotalTransactionCount()
const {
return mReadTransactionCount + mWriteTransactionCount;
}
nsresult Dispatch(already_AddRefed<nsIRunnable> aRunnable);
private:
~DatabaseInfo();
DatabaseInfo(
const DatabaseInfo&) =
delete;
DatabaseInfo&
operator=(
const DatabaseInfo&) =
delete;
};
struct ConnectionPool::DatabaseCompleteCallback final {
friend class DefaultDelete<DatabaseCompleteCallback>;
nsCString mDatabaseId;
nsCOMPtr<nsIRunnable> mCallback;
DatabaseCompleteCallback(
const nsCString& aDatabaseIds,
nsIRunnable* aCallback);
private:
~DatabaseCompleteCallback();
};
class NS_NO_VTABLE ConnectionPool::FinishCallback :
public nsIRunnable {
public:
// Called on the owning thread before any additional transactions are
// unblocked.
virtual void TransactionFinishedBeforeUnblock() = 0;
// Called on the owning thread after additional transactions may have been
// unblocked.
virtual void TransactionFinishedAfterUnblock() = 0;
protected:
FinishCallback() =
default;
virtual ~FinishCallback() =
default;
};
class ConnectionPool::FinishCallbackWrapper final :
public Runnable {
RefPtr<ConnectionPool> mConnectionPool;
RefPtr<FinishCallback> mCallback;
nsCOMPtr<nsIEventTarget> mOwningEventTarget;
uint64_t mTransactionId;
bool mHasRunOnce;
public:
FinishCallbackWrapper(ConnectionPool* aConnectionPool,
uint64_t aTransactionId, FinishCallback* aCallback);
NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishCallbackWrapper, Runnable)
private:
~FinishCallbackWrapper() override;
NS_DECL_NSIRUNNABLE
};
#ifdef DEBUG
class ConnectionPool::TransactionRunnable final :
public Runnable {
public:
explicit TransactionRunnable(nsCOMPtr<nsIRunnable> aRunnable);
private:
NS_DECL_NSIRUNNABLE
nsCOMPtr<nsIRunnable> mRunnable;
};
#endif
class ConnectionPool::TransactionInfo final {
friend class mozilla::DefaultDelete<TransactionInfo>;
nsTHashSet<TransactionInfo*> mBlocking;
nsTArray<NotNull<TransactionInfo*>> mBlockingOrdered;
public:
DatabaseInfo& mDatabaseInfo;
const nsID mBackgroundChildLoggingId;
const nsCString mDatabaseId;
const uint64_t mTransactionId;
const int64_t mLoggingSerialNumber;
const nsTArray<nsString> mObjectStoreNames;
nsTHashSet<TransactionInfo*> mBlockedOn;
nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables;
const bool mIsWriteTransaction;
bool mRunning;
#ifdef DEBUG
FlippedOnce<
false> mFinished;
#endif
TransactionInfo(DatabaseInfo& aDatabaseInfo,
const nsID& aBackgroundChildLoggingId,
const nsACString& aDatabaseId, uint64_t aTransactionId,
int64_t aLoggingSerialNumber,
const nsTArray<nsString>& aObjectStoreNames,
bool aIsWriteTransaction,
TransactionDatabaseOperationBase* aTransactionOp);
void AddBlockingTransaction(TransactionInfo& aTransactionInfo);
void RemoveBlockingTransactions();
private:
~TransactionInfo();
void MaybeUnblock(TransactionInfo& aTransactionInfo);
};
struct ConnectionPool::TransactionInfoPair final {
// Multiple reading transactions can block future writes.
nsTArray<NotNull<TransactionInfo*>> mLastBlockingWrites;
// But only a single writing transaction can block future reads.
Maybe<TransactionInfo&> mLastBlockingReads;
#if defined(DEBUG) ||
defined(NS_BUILD_REFCNT_LOGGING)
TransactionInfoPair();
~TransactionInfoPair();
#endif
};
/*******************************************************************************
* Actor class declarations
******************************************************************************/
template <IDBCursorType CursorType>
class CommonOpenOpHelper;
template <IDBCursorType CursorType>
class IndexOpenOpHelper;
template <IDBCursorType CursorType>
class ObjectStoreOpenOpHelper;
template <IDBCursorType CursorType>
class OpenOpHelper;
class DatabaseOperationBase :
public Runnable,
public mozIStorageProgressHandler {
template <IDBCursorType CursorType>
friend class OpenOpHelper;
protected:
class AutoSetProgressHandler;
using UniqueIndexTable = nsTHashMap<nsUint64HashKey,
bool>;
const nsCOMPtr<nsIEventTarget> mOwningEventTarget;
const nsID mBackgroundChildLoggingId;
const uint64_t mLoggingSerialNumber;
private:
nsresult mResultCode = NS_OK;
Atomic<
bool> mOperationMayProceed;
FlippedOnce<
false> mActorDestroyed;
public:
NS_DECL_ISUPPORTS_INHERITED
bool IsOnOwningThread()
const {
MOZ_ASSERT(mOwningEventTarget);
bool current;
return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) &&
current;
}
void AssertIsOnOwningThread()
const {
MOZ_ASSERT(IsOnBackgroundThread());
MOZ_ASSERT(IsOnOwningThread());
}
void NoteActorDestroyed() {
AssertIsOnOwningThread();
mActorDestroyed.EnsureFlipped();
mOperationMayProceed =
false;
}
bool IsActorDestroyed()
const {
AssertIsOnOwningThread();
return mActorDestroyed;
}
// May be called on any thread, but you should call IsActorDestroyed() if
// you know you're on the background thread because it is slightly faster.
bool OperationMayProceed()
const {
return mOperationMayProceed; }
const nsID& BackgroundChildLoggingId()
const {
return mBackgroundChildLoggingId;
}
uint64_t LoggingSerialNumber()
const {
return mLoggingSerialNumber; }
nsresult ResultCode()
const {
return mResultCode; }
void SetFailureCode(nsresult aFailureCode) {
MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
OverrideFailureCode(aFailureCode);
}
void SetFailureCodeIfUnset(nsresult aFailureCode) {
if (NS_SUCCEEDED(mResultCode)) {
OverrideFailureCode(aFailureCode);
}
}
bool HasFailed()
const {
return NS_FAILED(mResultCode); }
protected:
DatabaseOperationBase(
const nsID& aBackgroundChildLoggingId,
uint64_t aLoggingSerialNumber)
: Runnable(
"dom::indexedDB::DatabaseOperationBase"),
mOwningEventTarget(GetCurrentSerialEventTarget()),
mBackgroundChildLoggingId(aBackgroundChildLoggingId),
mLoggingSerialNumber(aLoggingSerialNumber),
mOperationMayProceed(
true) {
AssertIsOnOwningThread();
}
~DatabaseOperationBase() override { MOZ_ASSERT(mActorDestroyed); }
void OverrideFailureCode(nsresult aFailureCode) {
MOZ_ASSERT(NS_FAILED(aFailureCode));
mResultCode = aFailureCode;
}
static nsAutoCString MaybeGetBindingClauseForKeyRange(
const Maybe<SerializedKeyRange>& aOptionalKeyRange,
const nsACString& aKeyColumnName);
static nsAutoCString GetBindingClauseForKeyRange(
const SerializedKeyRange& aKeyRange,
const nsACString& aKeyColumnName);
static uint64_t ReinterpretDoubleAsUInt64(
double aDouble);
static nsresult BindKeyRangeToStatement(
const SerializedKeyRange& aKeyRange,
mozIStorageStatement* aStatement);
static nsresult BindKeyRangeToStatement(
const SerializedKeyRange& aKeyRange,
mozIStorageStatement* aStatement,
const nsCString& aLocale);
static Result<IndexDataValuesAutoArray, nsresult>
IndexDataValuesFromUpdateInfos(
const nsTArray<IndexUpdateInfo>& aUpdateInfos,
const UniqueIndexTable& aUniqueIndexTable);
static nsresult InsertIndexTableRows(
DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
const Key& aObjectStoreKey,
const nsTArray<IndexDataValue>& aIndexValues);
static nsresult DeleteIndexDataTableRows(
DatabaseConnection* aConnection,
const Key& aObjectStoreKey,
const nsTArray<IndexDataValue>& aIndexValues);
static nsresult DeleteObjectStoreDataTableRowsWithIndexes(
DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
const Maybe<SerializedKeyRange>& aKeyRange);
static nsresult UpdateIndexValues(
DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
const Key& aObjectStoreKey,
const nsTArray<IndexDataValue>& aIndexValues);
static Result<
bool, nsresult> ObjectStoreHasIndexes(
DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId);
private:
template <
typename KeyTransformation>
static nsresult MaybeBindKeyToStatement(
const Key& aKey, mozIStorageStatement* aStatement,
const nsACString& aParameterName,
const KeyTransformation& aKeyTransformation);
template <
typename KeyTransformation>
static nsresult BindTransformedKeyRangeToStatement(
const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement,
const KeyTransformation& aKeyTransformation);
// Not to be overridden by subclasses.
NS_DECL_MOZISTORAGEPROGRESSHANDLER
};
class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final {
Maybe<mozIStorageConnection&> mConnection;
#ifdef DEBUG
DatabaseOperationBase* mDEBUGDatabaseOp;
#endif
public:
AutoSetProgressHandler();
~AutoSetProgressHandler();
nsresult
Register(mozIStorageConnection& aConnection,
DatabaseOperationBase* aDatabaseOp);
void Unregister();
};
class TransactionDatabaseOperationBase :
public DatabaseOperationBase {
enum class InternalState {
Initial,
DatabaseWork,
SendingPreprocess,
WaitingForContinue,
SendingResults,
Completed
};
InitializedOnce<
const NotNull<SafeRefPtr<TransactionBase>>> mTransaction;
// Unique request id within the context of the transaction, allocated by the
// transaction in the content process starting from 0. Values less than 0 are
// impossible and forbidden. Used to support the explicit commit() request.
const int64_t mRequestId;
InternalState mInternalState = InternalState::Initial;
bool mWaitingForContinue =
false;
const bool mTransactionIsAborted;
protected:
const int64_t mTransactionLoggingSerialNumber;
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
protected:
// A check only enables when the diagnostic assert turns on. It assumes the
// mUpdateRefcountFunction is a nullptr because the previous
// StartTransactionOp failed on the connection thread and the next write
--> --------------------
--> maximum size reached
--> --------------------