| // 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_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_BEGIN("disk_cache", "SqlSharedCacheIndexDatabase::Initialize"); | |
| base::ElapsedTimer timer; | |
| auto result = InitializeInternal(); | |
| RecordTimeAndErrorResultHistogram("Initialize", timer.Elapsed(), | |
| result.error_or(kNoErrorForMetrics)); | |
| TRACE_EVENT_END("disk_cache", "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. | |
| sql::MetaTable 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_BEGIN("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_END( | |
| "disk_cache", "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_BEGIN("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_END( | |
| "disk_cache", "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_BEGIN("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_END("disk_cache", "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 |