| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/disk_cache/sql/sql_shared_cache_index_database.h" |
| |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "net/disk_cache/sql/sql_backend_constants.h" |
| #include "net/disk_cache/sql/sql_shared_cache_index_database_queries.h" |
| #include "sql/database.h" |
| #include "sql/meta_table.h" |
| #include "sql/statement.h" |
| #include "sql/transaction.h" |
| |
| using disk_cache_sql_queries::GetSharedCacheIndexDatabaseQuery; |
| using disk_cache_sql_queries::SharedCacheIndexDatabaseQuery; |
| |
| namespace disk_cache { |
| |
| namespace { |
| |
| constexpr int kSharedCacheIndexDatabaseCurrentVersion = 1; |
| constexpr int kSharedCacheIndexDatabaseCompatibleVersion = 1; |
| constexpr std::string_view kSqlSharedCacheIndexDatabaseHistogramPrefix = |
| "Net.SqlSharedCacheIndexDatabase."; |
| |
| void RecordTimeAndErrorResultHistogram( |
| std::string_view method_name, |
| base::TimeDelta time_delta, |
| SqlSharedCacheIndexDatabase::Error error) { |
| base::UmaHistogramMicrosecondsTimes( |
| base::StrCat({kSqlSharedCacheIndexDatabaseHistogramPrefix, method_name, |
| error == SqlSharedCacheIndexDatabase::kNoErrorForMetrics |
| ? ".SuccessTime" |
| : ".FailureTime"}), |
| time_delta); |
| base::UmaHistogramEnumeration( |
| base::StrCat({kSqlSharedCacheIndexDatabaseHistogramPrefix, method_name, |
| ".Result"}), |
| error); |
| } |
| |
| } // namespace |
| |
| SqlSharedCacheIndexDatabase::SqlSharedCacheIndexDatabase( |
| const base::FilePath& storage_directory) |
| : database_path_( |
| storage_directory.Append(kSqlBackendSharedCacheIndexFileName)), |
| db_(sql::DatabaseOptions() |
| .set_exclusive_locking(true) |
| #if BUILDFLAG(IS_WIN) |
| .set_exclusive_database_file_lock(true) |
| #endif // IS_WIN |
| .set_preload(true) |
| .set_wal_mode(true), |
| sql::Database::Tag("SharedCacheIndex")) { |
| } |
| |
| SqlSharedCacheIndexDatabase::~SqlSharedCacheIndexDatabase() = default; |
| |
| base::expected<void, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::Initialize() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT_BEGIN0("disk_cache", "SqlSharedCacheIndexDatabase::Initialize"); |
| base::ElapsedTimer timer; |
| auto result = InitializeInternal(); |
| RecordTimeAndErrorResultHistogram("Initialize", timer.Elapsed(), |
| result.error_or(kNoErrorForMetrics)); |
| TRACE_EVENT_END1("disk_cache", "SqlSharedCacheIndexDatabase::Initialize", |
| "result", |
| static_cast<int>(result.error_or(kNoErrorForMetrics))); |
| return result; |
| } |
| |
| base::expected<void, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::InitializeInternal() { |
| if (simulate_db_failure_) { |
| return base::unexpected(Error::kFailedForTesting); |
| } |
| if (db_.is_open()) { |
| return base::ok(); |
| } |
| |
| // TODO(crbug.com/473666511): Set an error callback on `db_` to handle |
| // catastrophic errors (like SQLITE_CORRUPT). When such an error occurs, we |
| // should call `db_.RazeAndPoison()` and also trigger a cleanup of all |
| // isolated shared cache databases and entries in the main database |
| // (SqlPersistentStore::Backend) that reference the shared cache. |
| |
| base::FilePath directory = database_path_.DirName(); |
| if (!base::CreateDirectory(directory)) { |
| return base::unexpected(Error::kFailedToCreateDirectory); |
| } |
| |
| if (!db_.Open(database_path_)) { |
| return base::unexpected(Error::kFailedToOpenDatabase); |
| } |
| |
| if (sql::MetaTable::RazeIfIncompatible( |
| &db_, kSharedCacheIndexDatabaseCompatibleVersion, |
| kSharedCacheIndexDatabaseCurrentVersion) == |
| sql::RazeIfIncompatibleResult::kFailed) { |
| return base::unexpected(Error::kFailedToRazeIncompatibleDatabase); |
| } |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) { |
| return base::unexpected(Error::kFailedToStartTransaction); |
| } |
| |
| if (!sql::MetaTable::DoesTableExist(&db_)) { |
| // Initialize the schema. |
| if (!db_.Execute(GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kCreateStoragesTable))) { |
| return base::unexpected(Error::kFailedToCreateStoragesTable); |
| } |
| |
| if (!db_.Execute(GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kCreateUniqueIndex))) { |
| return base::unexpected(Error::kFailedToCreateUniqueIndex); |
| } |
| } |
| |
| // Initialize the meta table. |
| if (!meta_table_.Init(&db_, kSharedCacheIndexDatabaseCurrentVersion, |
| kSharedCacheIndexDatabaseCompatibleVersion)) { |
| return base::unexpected(Error::kFailedToInitializeMetaTable); |
| } |
| |
| if (!transaction.Commit()) { |
| return base::unexpected(Error::kFailedToCommitTransaction); |
| } |
| |
| return base::ok(); |
| } |
| |
| base::expected<SqlSharedCacheDbId, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::GetDbIdByNetworkIsolationKey( |
| const net::NetworkIsolationKey& network_isolation_key, |
| bool create_if_not_exists) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT_BEGIN1( |
| "disk_cache", "SqlSharedCacheIndexDatabase::GetDbIdByNetworkIsolationKey", |
| "nik", network_isolation_key.ToDebugString()); |
| base::ElapsedTimer timer; |
| auto result = GetDbIdByNetworkIsolationKeyInternal(network_isolation_key, |
| create_if_not_exists); |
| RecordTimeAndErrorResultHistogram("GetDbIdByNetworkIsolationKey", |
| timer.Elapsed(), |
| result.error_or(kNoErrorForMetrics)); |
| TRACE_EVENT_END1( |
| "disk_cache", "SqlSharedCacheIndexDatabase::GetDbIdByNetworkIsolationKey", |
| "result", [&](perfetto::TracedValue trace_context) { |
| auto dict = std::move(trace_context).WriteDictionary(); |
| dict.Add("error", |
| static_cast<int>(result.error_or(kNoErrorForMetrics))); |
| if (result.has_value()) { |
| dict.Add("db_id", result.value().value()); |
| } |
| }); |
| return result; |
| } |
| |
| base::expected<SqlSharedCacheDbId, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::GetDbIdByNetworkIsolationKeyInternal( |
| const net::NetworkIsolationKey& network_isolation_key, |
| bool create_if_not_exists) { |
| if (simulate_db_failure_) { |
| return base::unexpected(Error::kFailedForTesting); |
| } |
| if (!db_.is_open()) { |
| return base::unexpected(Error::kFailedToOpenDatabase); |
| } |
| |
| const auto isolation_key_str_opt = network_isolation_key.ToCacheKeyString(); |
| if (!isolation_key_str_opt) { |
| return base::unexpected(Error::kInvalidNetworkIsolationKey); |
| } |
| |
| // First try to find the existing key. |
| { |
| sql::Statement select_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kSelectDbIdByIsolationKey))); |
| select_statement.BindString(0, *isolation_key_str_opt); |
| if (select_statement.Step()) { |
| return SqlSharedCacheDbId(select_statement.ColumnInt64(0)); |
| } |
| if (!select_statement.Succeeded()) { |
| return base::unexpected(Error::kFailedToExecute); |
| } |
| } |
| |
| if (!create_if_not_exists) { |
| return base::unexpected(Error::kNotFound); |
| } |
| |
| // If not found, insert a new one. |
| sql::Statement insert_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kInsertStorage))); |
| insert_statement.BindString(0, *isolation_key_str_opt); |
| if (!insert_statement.Run()) { |
| return base::unexpected(Error::kFailedToExecute); |
| } |
| return SqlSharedCacheDbId(db_.GetLastInsertRowId()); |
| } |
| |
| base::expected<std::string, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::GetIsolationKeyStringByDbId( |
| SqlSharedCacheDbId shared_cache_db_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT_BEGIN1("disk_cache", |
| "SqlSharedCacheIndexDatabase::GetIsolationKeyStringByDbId", |
| "db_id", shared_cache_db_id.value()); |
| base::ElapsedTimer timer; |
| auto result = GetIsolationKeyStringByDbIdInternal(shared_cache_db_id); |
| RecordTimeAndErrorResultHistogram("GetIsolationKeyStringByDbId", |
| timer.Elapsed(), |
| result.error_or(kNoErrorForMetrics)); |
| TRACE_EVENT_END1( |
| "disk_cache", "SqlSharedCacheIndexDatabase::GetIsolationKeyStringByDbId", |
| "result", [&](perfetto::TracedValue trace_context) { |
| auto dict = std::move(trace_context).WriteDictionary(); |
| dict.Add("error", |
| static_cast<int>(result.error_or(kNoErrorForMetrics))); |
| if (result.has_value()) { |
| dict.Add("nik_string", result.value()); |
| } |
| }); |
| return result; |
| } |
| |
| base::expected<std::string, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::GetIsolationKeyStringByDbIdInternal( |
| SqlSharedCacheDbId shared_cache_db_id) { |
| if (simulate_db_failure_) { |
| return base::unexpected(Error::kFailedForTesting); |
| } |
| if (!db_.is_open()) { |
| return base::unexpected(Error::kFailedToOpenDatabase); |
| } |
| sql::Statement select_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kSelectIsolationKeyByDbId))); |
| select_statement.BindInt64(0, shared_cache_db_id.value()); |
| if (select_statement.Step()) { |
| return select_statement.ColumnString(0); |
| } |
| if (!select_statement.Succeeded()) { |
| return base::unexpected(Error::kFailedToExecute); |
| } |
| return base::unexpected(Error::kNotFound); |
| } |
| |
| base::expected<void, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::DeleteByDbId( |
| SqlSharedCacheDbId shared_cache_db_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT_BEGIN1("disk_cache", "SqlSharedCacheIndexDatabase::DeleteByDbId", |
| "db_id", shared_cache_db_id.value()); |
| base::ElapsedTimer timer; |
| auto result = DeleteByDbIdInternal(shared_cache_db_id); |
| RecordTimeAndErrorResultHistogram("DeleteByDbId", timer.Elapsed(), |
| result.error_or(kNoErrorForMetrics)); |
| TRACE_EVENT_END1("disk_cache", "SqlSharedCacheIndexDatabase::DeleteByDbId", |
| "result", |
| static_cast<int>(result.error_or(kNoErrorForMetrics))); |
| return result; |
| } |
| |
| base::expected<void, SqlSharedCacheIndexDatabase::Error> |
| SqlSharedCacheIndexDatabase::DeleteByDbIdInternal( |
| SqlSharedCacheDbId shared_cache_db_id) { |
| if (simulate_db_failure_) { |
| return base::unexpected(Error::kFailedForTesting); |
| } |
| if (!db_.is_open()) { |
| return base::unexpected(Error::kFailedToOpenDatabase); |
| } |
| |
| sql::Statement delete_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, GetSharedCacheIndexDatabaseQuery( |
| SharedCacheIndexDatabaseQuery::kDeleteStorage))); |
| delete_statement.BindInt64(0, shared_cache_db_id.value()); |
| if (!delete_statement.Run()) { |
| return base::unexpected(Error::kFailedToExecute); |
| } |
| if (db_.GetLastChangeCount() == 0) { |
| return base::unexpected(Error::kNotFound); |
| } |
| return base::ok(); |
| } |
| |
| void SqlSharedCacheIndexDatabase::SetSimulateDbFailureForTesting(bool fail) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| simulate_db_failure_ = fail; |
| } |
| |
| } // namespace disk_cache |