blob: 0ff0b31a6a796bc00d156fb5d7b0ebb448462c40 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/instance/bucket_context.h"
#include <inttypes.h>
#include <stddef.h>
#include <algorithm>
#include <compare>
#include <cstdint>
#include <limits>
#include <list>
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/map_util.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/checked_math.h"
#include "base/sequence_checker.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_dump_request_args.h"
#include "base/types/expected.h"
#include "components/services/storage/privileged/cpp/bucket_client_info.h"
#include "components/services/storage/privileged/mojom/indexed_db_client_state_checker.mojom.h"
#include "components/services/storage/privileged/mojom/indexed_db_control_test.mojom.h"
#include "components/services/storage/privileged/mojom/indexed_db_internals_types.mojom.h"
#include "components/services/storage/public/cpp/buckets/bucket_info.h"
#include "components/services/storage/public/cpp/quota_error_or.h"
#include "components/services/storage/public/mojom/blob_storage_context.mojom.h"
#include "components/services/storage/public/mojom/file_system_access_context.mojom.h"
#include "content/browser/indexed_db/blob_reader.h"
#include "content/browser/indexed_db/file_path_util.h"
#include "content/browser/indexed_db/indexed_db_external_object.h"
#include "content/browser/indexed_db/indexed_db_reporting.h"
#include "content/browser/indexed_db/instance/backing_store.h"
#include "content/browser/indexed_db/instance/bucket_context_handle.h"
#include "content/browser/indexed_db/instance/connection.h"
#include "content/browser/indexed_db/instance/database.h"
#include "content/browser/indexed_db/instance/database_callbacks.h"
#include "content/browser/indexed_db/instance/leveldb/backing_store.h"
#include "content/browser/indexed_db/instance/pending_connection.h"
#include "content/browser/indexed_db/instance/sqlite/backing_store_impl.h"
#include "content/browser/indexed_db/status.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "url/origin.h"
namespace content::indexed_db {
namespace {
// Time after the last connection to a database is closed and when we destroy
// the backing store.
const int64_t kBackingStoreGracePeriodSeconds = 2;
// This struct facilitates requesting bucket space usage from the quota manager.
// There have been reports of the callback being passed to the quota manager
// never being invoked. This struct will make sure to invoke the wrapped
// callback when it goes out of scope. The struct itself is in turn intended to
// be wrapped in a callback passed to the quota manager.
//
// There are three main tasks for this struct.
// * It makes sure the passed callback is run by doing so on destruction.
// * It logs UMA.
// * It times out the request if the quota manager is taking too long.
struct GetBucketSpaceRequestWrapper {
static constexpr base::TimeDelta kTimeoutDuration = base::Seconds(45);
// The timeout is split into 3 steps. See similar logic in
// Transaction::kMaxTimeoutStrikes for reasoning.
static constexpr int kTimeoutFraction = 3;
explicit GetBucketSpaceRequestWrapper(
base::OnceCallback<void(storage::QuotaErrorOr<int64_t>)> callback)
: wrapped_callback(std::move(callback)) {
StartTimer();
}
GetBucketSpaceRequestWrapper(GetBucketSpaceRequestWrapper&& other) {
wrapped_callback = std::move(other.wrapped_callback);
start_time = other.start_time;
StartTimer();
}
~GetBucketSpaceRequestWrapper() { InvokeCallback(); }
void StartTimer() {
timeout.Start(
FROM_HERE,
(timeouts_observed + 1) * (kTimeoutDuration / kTimeoutFraction) -
(base::TimeTicks::Now() - start_time),
base::BindOnce(&GetBucketSpaceRequestWrapper::TimeOut,
base::Unretained(this)));
}
void TimeOut() {
if (++timeouts_observed == kTimeoutFraction) {
InvokeCallback();
} else {
StartTimer();
}
}
void InvokeCallback() {
if (!wrapped_callback) {
return;
}
static const char kDroppedRequest[] =
"IndexedDB.QuotaCheckTime2.DroppedRequest";
static const char kSuccess[] = "IndexedDB.QuotaCheckTime2.Success";
static const char kQuotaError[] = "IndexedDB.QuotaCheckTime2.QuotaError";
const char* histogram =
result_value ? result_value->has_value() ? kSuccess : kQuotaError
: kDroppedRequest;
base::UmaHistogramCustomTimes(
histogram, base::TimeTicks::Now() - start_time, base::Milliseconds(1),
kTimeoutDuration * 2, /*buckets=*/50U);
std::move(wrapped_callback)
.Run(result_value.value_or(
base::unexpected(storage::QuotaError::kUnknownError)));
}
int timeouts_observed = 0;
base::OneShotTimer timeout;
base::OnceCallback<void(storage::QuotaErrorOr<int64_t>)> wrapped_callback;
std::optional<storage::QuotaErrorOr<int64_t>> result_value;
base::TimeTicks start_time = base::TimeTicks::Now();
};
DatabaseError CreateDefaultError() {
return DatabaseError(
blink::mojom::IDBException::kUnknownError,
u"Internal error opening backing store for indexedDB.open.");
}
// Creates the leveldb and blob storage directories for IndexedDB.
std::
tuple<base::FilePath /*leveldb_path*/, base::FilePath /*blob_path*/, Status>
CreateDatabaseDirectories(const base::FilePath& path_base,
const storage::BucketLocator& bucket_locator) {
Status status;
if (!base::CreateDirectory(path_base)) {
status = Status::IOError("Unable to create IndexedDB database path");
LOG(ERROR) << status.ToString() << ": \"" << path_base.AsUTF8Unsafe()
<< "\"";
ReportOpenStatus(INDEXED_DB_BACKING_STORE_OPEN_FAILED_DIRECTORY,
bucket_locator);
return {base::FilePath(), base::FilePath(), status};
}
base::FilePath leveldb_path =
path_base.Append(GetLevelDBFileName(bucket_locator));
base::FilePath blob_path =
path_base.Append(GetBlobStoreFileName(bucket_locator));
if (IsPathTooLong(leveldb_path)) {
ReportOpenStatus(INDEXED_DB_BACKING_STORE_OPEN_ORIGIN_TOO_LONG,
bucket_locator);
status = Status::IOError("File path too long");
return {base::FilePath(), base::FilePath(), status};
}
return {leveldb_path, blob_path, status};
}
} // namespace
// TODO(crbug.com/40253999): Move to blink when needed there.
BASE_FEATURE(kSqliteBackingStore,
"IdbSqliteBackingStore",
base::FEATURE_DISABLED_BY_DEFAULT);
BucketContext::Delegate::Delegate()
: on_ready_for_destruction(base::DoNothing()),
on_receiver_bounced(base::DoNothing()),
on_content_changed(base::DoNothing()),
on_files_written(base::DoNothing()) {}
BucketContext::Delegate::Delegate(Delegate&& other) = default;
BucketContext::Delegate::~Delegate() = default;
BucketContext::BucketContext(
storage::BucketInfo bucket_info,
const base::FilePath& data_path,
Delegate&& delegate,
scoped_refptr<base::UpdateableSequencedTaskRunner> updateable_task_runner,
scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy,
mojo::PendingRemote<storage::mojom::BlobStorageContext>
blob_storage_context,
mojo::PendingRemote<storage::mojom::FileSystemAccessContext>
file_system_access_context)
: bucket_info_(std::move(bucket_info)),
updateable_task_runner_(updateable_task_runner),
data_path_(data_path),
quota_manager_proxy_(std::move(quota_manager_proxy)),
blob_storage_context_(std::move(blob_storage_context)),
file_system_access_context_(std::move(file_system_access_context)),
delegate_(std::move(delegate)) {
base::trace_event::MemoryDumpManager::GetInstance()
->RegisterDumpProviderWithSequencedTaskRunner(
this, "BucketContext", base::SequencedTaskRunner::GetCurrentDefault(),
base::trace_event::MemoryDumpProvider::Options());
receivers_.set_disconnect_handler(base::BindRepeating(
&BucketContext::OnReceiverDisconnected, base::Unretained(this)));
}
BucketContext::~BucketContext() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
this);
delegate_.on_ready_for_destruction.Reset();
ResetBackingStore();
}
void BucketContext::ForceClose(bool doom, const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
is_doomed_ = doom;
{
// This handle keeps `this` from closing until it goes out of scope.
BucketContextHandle handle(*this);
for (const auto& [name, database] : databases_) {
// Note: We purposefully ignore the result here as force close needs to
// continue tearing things down anyways.
database->ForceCloseAndRunTasks(SanitizeErrorMessage(message));
}
databases_.clear();
has_blobs_outstanding_ = false;
close_timer_.Stop();
if (backing_store()) {
backing_store()->InvalidateBlobReferences();
// Don't run the preclosing tasks after a ForceClose, whether or not we've
// started them. Compaction in particular can run long and cannot be
// interrupted, so it can cause shutdown hangs.
backing_store()->StopPreCloseTasks();
}
skip_closing_sequence_ = true;
}
// Initiate deletion if appropriate.
RunTasks();
}
void BucketContext::StartMetadataRecording() {
CHECK(!metadata_recording_enabled_);
metadata_recording_start_time_ = base::Time::Now();
metadata_recording_enabled_ = true;
RecordInternalsSnapshot(); // Capture the initial snapshot.
}
std::vector<storage::mojom::IdbBucketMetadataPtr>
BucketContext::StopMetadataRecording() {
metadata_recording_enabled_ = false;
// Track the previous snapshot for each transaction in each database, keyed
// by a string of db name, transaction ID and connection ID.
std::unordered_map<std::string, storage::mojom::IdbTransactionMetadata*>
transaction_snapshots;
for (const storage::mojom::IdbBucketMetadataPtr& snapshot :
metadata_recording_buffer_) {
for (const storage::mojom::IdbDatabaseMetadataPtr& db :
snapshot->databases) {
for (const storage::mojom::IdbTransactionMetadataPtr& tx :
db->transactions) {
auto key = base::StringPrintf(
"%s-%li-%i", base::UTF16ToASCII(db->name).c_str(),
static_cast<long>(tx->tid), tx->connection_id);
if (storage::mojom::IdbTransactionMetadata* prev_snapshot =
base::FindPtrOrNull(transaction_snapshots, key)) {
// Copy the state from the previous snapshot for this transaction ID.
for (const auto& snap : prev_snapshot->state_history) {
tx->state_history.push_back(snap->Clone());
}
tx->state_history.back()->duration += tx->age - prev_snapshot->age;
if (prev_snapshot->state != tx->state) {
tx->state_history.push_back(
storage::mojom::IdbTransactionMetadataStateHistory::New(
tx->state, 0));
}
} else {
tx->state_history.push_back(
storage::mojom::IdbTransactionMetadataStateHistory::New(tx->state,
tx->age));
}
transaction_snapshots[key] = tx.get();
}
}
}
return std::move(metadata_recording_buffer_);
}
int64_t BucketContext::GetInMemorySize() {
return backing_store_ ? backing_store_->GetInMemorySize() : 0;
}
void BucketContext::ReportOutstandingBlobs(bool blobs_outstanding) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
has_blobs_outstanding_ = blobs_outstanding;
MaybeStartClosing();
}
void BucketContext::OnConnectionPriorityUpdated() {
if (!updateable_task_runner_) {
return;
}
base::TaskPriority priority = CalculateSchedulingPriority() == 0
? base::TaskPriority::USER_BLOCKING
: base::TaskPriority::USER_VISIBLE;
updateable_task_runner_->UpdatePriority(priority);
}
std::optional<int> BucketContext::CalculateSchedulingPriority() {
std::optional<int> scheduling_priority;
// Established connections:
for (const auto& [name, database] : databases_) {
for (auto* connection : database->connections()) {
scheduling_priority = std::min(
scheduling_priority.value_or(std::numeric_limits<int>::max()),
connection->scheduling_priority());
}
}
// Pending connections:
for (auto iter = pending_connections_.begin();
iter != pending_connections_.end();) {
if (iter->WasInvalidated()) {
iter = pending_connections_.erase(iter);
} else {
scheduling_priority = std::min(
scheduling_priority.value_or(std::numeric_limits<int>::max()),
(*iter)->scheduling_priority);
++iter;
}
}
return scheduling_priority;
}
void BucketContext::CheckCanUseDiskSpace(
int64_t space_requested,
base::OnceCallback<void(bool)> bucket_space_check_callback) {
if (space_requested <= GetBucketSpaceToAllot()) {
if (bucket_space_check_callback) {
bucket_space_remaining_ -= space_requested;
std::move(bucket_space_check_callback).Run(/*allowed=*/true);
}
return;
}
bucket_space_remaining_ = 0;
bucket_space_remaining_timestamp_ = base::TimeTicks();
bool check_pending = !bucket_space_check_callbacks_.empty();
bucket_space_check_callbacks_.emplace(space_requested,
std::move(bucket_space_check_callback));
if (!check_pending) {
auto callback_with_logging = base::BindOnce(
[](GetBucketSpaceRequestWrapper request_wrapper,
storage::QuotaErrorOr<int64_t> result) {
request_wrapper.result_value = result;
},
GetBucketSpaceRequestWrapper(
base::BindOnce(&BucketContext::OnGotBucketSpaceRemaining,
weak_factory_.GetWeakPtr())));
quota_manager()->GetBucketSpaceRemaining(
bucket_locator(), base::SequencedTaskRunner::GetCurrentDefault(),
std::move(callback_with_logging));
}
}
void BucketContext::OnGotBucketSpaceRemaining(
storage::QuotaErrorOr<int64_t> space_left) {
bool allowed = space_left.has_value();
bucket_space_remaining_ = space_left.value_or(0);
bucket_space_remaining_timestamp_ = base::TimeTicks::Now();
while (!bucket_space_check_callbacks_.empty()) {
auto& [space_requested, result_callback] =
bucket_space_check_callbacks_.front();
allowed = allowed && (space_requested <= bucket_space_remaining_);
if (allowed && result_callback) {
bucket_space_remaining_ -= space_requested;
}
auto taken_callback = std::move(result_callback);
bucket_space_check_callbacks_.pop();
if (taken_callback) {
std::move(taken_callback).Run(allowed);
}
}
}
int64_t BucketContext::GetBucketSpaceToAllot() {
base::TimeDelta bucket_space_age =
base::TimeTicks::Now() - bucket_space_remaining_timestamp_;
if (bucket_space_age > kBucketSpaceCacheTimeLimit) {
return 0;
}
return bucket_space_remaining_ *
(1 - bucket_space_age / kBucketSpaceCacheTimeLimit);
}
void BucketContext::CreateAllExternalObjects(
const std::vector<IndexedDBExternalObject>& objects,
std::vector<blink::mojom::IDBExternalObjectPtr>* mojo_objects) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT0("IndexedDB", "BucketContext::CreateAllExternalObjects");
DCHECK_EQ(objects.size(), mojo_objects->size());
if (objects.empty()) {
return;
}
for (size_t i = 0; i < objects.size(); ++i) {
const IndexedDBExternalObject& blob_info = objects[i];
blink::mojom::IDBExternalObjectPtr& mojo_object = (*mojo_objects)[i];
switch (blob_info.object_type()) {
case IndexedDBExternalObject::ObjectType::kBlob:
case IndexedDBExternalObject::ObjectType::kFile: {
DCHECK(mojo_object->is_blob_or_file());
blink::mojom::IDBBlobInfoPtr& output_info =
mojo_object->get_blob_or_file();
mojo::PendingReceiver<blink::mojom::Blob> receiver =
output_info->blob.InitWithNewPipeAndPassReceiver();
if (blob_info.is_remote_valid()) {
blob_info.Clone(std::move(receiver));
continue;
}
BindBlobReader(blob_info, std::move(receiver));
break;
}
case IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle: {
DCHECK(mojo_object->is_file_system_access_token());
mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>
mojo_token;
if (blob_info.is_file_system_access_remote_valid()) {
blob_info.file_system_access_token_remote()->Clone(
mojo_token.InitWithNewPipeAndPassReceiver());
} else {
DCHECK(!blob_info.serialized_file_system_access_handle().empty());
file_system_access_context_->DeserializeHandle(
bucket_info_.storage_key,
blob_info.serialized_file_system_access_handle(),
mojo_token.InitWithNewPipeAndPassReceiver());
}
mojo_object->get_file_system_access_token() = std::move(mojo_token);
break;
}
}
}
}
void BucketContext::QueueRunTasks() {
if (task_run_queued_) {
return;
}
task_run_queued_ = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&BucketContext::RunTasks, weak_factory_.GetWeakPtr()));
}
void BucketContext::RunTasks() {
task_run_queued_ = false;
for (auto db_it = databases_.begin(); db_it != databases_.end();) {
Database& db = *db_it->second;
Status status = db.RunTasks();
if (!status.ok()) {
OnDatabaseError(status, {});
return;
}
if (db.CanBeDestroyed()) {
db_it = databases_.erase(db_it);
} else {
++db_it;
}
}
if (CanClose() && closing_stage_ == ClosingState::kClosed) {
ResetBackingStore();
}
}
void BucketContext::AddReceiver(
const storage::BucketClientInfo& client_info,
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
client_state_checker_remote,
mojo::PendingReceiver<blink::mojom::IDBFactory> pending_receiver) {
// When `on_ready_for_destruction` is non-null, `this` hasn't requested its
// own destruction. When it is null, this is to be torn down and has to bounce
// the AddReceiver request back to the delegate.
if (delegate().on_ready_for_destruction) {
receivers_.Add(
this, std::move(pending_receiver),
ReceiverContext(client_info, std::move(client_state_checker_remote)));
} else {
delegate().on_receiver_bounced.Run(client_info,
std::move(client_state_checker_remote),
std::move(pending_receiver));
}
}
void BucketContext::GetDatabaseInfo(GetDatabaseInfoCallback callback) {
Status s;
DatabaseError error;
std::vector<blink::mojom::IDBNameAndVersionPtr> names_and_versions;
std::tie(s, error, std::ignore) =
InitBackingStoreIfNeeded(/*create_if_missing=*/false);
DCHECK_EQ(s.ok(), !!backing_store_);
if (s.ok()) {
s = backing_store_->GetDatabaseNamesAndVersions(&names_and_versions);
if (!s.ok()) {
error = DatabaseError(blink::mojom::IDBException::kUnknownError,
"Internal error opening backing store for "
"indexedDB.databases().");
}
}
std::move(callback).Run(
std::move(names_and_versions),
blink::mojom::IDBError::New(error.code(), error.message()));
if (s.IsCorruption()) {
HandleBackingStoreCorruption(base::UTF16ToUTF8(error.message()));
}
}
void BucketContext::Open(
mojo::PendingAssociatedRemote<blink::mojom::IDBFactoryClient>
factory_client,
mojo::PendingAssociatedRemote<blink::mojom::IDBDatabaseCallbacks>
database_callbacks_remote,
const std::u16string& name,
int64_t version,
mojo::PendingAssociatedReceiver<blink::mojom::IDBTransaction>
transaction_receiver,
int64_t transaction_id,
int scheduling_priority) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT0("IndexedDB", "BucketContext::Open");
// TODO(dgrogan): Don't let a non-existing database be opened (and therefore
// created) if this origin is already over quota.
bool was_cold_open = !backing_store_;
Status s;
DatabaseError error;
IndexedDBDataLossInfo data_loss_info;
std::tie(s, error, data_loss_info) =
InitBackingStoreIfNeeded(/*create_if_missing=*/true);
if (!backing_store_) {
FactoryClient(std::move(factory_client)).OnError(error);
if (s.IsCorruption()) {
HandleBackingStoreCorruption(base::UTF16ToUTF8(error.message()));
}
return;
}
auto connection = std::make_unique<PendingConnection>(
std::make_unique<FactoryClient>(std::move(factory_client)),
std::make_unique<DatabaseCallbacks>(std::move(database_callbacks_remote)),
transaction_id, version, std::move(transaction_receiver));
connection->was_cold_open = was_cold_open;
connection->data_loss_info = data_loss_info;
connection->scheduling_priority = scheduling_priority;
ReceiverContext& client = receivers_.current_context();
// `Connection` only needs an opaque token to uniquely identify the
// document or worker that owns the other side of the connection.
connection->client_token = client.client_info.document_token
? client.client_info.document_token->value()
: client.client_info.context_token.value();
// Null in unit tests.
if (client.client_state_checker_remote) {
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
state_checker_clone;
client.client_state_checker_remote->MakeClone(
state_checker_clone.InitWithNewPipeAndPassReceiver());
connection->client_state_checker.Bind(std::move(state_checker_clone));
}
Database* database_ptr = nullptr;
auto it = databases_.find(name);
if (it == databases_.end()) {
auto database = std::make_unique<Database>(name, *this);
// The database must be added before the schedule call, as the
// CreateDatabaseDeleteClosure can be called synchronously.
database_ptr = database.get();
AddDatabase(name, std::move(database));
} else {
database_ptr = it->second.get();
}
pending_connections_.push_back(connection->weak_factory.GetWeakPtr());
database_ptr->ScheduleOpenConnection(std::move(connection));
OnConnectionPriorityUpdated();
}
void BucketContext::DeleteDatabase(
mojo::PendingAssociatedRemote<blink::mojom::IDBFactoryClient>
factory_client,
const std::u16string& name,
bool force_close) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT0("IndexedDB", "BucketContext::DeleteDatabase");
std::string force_close_message = "The database is deleted.";
{
Status s;
DatabaseError error;
// Note: Any data loss information here is not piped up to the renderer, and
// will be lost.
std::tie(s, error, std::ignore) = InitBackingStoreIfNeeded(
/*create_if_missing=*/false);
if (!backing_store_) {
if (s.IsNotFound()) {
FactoryClient(std::move(factory_client)).OnDeleteSuccess(/*version=*/0);
return;
}
FactoryClient(std::move(factory_client)).OnError(error);
if (s.IsCorruption()) {
HandleBackingStoreCorruption(base::UTF16ToUTF8(error.message()));
}
return;
}
}
auto on_deletion_complete =
base::BindOnce(delegate().on_files_written, /*flushed=*/true);
// First, check the databases that are already represented by
// `Database` objects. If one exists, schedule it to be deleted and
// we're done.
auto it = databases_.find(name);
if (it != databases_.end()) {
base::WeakPtr<Database> database = it->second->AsWeakPtr();
it->second->ScheduleDeleteDatabase(
std::make_unique<FactoryClient>(std::move(factory_client)),
std::move(on_deletion_complete));
if (force_close) {
Status status = database->ForceCloseAndRunTasks(force_close_message);
if (!status.ok()) {
OnDatabaseError(status, "Error aborting transactions.");
}
}
return;
}
// Otherwise, verify that a database with the given name exists in the backing
// store. If not, report success.
std::vector<std::u16string> names;
Status s = backing_store()->GetDatabaseNames(&names);
if (!s.ok()) {
std::string error_message =
"Internal error opening backing store for indexedDB.deleteDatabase.";
DatabaseError error(blink::mojom::IDBException::kUnknownError,
error_message);
FactoryClient(std::move(factory_client)).OnError(error);
if (s.IsCorruption()) {
HandleBackingStoreCorruption(error_message);
}
return;
}
if (!base::Contains(names, name)) {
FactoryClient(std::move(factory_client)).OnDeleteSuccess(/*version=*/0);
return;
}
// If it exists but does not already have an `Database` object,
// create it and initiate deletion.
auto database = std::make_unique<Database>(name, *this);
Database* database_ptr = AddDatabase(name, std::move(database));
database_ptr->ScheduleDeleteDatabase(
std::make_unique<FactoryClient>(std::move(factory_client)),
std::move(on_deletion_complete));
if (force_close) {
Status status = database_ptr->ForceCloseAndRunTasks(force_close_message);
if (!status.ok()) {
OnDatabaseError(status, "Error aborting transactions.");
}
}
}
storage::mojom::IdbBucketMetadataPtr BucketContext::FillInMetadata(
storage::mojom::IdbBucketMetadataPtr info) {
// TODO(jsbell): Sort by name?
std::vector<storage::mojom::IdbDatabaseMetadataPtr> database_list;
if (backing_store_ && in_memory()) {
info->size = GetInMemorySize();
}
for (const auto& [name, db] : databases_) {
info->connection_count += db->ConnectionCount();
database_list.push_back(db->GetIdbInternalsMetadata());
}
info->databases = std::move(database_list);
for (const auto& [_, context] : receivers_.GetAllContexts()) {
info->clients.push_back(context->client_info);
}
return info;
}
void BucketContext::NotifyOfIdbInternalsRelevantChange() {
if (metadata_recording_enabled_) {
RecordInternalsSnapshot();
}
}
BucketContext* BucketContext::GetReferenceForTesting() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return this;
}
void BucketContext::FlushBackingStoreForTesting() {
backing_store()->FlushForTesting();
}
void BucketContext::BindMockFailureSingletonForTesting(
mojo::PendingReceiver<storage::mojom::MockFailureInjector> receiver) {
level_db::BindMockFailureSingletonForTesting(std::move(receiver)); // IN-TEST
}
Database* BucketContext::AddDatabase(const std::u16string& name,
std::unique_ptr<Database> database) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!base::Contains(databases_, name));
return databases_.emplace(name, std::move(database)).first->second.get();
}
void BucketContext::OnHandleCreated() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
++open_handles_;
if (closing_stage_ != ClosingState::kNotClosing) {
closing_stage_ = ClosingState::kNotClosing;
close_timer_.Stop();
if (backing_store()) {
backing_store()->StopPreCloseTasks();
}
}
}
void BucketContext::OnHandleDestruction() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_GT(open_handles_, 0ll);
--open_handles_;
MaybeStartClosing();
}
bool BucketContext::CanClose() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_GE(open_handles_, 0);
return !has_blobs_outstanding_ && open_handles_ <= 0 &&
(!backing_store_ || is_doomed_ || !in_memory());
}
void BucketContext::MaybeStartClosing() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsClosing() && CanClose()) {
StartClosing();
}
}
void BucketContext::StartClosing() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(CanClose());
DCHECK(!IsClosing());
if (skip_closing_sequence_) {
CloseNow();
return;
}
// Start a timer to close the backing store, unless something else opens it
// in the mean time.
DCHECK(!close_timer_.IsRunning());
closing_stage_ = ClosingState::kPreCloseGracePeriod;
close_timer_.Start(FROM_HERE, base::Seconds(kBackingStoreGracePeriodSeconds),
base::BindOnce(&BucketContext::StartPreCloseTasks,
weak_factory_.GetWeakPtr()));
}
void BucketContext::StartPreCloseTasks() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (closing_stage_ != ClosingState::kPreCloseGracePeriod) {
return;
}
closing_stage_ = ClosingState::kRunningPreCloseTasks;
backing_store()->StartPreCloseTasks(base::BindOnce(
[](base::WeakPtr<BucketContext> bucket_context) {
if (!bucket_context || bucket_context->closing_stage_ !=
ClosingState::kRunningPreCloseTasks) {
return;
}
bucket_context->CloseNow();
},
weak_factory_.GetWeakPtr()));
}
void BucketContext::CloseNow() {
closing_stage_ = ClosingState::kClosed;
close_timer_.Stop();
if (backing_store()) {
backing_store()->StopPreCloseTasks();
}
QueueRunTasks();
}
void BucketContext::BindBlobReader(
const IndexedDBExternalObject& blob_info,
mojo::PendingReceiver<blink::mojom::Blob> blob_receiver) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const base::FilePath& path = blob_info.indexed_db_file_path();
auto itr = file_reader_map_.find(path);
if (itr == file_reader_map_.end()) {
// Unretained is safe because `this` owns the reader.
auto reader = std::make_unique<BlobReader>(
blob_info, base::BindOnce(&BucketContext::RemoveBoundReaders,
base::Unretained(this), path));
itr =
file_reader_map_
.insert({path, std::make_tuple(std::move(reader),
base::ScopedClosureRunner(
blob_info.release_callback()))})
.first;
}
std::get<0>(itr->second)
->AddReceiver(std::move(blob_receiver), *blob_storage_context_);
}
void BucketContext::RemoveBoundReaders(const base::FilePath& path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
file_reader_map_.erase(path);
}
std::string BucketContext::SanitizeErrorMessage(const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The message may contain the database path, which may be considered
// sensitive data, and those strings are passed to the extension, so strip it.
std::string sanitized_message = message;
base::ReplaceSubstringsAfterOffset(&sanitized_message, 0u,
data_path_.AsUTF8Unsafe(), "...");
return sanitized_message;
}
bool BucketContext::ShouldUseSqliteBackingStore() {
// Additional checks may be added subsequently.
return base::FeatureList::IsEnabled(kSqliteBackingStore);
}
void BucketContext::HandleBackingStoreCorruption(
const std::string& error_message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::string sanitized_error_message = SanitizeErrorMessage(error_message);
base::OnceClosure handle_corruption =
base::BindOnce(&level_db::BackingStore::HandleCorruption, data_path_,
bucket_locator(), sanitized_error_message);
ForceClose(/*doom=*/false, sanitized_error_message);
// In order to successfully delete the corrupted DB, the open handle must
// first be closed.
ResetBackingStore();
// NOTE: `this` may be deleted (in tests, where `on_ready_for_destruction`
// executes synchronously).
std::move(handle_corruption).Run();
}
void BucketContext::OnDatabaseError(Status status, const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!status.ok());
const std::string error_message =
message.empty() ? status.ToString() : message;
if (status.IsCorruption()) {
HandleBackingStoreCorruption(error_message);
return;
}
if (status.IsIOError()) {
quota_manager_proxy_->OnClientWriteFailed(bucket_info_.storage_key);
}
ForceClose(/*doom=*/false, error_message);
}
bool BucketContext::OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!backing_store_) {
// Nothing to report when no databases have been loaded.
return true;
}
base::CheckedNumeric<uint64_t> total_memory_in_flight = 0;
for (const auto& [name, database] : databases_) {
for (Connection* connection : database->connections()) {
for (const auto& txn_id_pair : connection->transactions()) {
total_memory_in_flight += txn_id_pair.second->in_flight_memory();
}
}
}
auto* db_dump = pmd->CreateAllocatorDump(
base::StringPrintf("site_storage/index_db/in_flight_0x%" PRIXPTR,
backing_store()->GetIdentifierForMemoryDump()));
db_dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
base::trace_event::MemoryAllocatorDump::kUnitsBytes,
total_memory_in_flight.ValueOrDefault(0));
return true;
}
std::tuple<Status, DatabaseError, IndexedDBDataLossInfo>
BucketContext::InitBackingStoreIfNeeded(bool create_if_missing) {
if (backing_store_) {
return {};
}
base::FilePath blob_path;
base::FilePath database_path;
Status status = Status::OK();
if (!in_memory()) {
std::tie(database_path, blob_path, status) =
CreateDatabaseDirectories(data_path_, bucket_locator());
if (!status.ok()) {
return {status, CreateDefaultError(), IndexedDBDataLossInfo()};
}
}
auto lock_manager = std::make_unique<PartitionedLockManager>();
IndexedDBDataLossInfo data_loss_info;
std::unique_ptr<BackingStore> backing_store;
bool disk_full = false;
base::ElapsedTimer open_timer;
Status first_try_status;
constexpr static const int kNumOpenTries = 2;
for (int i = 0; i < kNumOpenTries; ++i) {
const bool is_first_attempt = i == 0;
std::tie(backing_store, status, data_loss_info, disk_full) =
ShouldUseSqliteBackingStore()
? sqlite::BackingStoreImpl::OpenAndVerify(data_path_)
: level_db::BackingStore::OpenAndVerify(
*this, data_path_, database_path, blob_path,
lock_manager.get(), is_first_attempt, create_if_missing);
if (is_first_attempt) [[likely]] {
first_try_status = status;
}
if (status.ok()) [[likely]] {
break;
}
if (!create_if_missing && status.IsNotFound()) {
return {status, DatabaseError(), data_loss_info};
}
DCHECK(!backing_store);
// If the disk is full, always exit immediately.
if (disk_full) {
break;
}
}
// Record this here because the !create_if_missing && not_found case shouldn't
// count as either a success or failure.
base::UmaHistogramEnumeration(kBackingStoreActionUmaName,
IndexedDBAction::kBackingStoreOpenAttempt);
first_try_status.Log("WebCore.IndexedDB.BackingStore.OpenFirstTryResult");
if (first_try_status.ok()) [[likely]] {
UMA_HISTOGRAM_TIMES(
"WebCore.IndexedDB.BackingStore.OpenFirstTrySuccessTime",
open_timer.Elapsed());
}
if (status.ok()) [[likely]] {
base::UmaHistogramTimes("WebCore.IndexedDB.BackingStore.OpenSuccessTime",
open_timer.Elapsed());
} else {
base::UmaHistogramTimes("WebCore.IndexedDB.BackingStore.OpenFailureTime",
open_timer.Elapsed());
if (disk_full) {
ReportOpenStatus(INDEXED_DB_BACKING_STORE_OPEN_DISK_FULL,
bucket_locator());
quota_manager()->OnClientWriteFailed(bucket_locator().storage_key);
return {status,
DatabaseError(blink::mojom::IDBException::kQuotaError,
u"Encountered full disk while opening "
"backing store for indexedDB.open."),
data_loss_info};
}
ReportOpenStatus(INDEXED_DB_BACKING_STORE_OPEN_NO_RECOVERY,
bucket_locator());
return {status, CreateDefaultError(), data_loss_info};
}
if (!in_memory()) {
ReportOpenStatus(INDEXED_DB_BACKING_STORE_OPEN_SUCCESS, bucket_locator());
}
lock_manager_ = std::move(lock_manager);
backing_store_ = std::move(backing_store);
delegate().on_files_written.Run(/*flushed=*/true);
return {Status::OK(), DatabaseError(), data_loss_info};
}
void BucketContext::ResetBackingStore() {
file_reader_map_.clear();
weak_factory_.InvalidateWeakPtrs();
if (backing_store_) {
base::WaitableEvent leveldb_destruct_event;
backing_store_->TearDown(&leveldb_destruct_event);
const auto start = base::TimeTicks::Now();
backing_store_.reset();
leveldb_destruct_event.Wait();
base::UmaHistogramTimes("IndexedDB.BackingStoreCloseDuration",
base::TimeTicks::Now() - start);
}
task_run_queued_ = false;
is_doomed_ = false;
bucket_space_check_callbacks_ = {};
open_handles_ = 0;
databases_.clear();
lock_manager_.reset();
close_timer_.Stop();
closing_stage_ = ClosingState::kNotClosing;
has_blobs_outstanding_ = false;
skip_closing_sequence_ = false;
running_tasks_ = false;
if (receivers_.empty() && delegate().on_ready_for_destruction) {
std::move(delegate().on_ready_for_destruction).Run();
}
}
void BucketContext::OnReceiverDisconnected() {
if (receivers_.empty() && !backing_store_ &&
delegate().on_ready_for_destruction) {
std::move(delegate().on_ready_for_destruction).Run();
}
}
void BucketContext::RecordInternalsSnapshot() {
storage::mojom::IdbBucketMetadataPtr metadata =
storage::mojom::IdbBucketMetadata::New();
metadata->bucket_locator = bucket_locator();
metadata = FillInMetadata(std::move(metadata));
metadata->delta_recording_start_ms =
(base::Time::Now() - metadata_recording_start_time_).InMilliseconds();
metadata_recording_buffer_.push_back(std::move(metadata));
}
BucketContext::ReceiverContext::ReceiverContext(
const storage::BucketClientInfo& client_info,
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
client_state_checker_remote)
: client_info(client_info),
client_state_checker_remote(std::move(client_state_checker_remote)) {}
BucketContext::ReceiverContext::ReceiverContext(
BucketContext::ReceiverContext&&) noexcept = default;
BucketContext::ReceiverContext::~ReceiverContext() = default;
} // namespace content::indexed_db