| // 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.h" |
| |
| #include <algorithm> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/hash/hash.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/no_destructor.h" |
| #include "base/state_transitions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "net/base/features.h" |
| #include "net/disk_cache/backend_cleanup_tracker.h" |
| #include "net/disk_cache/sql/shared_cache_client_remote.h" |
| #include "net/disk_cache/sql/sql_persistent_store.h" |
| #include "net/disk_cache/sql/sql_shared_cache_handle.h" |
| #include "net/disk_cache/sql/sql_shared_cache_isolated_database.h" |
| #include "net/http/http_cache.h" |
| #include "net/http/http_response_info.h" |
| |
| namespace disk_cache { |
| |
| SqlSharedCache::SqlSharedCache( |
| std::string nik_string, |
| SqlPersistentStore& store, |
| const base::FilePath& directory, |
| base::RepeatingCallback<void(SqlSharedCache&)> on_unreferenced_callback, |
| scoped_refptr<base::SequencedTaskRunner> db_task_runner, |
| scoped_refptr<SqlReadCacheMemoryMonitor> read_cache_memory_monitor, |
| scoped_refptr<BackendCleanupTracker> cleanup_tracker) |
| : nik_string_(std::move(nik_string)), |
| store_(store), |
| directory_(directory), |
| on_unreferenced_callback_(std::move(on_unreferenced_callback)), |
| db_task_runner_(std::move(db_task_runner)), |
| read_cache_memory_monitor_(std::move(read_cache_memory_monitor)), |
| cleanup_tracker_(std::move(cleanup_tracker)) {} |
| |
| SqlSharedCache::~SqlSharedCache() { |
| isolated_database_.Reset(); |
| if (cleanup_tracker_) { |
| CHECK(db_task_runner_); |
| db_task_runner_->PostTaskAndReply( |
| FROM_HERE, base::DoNothing(), |
| base::DoNothingWithBoundArgs(std::move(cleanup_tracker_))); |
| } |
| } |
| |
| void SqlSharedCache::Cleanup(base::OnceClosure callback) { |
| if (!isolated_database_) { |
| std::move(callback).Run(); |
| return; |
| } |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::Cleanup) |
| .Then(std::move(callback)); |
| isolated_database_.Reset(); |
| } |
| |
| void SqlSharedCache::InitIsolatedDatabase( |
| SqlSharedCacheDbId shared_cache_db_id, |
| base::OnceCallback<void(bool)> callback) { |
| CHECK(!shared_cache_db_id_); |
| shared_cache_db_id_ = shared_cache_db_id; |
| isolated_database_ = SqlTrackedSequenceBound<SqlSharedCacheIsolatedDatabase>( |
| db_task_runner_, store_->GetAsyncTaskManager(), nik_string_, directory_, |
| shared_cache_db_id, db_task_runner_, read_cache_memory_monitor_); |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::Init) |
| .Then(base::BindOnce( |
| [](base::OnceCallback<void(bool)> callback, |
| base::expected<void, SqlSharedCacheIsolatedDatabase::Error> |
| result) { std::move(callback).Run(result.has_value()); }, |
| std::move(callback))); |
| |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::GetAllUrlHashes) |
| .Then(base::BindOnce(&SqlSharedCache::OnHashesLoaded, |
| weak_factory_.GetWeakPtr())); |
| |
| for (ClientsMap::iterator it(&clients_); !it.IsAtEnd(); it.Advance()) { |
| isolated_database_ |
| .AsyncCall(&SqlSharedCacheIsolatedDatabase::GetSharedReadOnlyConnection) |
| .Then(base::BindOnce(&SqlSharedCache::OnPendingFileSetForClient, |
| weak_factory_.GetWeakPtr(), it.GetCurrentKey())); |
| } |
| } |
| |
| void SqlSharedCache::RegisterClient( |
| std::unique_ptr<SharedCacheClientRemote> client) { |
| CHECK(client); |
| auto* client_ptr = client.get(); |
| auto client_id = clients_.Add(std::move(client)); |
| client_ptr->SetDisconnectHandler( |
| base::BindOnce(&SqlSharedCache::OnClientDisconnected, |
| weak_factory_.GetWeakPtr(), client_id, CreateHandle())); |
| |
| if (cached_hashes_.has_value()) { |
| std::vector<uint32_t> hashes(cached_hashes_->begin(), |
| cached_hashes_->end()); |
| client_ptr->OnResourcesAdded(hashes); |
| } |
| |
| if (isolated_database_) { |
| isolated_database_ |
| .AsyncCall(&SqlSharedCacheIsolatedDatabase::GetSharedReadOnlyConnection) |
| .Then(base::BindOnce(&SqlSharedCache::OnPendingFileSetForClient, |
| weak_factory_.GetWeakPtr(), client_id)); |
| } |
| } |
| |
| void SqlSharedCache::OnPendingFileSetForClient( |
| ClientId client_id, |
| base::expected<sqlite_vfs::PendingFileSet, |
| SqlSharedCacheIsolatedDatabase::Error> pending_file_set) { |
| if (!pending_file_set.has_value()) { |
| return; |
| } |
| auto* client = clients_.Lookup(client_id); |
| if (!client) { |
| return; |
| } |
| client->Initialize(std::move(*pending_file_set)); |
| } |
| |
| void SqlSharedCache::OnHashesLoaded( |
| base::expected<std::vector<uint32_t>, SqlSharedCacheIsolatedDatabase::Error> |
| hashes) { |
| if (!cached_hashes_.has_value()) { |
| cached_hashes_ = absl::flat_hash_set<uint32_t>(); |
| } |
| if (!hashes.has_value()) { |
| return; |
| } |
| for (auto hash : *hashes) { |
| cached_hashes_->insert(hash); |
| } |
| |
| if (!hashes->empty()) { |
| for (ClientsMap::iterator it(&clients_); !it.IsAtEnd(); it.Advance()) { |
| it.GetCurrentValue()->OnResourcesAdded(*hashes); |
| } |
| } |
| } |
| void SqlSharedCache::OnClientDisconnected( |
| ClientId client_id, |
| scoped_refptr<SqlSharedCacheHandle> handle) { |
| clients_.Remove(client_id); |
| } |
| |
| scoped_refptr<SqlSharedCacheHandle> SqlSharedCache::CreateHandle() { |
| return base::MakeRefCounted<SqlSharedCacheHandle>( |
| weak_factory_.GetWeakPtr(), base::PassKey<SqlSharedCache>()); |
| } |
| |
| void SqlSharedCache::IncrementHandleCount(base::PassKey<SqlSharedCacheHandle>) { |
| handle_count_++; |
| } |
| |
| void SqlSharedCache::DecrementHandleCount(base::PassKey<SqlSharedCacheHandle>) { |
| handle_count_--; |
| if (!IsReferenced()) { |
| on_unreferenced_callback_.Run(*this); |
| } |
| } |
| |
| void SqlSharedCache::CopyEntries( |
| base::queue<SqlPersistentStore::SharedCacheEligibleEntry> entries, |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, |
| base::OnceCallback<void( |
| base::queue<SqlPersistentStore::SharedCacheEligibleEntry>)> callback, |
| base::RepeatingCallback<void(const CacheEntryKey&)> |
| on_entry_copied_callback) { |
| CHECK(pending_copy_entries_.empty()); |
| CHECK(!copy_callback_); |
| CHECK(!current_copy_row_id_); |
| CHECK(!on_entry_copied_callback_); |
| CHECK(!entries.empty()); |
| CHECK(shared_cache_db_id_); |
| CHECK(isolated_database_); |
| pending_copy_entries_ = std::move(entries); |
| copy_abort_flag_ = std::move(abort_flag); |
| copy_callback_ = std::move(callback); |
| on_entry_copied_callback_ = std::move(on_entry_copied_callback); |
| CopyNextEntry(); |
| } |
| |
| void SqlSharedCache::CopyNextEntry() { |
| if (pending_copy_entries_.empty() || |
| (copy_abort_flag_ && |
| copy_abort_flag_->data.load(std::memory_order_relaxed))) { |
| FinishCopy(); |
| return; |
| } |
| auto entry = std::move(pending_copy_entries_.front()); |
| pending_copy_entries_.pop(); |
| current_entry_hash_ = base::PersistentHash(entry.url.spec()); |
| const auto key = entry.key; |
| store_->OpenEntry( |
| key, base::BindOnce(&SqlSharedCache::OnEntryOpenedForSharedCache, |
| weak_factory_.GetWeakPtr(), std::move(entry))); |
| } |
| |
| void SqlSharedCache::OnEntryOpenedForSharedCache( |
| SqlPersistentStore::SharedCacheEligibleEntry entry, |
| SqlPersistentStore::EntryInfoOrError result) { |
| if (!result.has_value() || !result->head) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| |
| // If the entry already has a `shared_cache_resource_id`, its blob data in the |
| // persistent store has already been moved to the shared cache. Attempting to |
| // copy it again would read zero-filled body data and create an all-zero |
| // duplicate entry in the isolated database. |
| if (result->shared_cache_resource_id) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| auto info = std::move(*result); |
| if (info.body_end > |
| net::features::kSqlDiskCacheMaxSharedCacheCopyEntrySize.Get()) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| net::HttpResponseInfo disk_response_info; |
| bool response_truncated = false; |
| if (!net::HttpCache::ParseResponseInfo(base::as_bytes(info.head->span()), |
| &disk_response_info, |
| &response_truncated) || |
| response_truncated || |
| disk_response_info.response_time != entry.response_info->response_time) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| auto pickled_buffer = base::MakeRefCounted<net::PickledIOBuffer>( |
| entry.response_info->MakePickle(/*skip_transient_headers=*/true, |
| /*response_truncated=*/false)); |
| if (info.body_end > 0) { |
| int64_t chunk_size = std::min( |
| static_cast<int64_t>( |
| net::features::kSqlDiskCacheSharedCacheReadBufferSize.Get()), |
| info.body_end); |
| auto buffer = base::MakeRefCounted<net::IOBufferWithSize>(chunk_size); |
| const auto key = entry.key; |
| const auto res_id = info.res_id; |
| const auto body_end = info.body_end; |
| store_->ReadEntryData( |
| key, res_id, 0, buffer, chunk_size, body_end, |
| /*sparse_reading=*/false, |
| base::BindOnce(&SqlSharedCache::OnEntryDataReadForInsert, |
| weak_factory_.GetWeakPtr(), std::move(entry), res_id, |
| std::move(pickled_buffer), body_end, buffer)); |
| return; |
| } |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert) |
| .WithArgs(entry.key, std::move(pickled_buffer), info.body_end, nullptr) |
| .Then(base::BindOnce(&SqlSharedCache::OnIsolatedDatabaseInserted, |
| weak_factory_.GetWeakPtr(), entry.key, info.res_id, |
| info.body_end, 0)); |
| } |
| |
| void SqlSharedCache::OnEntryDataReadForInsert( |
| SqlPersistentStore::SharedCacheEligibleEntry entry, |
| SqlPersistentStore::ResId res_id, |
| scoped_refptr<net::PickledIOBuffer> headers, |
| int64_t body_end, |
| scoped_refptr<net::IOBuffer> buffer, |
| base::expected<SqlPersistentStore::ReadResult, SqlPersistentStore::Error> |
| result) { |
| // For `sparse_reading = false`, `SqlPersistentStoreBackend::ReadEntryData` |
| // zero-fills gaps and returns `read_bytes` equal to the requested size |
| // as long as the read is within bounds. |
| if (!result.has_value() || |
| result.value().read_bytes != static_cast<int>(buffer->size())) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert) |
| .WithArgs(entry.key, std::move(headers), body_end, buffer) |
| .Then(base::BindOnce(&SqlSharedCache::OnIsolatedDatabaseInserted, |
| weak_factory_.GetWeakPtr(), entry.key, res_id, |
| body_end, buffer->size())); |
| } |
| |
| void SqlSharedCache::OnIsolatedDatabaseInserted( |
| CacheEntryKey key, |
| SqlPersistentStore::ResId res_id, |
| int64_t body_end, |
| int64_t offset, |
| base::expected<SqlSharedCacheRowId, SqlSharedCacheIsolatedDatabase::Error> |
| result) { |
| if (!result.has_value()) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| CHECK(!current_copy_row_id_); |
| current_copy_row_id_ = result.value(); |
| ReadNextChunk(std::move(key), res_id, body_end, offset, result.value()); |
| } |
| |
| void SqlSharedCache::ReadNextChunk(CacheEntryKey key, |
| SqlPersistentStore::ResId res_id, |
| int64_t body_end, |
| int64_t offset, |
| SqlSharedCacheRowId shared_cache_row_id) { |
| CHECK_LE(offset, body_end); |
| if (offset == body_end) { |
| MoveBlobsToSharedCache(key, res_id, shared_cache_row_id); |
| return; |
| } |
| int64_t chunk_size = |
| std::min(static_cast<int64_t>( |
| net::features::kSqlDiskCacheSharedCacheReadBufferSize.Get()), |
| body_end - offset); |
| auto buffer = base::MakeRefCounted<net::IOBufferWithSize>(chunk_size); |
| store_->ReadEntryData( |
| key, res_id, offset, buffer, chunk_size, body_end, |
| /*sparse_reading=*/false, |
| base::BindOnce(&SqlSharedCache::OnEntryDataRead, |
| weak_factory_.GetWeakPtr(), key, res_id, body_end, offset, |
| shared_cache_row_id, buffer)); |
| } |
| |
| void SqlSharedCache::OnEntryDataRead( |
| CacheEntryKey key, |
| SqlPersistentStore::ResId res_id, |
| int64_t body_end, |
| int64_t offset, |
| SqlSharedCacheRowId shared_cache_row_id, |
| scoped_refptr<net::IOBuffer> buffer, |
| base::expected<SqlPersistentStore::ReadResult, SqlPersistentStore::Error> |
| result) { |
| // For `sparse_reading = false`, `SqlPersistentStoreBackend::ReadEntryData` |
| // zero-fills gaps and returns `read_bytes` equal to the requested size |
| // as long as the read is within bounds. |
| if (!result.has_value() || result.value().read_bytes != buffer->size()) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| bool set_ready = (offset + buffer->size() == body_end); |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::WriteBody) |
| .WithArgs(key, shared_cache_row_id, offset, buffer, set_ready) |
| .Then(base::BindOnce(&SqlSharedCache::OnIsolatedDatabaseWritten, |
| weak_factory_.GetWeakPtr(), key, res_id, body_end, |
| offset + buffer->size(), shared_cache_row_id)); |
| } |
| |
| void SqlSharedCache::OnIsolatedDatabaseWritten( |
| CacheEntryKey key, |
| SqlPersistentStore::ResId res_id, |
| int64_t body_end, |
| int64_t next_offset, |
| SqlSharedCacheRowId shared_cache_row_id, |
| base::expected<void, SqlSharedCacheIsolatedDatabase::Error> result) { |
| if (!result.has_value()) { |
| OnCopyEntryFailed(); |
| return; |
| } |
| ReadNextChunk(std::move(key), res_id, body_end, next_offset, |
| shared_cache_row_id); |
| } |
| |
| void SqlSharedCache::MoveBlobsToSharedCache( |
| CacheEntryKey key, |
| SqlPersistentStore::ResId res_id, |
| SqlSharedCacheRowId shared_cache_row_id) { |
| store_->MoveBlobsToSharedCache( |
| key, res_id, {*shared_cache_db_id_, shared_cache_row_id}, |
| base::BindOnce( |
| [](base::WeakPtr<SqlSharedCache> self, CacheEntryKey key, |
| SqlPersistentStore::Error error) { |
| if (self) { |
| if (error == SqlPersistentStore::Error::kOk) { |
| self->OnCopyEntryComplete(key); |
| } else { |
| self->OnCopyEntryFailed(); |
| } |
| } |
| }, |
| weak_factory_.GetWeakPtr(), key)); |
| } |
| |
| void SqlSharedCache::OnCopyEntryComplete(const CacheEntryKey& key) { |
| CHECK(current_entry_hash_); |
| // `cached_hashes_` is guaranteed to be populated by `OnHashesLoaded` which |
| // was scheduled on the database task runner during `InitIsolatedDatabase` |
| // prior to any entry copy operations on the same sequence. |
| CHECK(cached_hashes_.has_value()); |
| if (cached_hashes_->insert(*current_entry_hash_).second) { |
| copy_new_hashes_.push_back(*current_entry_hash_); |
| } |
| current_entry_hash_.reset(); |
| current_copy_row_id_ = std::nullopt; |
| if (on_entry_copied_callback_) { |
| on_entry_copied_callback_.Run(key); |
| } |
| CopyNextEntry(); |
| } |
| |
| void SqlSharedCache::OnCopyEntryFailed() { |
| if (current_copy_row_id_.has_value()) { |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::DeleteEntry) |
| .WithArgs(*current_copy_row_id_); |
| current_copy_row_id_ = std::nullopt; |
| } |
| current_entry_hash_.reset(); |
| CopyNextEntry(); |
| } |
| |
| void SqlSharedCache::FinishCopy() { |
| if (!copy_new_hashes_.empty()) { |
| for (ClientsMap::iterator it(&clients_); !it.IsAtEnd(); it.Advance()) { |
| it.GetCurrentValue()->OnResourcesAdded(copy_new_hashes_); |
| } |
| copy_new_hashes_.clear(); |
| } |
| |
| CHECK(copy_callback_); |
| CHECK(!current_copy_row_id_); |
| on_entry_copied_callback_.Reset(); |
| auto unprocessed_entries = std::move(pending_copy_entries_); |
| CHECK(pending_copy_entries_.empty()); |
| copy_abort_flag_ = nullptr; |
| auto callback = std::move(copy_callback_); |
| CHECK(!copy_callback_); |
| std::move(callback).Run(std::move(unprocessed_entries)); |
| } |
| |
| void SqlSharedCache::DeleteEntries( |
| const std::vector<SqlSharedCacheRowId>& shared_cache_row_ids, |
| base::OnceCallback< |
| void(base::expected<void, SqlSharedCacheIsolatedDatabase::Error>)> |
| callback) { |
| if (!isolated_database_) { |
| std::move(callback).Run(base::unexpected( |
| SqlSharedCacheIsolatedDatabase::Error::kIsolatedDatabaseNotAvailable)); |
| return; |
| } |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::DeleteEntries) |
| .WithArgs(shared_cache_row_ids) |
| .Then(std::move(callback)); |
| } |
| |
| void SqlSharedCache::Read( |
| const CacheEntryKey& entry_key, |
| SqlSharedCacheRowId shared_cache_row_id, |
| int body_size, |
| int64_t offset, |
| scoped_refptr<net::IOBuffer> buffer, |
| SqlPersistentStore::ReadResultOrErrorCallback callback) { |
| if (!isolated_database_) { |
| std::move(callback).Run( |
| base::unexpected(SqlPersistentStore::Error::kNotFound)); |
| return; |
| } |
| if (offset > std::numeric_limits<int>::max()) { |
| std::move(callback).Run( |
| base::unexpected(SqlPersistentStore::Error::kFailedToExecute)); |
| return; |
| } |
| int offset_int = static_cast<int>(offset); |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read) |
| .WithArgs(entry_key, shared_cache_row_id, body_size, offset_int, buffer) |
| .Then(base::BindOnce(&SqlSharedCache::OnIsolatedDatabaseRead, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void SqlSharedCache::OnIsolatedDatabaseRead( |
| SqlPersistentStore::ReadResultOrErrorCallback callback, |
| SqlSharedCacheIsolatedDatabase::ReadResultOrError result) { |
| if (result.has_value()) { |
| std::move(callback).Run(result.value()); |
| } else { |
| SqlPersistentStore::Error store_error; |
| switch (result.error()) { |
| case SqlSharedCacheIsolatedDatabase::Error::kEntryNotFound: |
| store_error = SqlPersistentStore::Error::kNotFound; |
| break; |
| default: |
| store_error = SqlPersistentStore::Error::kFailedToExecute; |
| break; |
| } |
| std::move(callback).Run(base::unexpected(store_error)); |
| } |
| } |
| |
| void SqlSharedCache::GetBlobHandle( |
| const CacheEntryKey& entry_key, |
| SqlSharedCacheRowId shared_cache_row_id, |
| int body_size, |
| base::OnceCallback< |
| void(base::expected<scoped_refptr<SqlSharedCacheBlobHandle>, |
| SqlSharedCacheIsolatedDatabase::Error>)> callback) { |
| if (!isolated_database_) { |
| std::move(callback).Run(base::unexpected( |
| SqlSharedCacheIsolatedDatabase::Error::kIsolatedDatabaseNotAvailable)); |
| return; |
| } |
| isolated_database_.AsyncCall(&SqlSharedCacheIsolatedDatabase::GetBlobHandle) |
| .WithArgs(entry_key, shared_cache_row_id, body_size) |
| .Then(std::move(callback)); |
| } |
| |
| } // namespace disk_cache |