| // Copyright 2025 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #ifndef NET_DISK_CACHE_SQL_SQL_PERSISTENT_STORE_BACKEND_H_ | |
| #define NET_DISK_CACHE_SQL_SQL_PERSISTENT_STORE_BACKEND_H_ | |
| #include <atomic> | |
| #include "base/memory/ref_counted.h" | |
| #include "base/memory/weak_ptr.h" | |
| #include "base/types/expected.h" | |
| #include "net/disk_cache/sql/entry_write_buffer.h" | |
| #include "net/disk_cache/sql/eviction_candidate_aggregator.h" | |
| #include "net/disk_cache/sql/sql_persistent_store.h" | |
| #include "net/disk_cache/sql/sql_persistent_store_queries.h" | |
| #include "sql/database.h" | |
| #include "sql/meta_table.h" | |
| namespace sql { | |
| class Statement; | |
| class Transaction; | |
| } // namespace sql | |
| namespace disk_cache { | |
| class SqlReadCacheMemoryMonitor; | |
| // The `Backend` class encapsulates all direct interaction with the SQLite | |
| // database. It is designed to be owned by a `base::SequenceBound` and run on a | |
| // dedicated background sequence to avoid blocking the network IO thread. | |
| class NET_EXPORT_PRIVATE SqlPersistentStore::Backend { | |
| public: | |
| Backend(ShardId shard_id, | |
| const base::FilePath& path, | |
| net::CacheType type, | |
| bool shared_cache_enabled, | |
| scoped_refptr<SqlReadCacheMemoryMonitor> read_cache_memory_monitor); | |
| Backend(const Backend&) = delete; | |
| Backend& operator=(const Backend&) = delete; | |
| ~Backend(); | |
| // Initializes the database, including setting up the schema and reading | |
| // metadata. Returns the initialization result on success. | |
| InitResultOrError Initialize(int64_t user_max_bytes, | |
| base::TimeTicks start_time); | |
| int32_t GetEntryCount() const; | |
| EntryInfoOrErrorAndStoreStatus OpenOrCreateEntry(const CacheEntryKey& key, | |
| base::TimeTicks start_time); | |
| EntryInfoOrError OpenEntry(const CacheEntryKey& key, | |
| base::TimeTicks start_time); | |
| EntryInfoOrErrorAndStoreStatus CreateEntry(const CacheEntryKey& key, | |
| base::Time creation_time, | |
| bool run_existance_check, | |
| base::TimeTicks start_time); | |
| ErrorAndStoreStatus DoomEntry(const CacheEntryKey& key, | |
| ResId res_id, | |
| base::TimeTicks start_time); | |
| DeletedSharedCacheResourceOrError DeleteDoomedEntry( | |
| const CacheEntryKey& key, | |
| ResId res_id, | |
| base::TimeTicks start_time); | |
| DeletedSharedCacheResourcesOrError DeleteDoomedEntries( | |
| ResIdList res_ids_to_delete, | |
| base::TimeTicks start_time); | |
| DeleteLiveEntryResultOrErrorAndStoreStatus DeleteLiveEntry( | |
| const CacheEntryKey& key, | |
| base::TimeTicks start_time); | |
| ErrorAndStoreStatus DeleteAllEntries(base::TimeTicks start_time); | |
| DeleteLiveEntryResultOrErrorAndStoreStatus DeleteLiveEntriesBetween( | |
| base::Time initial_time, | |
| base::Time end_time, | |
| base::flat_set<ResId> excluded_res_ids, | |
| base::TimeTicks start_time); | |
| EntryMetadataOrError UpdateEntryLastUsedByKey(const CacheEntryKey& key, | |
| base::Time last_used, | |
| base::TimeTicks start_time); | |
| EntryMetadataOrErrorAndStoreStatus WriteEntryDataAndMetadata( | |
| const CacheEntryKey& key, | |
| std::optional<ResId> res_id, | |
| std::optional<int64_t> old_body_end, | |
| EntryWriteBuffer buffer, | |
| base::Time last_used, | |
| const std::optional<MemoryEntryDataHints>& new_hints, | |
| scoped_refptr<net::IOBuffer> head_buffer, | |
| int64_t header_size_delta, | |
| bool doomed_new_entry, | |
| base::TimeTicks start_time); | |
| EntryMetadataOrErrorAndStoreStatus WriteEntryData( | |
| const CacheEntryKey& key, | |
| const ResIdOrTime& res_id_or_last_used_time, | |
| int64_t old_body_end, | |
| EntryWriteBuffer buffer, | |
| bool truncate, | |
| bool doomed_new_entry, | |
| bool sparse_write, | |
| int64_t header_size, | |
| int64_t max_sparse_data_size, | |
| base::TimeTicks start_time); | |
| ReadResultOrError ReadEntryData(const CacheEntryKey& key, | |
| ResId res_id, | |
| int64_t offset, | |
| scoped_refptr<net::IOBuffer> buffer, | |
| int buf_len, | |
| int64_t body_end, | |
| bool sparse_reading, | |
| base::TimeTicks start_time); | |
| ErrorAndStoreStatus MoveBlobsToSharedCache( | |
| const CacheEntryKey& key, | |
| ResId res_id, | |
| SqlSharedCacheResourceId shared_cache_resource_id, | |
| base::TimeTicks start_time); | |
| RangeResult GetEntryAvailableRange(ResId res_id, | |
| int64_t offset, | |
| int len, | |
| base::TimeTicks start_time); | |
| Int64OrError CalculateSizeOfEntriesBetween(base::Time initial_time, | |
| base::Time end_time, | |
| base::TimeTicks start_time); | |
| OptionalEntryInfoWithKeyAndIterator OpenNextEntry( | |
| const EntryIterator& iterator, | |
| base::TimeTicks start_time); | |
| // Starts the eviction process. | |
| // | |
| // The process begins by selecting eviction candidates from the database. | |
| // Then, `aggregator->OnCandidate()` is called to aggregate candidates from | |
| // all shards. Finally, `EvictEntries()` is called to delete the selected | |
| // entries. | |
| // | |
| // `size_to_be_removed`: The target size to be removed from this shard. This | |
| // is used to select candidates. | |
| // `excluded_res_ids`: A set of resource IDs to exclude from eviction (e.g., | |
| // currently active entries). | |
| // `is_idle_time_eviction`: True if this is an eviction triggered by idle | |
| // time. If true, the eviction may be aborted if the | |
| // browser becomes active. | |
| // `aggregator`: The aggregator used to collect and select candidates across | |
| // all shards. | |
| // `abort_flag`: A flag used to signal an abort request. If set to true, the | |
| // eviction process will stop after the mandatory size has been | |
| // removed. | |
| // `remaining_mandatory_size`: The remaining size that *must* be evicted even | |
| // if `abort_flag` is set. This is typically the | |
| // amount needed to bring the cache size below the | |
| // high watermark. Once this value becomes <= 0, | |
| // and `abort_flag` is set, the eviction will | |
| // stop. | |
| // `index`: The in-memory index to be updated. | |
| // `callback`: Called when the eviction finishes or is aborted. | |
| void StartEviction( | |
| int64_t size_to_be_removed, | |
| base::flat_set<ResId> excluded_res_ids, | |
| bool is_idle_time_eviction, | |
| scoped_refptr<EvictionCandidateAggregator> aggregator, | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, | |
| scoped_refptr<base::RefCountedData<std::atomic_int64_t>> | |
| remaining_mandatory_size, | |
| std::optional<SqlPersistentStoreInMemoryIndex> index, | |
| EvictionResultWithMetadataCallback callback); | |
| // Resumes a previously paused eviction. | |
| // | |
| // This method continues evicting entries from `eviction_targets` that were | |
| // left over from a previous `StartEviction` or `ResumePendingEviction` call | |
| // that was aborted. | |
| // | |
| // `eviction_targets`: The queue of eviction targets remaining from the | |
| // previous attempt. | |
| // `excluded_res_ids`: A set of resource IDs to exclude from eviction. | |
| // `is_idle_time_eviction`: See `StartEviction`. | |
| // `abort_flag`: See `StartEviction`. | |
| // `remaining_mandatory_size`: See `StartEviction`. | |
| // `index`: The in-memory index to be updated. | |
| // `start_time`: The time when the resume operation was posted. | |
| EvictionResultWithMetadata ResumePendingEviction( | |
| EvictionTargetQueue eviction_targets, | |
| base::flat_set<ResId> excluded_res_ids, | |
| bool is_idle_time_eviction, | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, | |
| scoped_refptr<base::RefCountedData<std::atomic_int64_t>> | |
| remaining_mandatory_size, | |
| std::optional<SqlPersistentStoreInMemoryIndex> index, | |
| base::TimeTicks start_time); | |
| InMemoryIndexAndDoomedResIdsOrError LoadInMemoryIndex(); | |
| bool MaybeRunCheckpoint(); | |
| bool MaybeRunIncrementalVacuum( | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag); | |
| // Closes the database. | |
| void Close(); | |
| void EnableStrictCorruptionCheckForTesting() { | |
| strict_corruption_check_enabled_ = true; | |
| } | |
| void SetSimulateDbFailureForTesting(bool fail) { | |
| simulate_db_failure_for_testing_ = fail; | |
| } | |
| void RazeAndPoisonForTesting() { | |
| db_.RazeAndPoison(); | |
| store_status_ = StoreStatus(); | |
| } | |
| void SetEvictionHookForTesting(base::RepeatingClosure hook) { | |
| eviction_hook_ = std::move(hook); | |
| } | |
| private: | |
| using RangeResultOrError = base::expected<RangeResult, Error>; | |
| using OptionalEntryInfoWithKeyAndIteratorOrError = | |
| base::expected<OptionalEntryInfoWithKeyAndIterator, Error>; | |
| using EvictionCandidateList = | |
| EvictionCandidateAggregator::EvictionCandidateList; | |
| using EvictionTargetQueue = SqlPersistentStore::EvictionTargetQueue; | |
| // A helper struct to associate an IOBuffer with a starting offset. | |
| struct BufferWithStart { | |
| BufferWithStart(scoped_refptr<net::IOBuffer> buffer, int64_t start); | |
| ~BufferWithStart(); | |
| BufferWithStart(BufferWithStart&& other); | |
| BufferWithStart& operator=(BufferWithStart&& other); | |
| scoped_refptr<net::IOBuffer> buffer; | |
| int64_t start; | |
| }; | |
| struct UpdateResourceResult { | |
| bool doomed; | |
| int64_t bytes_usage; | |
| base::Time last_used; | |
| }; | |
| // A helper function to record the time delay from posting a task to its | |
| // execution. | |
| void RecordPostingDelay(std::string_view method_name, | |
| base::TimeDelta posting_delay); | |
| // Records timing and result histograms for a backend method. This logs the | |
| // method's duration to ".SuccessTime" or ".FailureTime" histograms and the | |
| // `Error` code to a ".Result" histogram. | |
| void RecordTimeAndErrorResultHistogram(std::string_view method_name, | |
| base::TimeDelta posting_delay, | |
| base::TimeDelta time_delta, | |
| Error error, | |
| bool corruption_detected); | |
| void DatabaseErrorCallback(int error, sql::Statement* statement); | |
| Error InitializeInternal(bool& corruption_detected); | |
| EntryInfoOrError OpenOrCreateEntryInternal(const CacheEntryKey& key, | |
| bool& corruption_detected); | |
| EntryInfoOrError OpenEntryInternal(const CacheEntryKey& key); | |
| EntryInfoOrError CreateEntryInternal(const CacheEntryKey& key, | |
| base::Time creation_time, | |
| bool run_existance_check, | |
| bool& corruption_detected); | |
| Error DoomEntryInternal(const CacheEntryKey& key, | |
| ResId res_id, | |
| bool& corruption_detected); | |
| DeletedSharedCacheResourceOrError DeleteDoomedEntryInternal(ResId res_id); | |
| DeletedSharedCacheResourcesOrError DeleteDoomedEntriesInternal( | |
| const ResIdList& res_ids_to_delete, | |
| bool& corruption_detected); | |
| DeleteLiveEntryResultOrError DeleteLiveEntryInternal( | |
| const CacheEntryKey& key, | |
| bool& corruption_detected); | |
| Error DeleteAllEntriesInternal(bool& corruption_detected); | |
| DeleteLiveEntryResultOrError DeleteLiveEntriesBetweenInternal( | |
| base::Time initial_time, | |
| base::Time end_time, | |
| const base::flat_set<ResId>& excluded_res_ids, | |
| bool& corruption_detected); | |
| base::expected<UpdateResourceResult, Error> UpdateResourceForWriteEntry( | |
| ResId res_id, | |
| int64_t body_end_delta, | |
| int64_t total_size_delta, | |
| int64_t expected_new_body_end, | |
| bool& corruption_detected); | |
| EntryMetadataOrError UpdateEntryLastUsedByKeyInternal( | |
| const CacheEntryKey& key, | |
| base::Time last_used); | |
| Error WriteEntryBodyDataHelper( | |
| const CacheEntryKey& key, | |
| ResId res_id, | |
| int64_t old_body_end, | |
| EntryWriteBuffer buffer, | |
| bool truncate, | |
| int64_t& body_end_delta, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta, | |
| int64_t& new_body_end, | |
| bool& corruption_detected); | |
| EntryMetadataOrError WriteEntryDataAndMetadataInternal( | |
| const CacheEntryKey& key, | |
| std::optional<ResId> res_id, | |
| std::optional<int64_t> old_body_end, | |
| EntryWriteBuffer buffer, | |
| base::Time last_used, | |
| const std::optional<MemoryEntryDataHints>& new_hints, | |
| scoped_refptr<net::IOBuffer> head_buffer, | |
| int64_t header_size_delta, | |
| bool doomed_new_entry, | |
| bool& corruption_detected); | |
| EntryMetadataOrError WriteEntryDataInternal( | |
| const CacheEntryKey& key, | |
| const ResIdOrTime& res_id_or_last_used_time, | |
| int64_t old_body_end, | |
| EntryWriteBuffer buffer, | |
| bool truncate, | |
| bool doomed_new_entry, | |
| bool sparse_write, | |
| int64_t header_size, | |
| int64_t max_sparse_data_size, | |
| bool& corruption_detected); | |
| ReadResultOrError ReadEntryDataInternal(const CacheEntryKey& key, | |
| ResId res_id, | |
| int64_t offset, | |
| scoped_refptr<net::IOBuffer> buffer, | |
| int buf_len, | |
| int64_t body_end, | |
| bool sparse_reading, | |
| bool& corruption_detected); | |
| RangeResultOrError GetEntryAvailableRangeInternal(ResId res_id, | |
| int64_t offset, | |
| int len); | |
| Int64OrError CalculateSizeOfEntriesBetweenInternal(base::Time initial_time, | |
| base::Time end_time); | |
| OptionalEntryInfoWithKeyAndIteratorOrError OpenNextEntryInternal( | |
| const EntryIterator& iterator, | |
| bool& corruption_detected); | |
| InMemoryIndexAndDoomedResIdsOrError LoadInMemoryIndexInternal(); | |
| // Trims blobs that overlap with the new write range [offset, end), and | |
| // updates the total size delta. | |
| Error TrimOverlappingBlobs( | |
| const CacheEntryKey& key, | |
| ResId res_id, | |
| int64_t offset, | |
| int64_t end, | |
| bool truncate, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta, | |
| bool& corruption_detected); | |
| // Truncates data by deleting all blobs that start at or after the given | |
| // offset. | |
| Error TruncateBlobsAfter( | |
| ResId res_id, | |
| int64_t truncate_offset, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta); | |
| // Inserts a vector of new blobs into the database, and updates the total size | |
| // delta. | |
| Error InsertNewBlobs(const CacheEntryKey& key, | |
| ResId res_id, | |
| const std::vector<BufferWithStart>& new_blobs, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta); | |
| // Inserts a single new blob into the database, and updates the total size | |
| // delta. | |
| Error InsertNewBlob(const CacheEntryKey& key, | |
| ResId res_id, | |
| int64_t start, | |
| const scoped_refptr<net::IOBuffer>& buffer, | |
| int buf_len, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta); | |
| // Deletes blobs by their IDs, and updates the total size delta. | |
| Error DeleteBlobsById(const std::vector<int64_t>& blob_ids_to_be_removed, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta, | |
| bool& corruption_detected); | |
| // Deletes a single blob by its ID, and updates the total size delta. | |
| Error DeleteBlobById(int64_t blob_id, | |
| base::CheckedNumeric<int64_t>& checked_total_size_delta, | |
| bool& corruption_detected); | |
| // Deletes all blobs associated with a given res_id. | |
| Error DeleteBlobsByResId(ResId res_id); | |
| // Deletes multiple blobs from the `blobs` table by their `res_id`s. | |
| Error DeleteBlobsByResIds(const ResIdList& res_ids); | |
| Error DeleteBlobsByResIds(const HashAndResIdList& hash_and_res_ids); | |
| // Deletes a single resource entry from the `resources` table by its `res_id`. | |
| DeletedSharedCacheResourceOrError DeleteResourceByResId(ResId res_id); | |
| // Deletes a single resource entry from the `resources` table by its `res_id` | |
| // and returns the `cache_key_hash` of the deleted entry. | |
| HashAndSharedCacheResourceOrError DeleteResourceByResIdReturnHash( | |
| ResId res_id); | |
| // Deletes a single live resource entry from the `resources` table by its | |
| // `res_id` and returns the `bytes_usage` and `cache_key_hash` of the deleted | |
| // entry. | |
| UsageAndHashOrError DeleteLiveResourceByResIdReturnUsageAndHash(ResId res_id); | |
| // Deletes multiple resource entries from the `resources` table by their | |
| // `res_id`s. | |
| DeletedSharedCacheResourcesOrError DeleteResourcesByResIds( | |
| const ResIdList& res_ids); | |
| // Selects a list of eviction candidates from the `resources` table. | |
| // Entries in `high_priority_res_ids` are less likely to be selected as | |
| // candidates if prioritized caching is enabled. | |
| base::expected<EvictionCandidateList, Error> SelectEvictionCandidates( | |
| int64_t size_to_be_removed, | |
| base::flat_set<ResId> excluded_res_ids, | |
| std::optional<SqlPersistentStoreInMemoryIndex>& index, | |
| bool is_idle_time_eviction, | |
| size_t& scanned_count, | |
| bool& used_in_memory_index); | |
| // Called by the `EvictionCandidateAggregator` to evict a list of selected | |
| // entries. | |
| void EvictEntries( | |
| EvictionResultWithMetadataCallback callback, | |
| bool is_idle_time_eviction, | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, | |
| scoped_refptr<base::RefCountedData<std::atomic_int64_t>> | |
| remaining_mandatory_size, | |
| std::optional<SqlPersistentStoreInMemoryIndex> index, | |
| EvictionTargetQueue eviction_targets, | |
| base::TimeTicks post_task_time); | |
| // A helper function to evict entries. | |
| // `trust_target_size`: If true, it assumes the entry exists and uses the size | |
| // from `eviction_targets` (used for new eviction). If false, entries that are | |
| // not found in the DB are ignored, and the size is retrieved from the DB | |
| // (used for resuming eviction). | |
| Error EvictEntriesHelper( | |
| EvictionTargetQueue& eviction_targets, | |
| const base::flat_set<ResId>& excluded_res_ids, | |
| bool is_idle_time_eviction, | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, | |
| scoped_refptr<base::RefCountedData<std::atomic_int64_t>> | |
| remaining_mandatory_size, | |
| bool trust_target_size, | |
| bool& corruption_detected, | |
| bool& index_mismatch_detected, | |
| size_t& evicted_entry_count, | |
| std::vector<SqlSharedCacheResourceId>& deleted_shared_resources, | |
| std::optional<SqlPersistentStoreInMemoryIndex>& index); | |
| // Updates the in-memory `store_status_` by `entry_count_delta` and | |
| // `total_size_delta`. If the update results in an overflow or a negative | |
| // value, it recalculates the correct value from the database to recover from | |
| // potential metadata corruption. | |
| // It then updates the meta table values and attempts to commit the | |
| // `transaction`. | |
| // Returns Error::kOk on success, or an error code on failure. | |
| Error UpdateStoreStatusAndCommitTransaction(sql::Transaction& transaction, | |
| int64_t entry_count_delta, | |
| int64_t total_size_delta, | |
| bool& corruption_detected); | |
| // Recalculates the store's status (entry count and total size) directly from | |
| // the database. This is a recovery mechanism used when metadata might be | |
| // inconsistent, e.g., after a numerical overflow. | |
| // Returns Error::kOk on success, or an error code on failure. | |
| Error RecalculateStoreStatusAndCommitTransaction( | |
| sql::Transaction& transaction); | |
| int64_t CalculateResourceEntryCount(); | |
| int64_t CalculateTotalSize(); | |
| // Checks the database status. Returns Error::kOk on success, or an error | |
| // code if something is wrong. | |
| Error CheckDatabaseStatus(); | |
| // Checks or initializes the `shared_cache_enabled` metadata entry in the | |
| // meta table. For new databases, writes the current `shared_cache_enabled_` | |
| // value. For existing databases, verifies that the recorded value matches | |
| // `shared_cache_enabled_` (returning Error::kSharedCacheEnabledMismatch on | |
| // mismatch). Legacy databases without the key are treated as disabled. | |
| Error CheckOrInitializeSharedCacheEnabledMetadata(bool is_new_db); | |
| void MaybeCrashIfCorrupted(bool corruption_detected); | |
| void OnCommitCallback(int pages); | |
| int GetFreelistCount(); | |
| Error MaybeRunIncrementalVacuumInternal( | |
| scoped_refptr<base::RefCountedData<std::atomic_bool>> abort_flag, | |
| int& pages_vacuumed); | |
| Error MoveBlobsToSharedCacheInternal( | |
| ResId res_id, | |
| SqlSharedCacheResourceId shared_cache_resource_id); | |
| base::FilePath GetDatabaseFilePath() const; | |
| base::cstring_view GetQuery(disk_cache_sql_queries::Query query) const { | |
| return disk_cache_sql_queries::GetQuery(query, shared_cache_enabled_); | |
| } | |
| const ShardId shard_id_; | |
| const base::FilePath path_; | |
| const net::CacheType type_; | |
| const bool shared_cache_enabled_; | |
| const scoped_refptr<SqlReadCacheMemoryMonitor> read_cache_memory_monitor_; | |
| // Cached value of `net::features::kSqlDiskCacheReduceUma`. | |
| const bool reduce_uma_; | |
| sql::Database db_; | |
| sql::MetaTable meta_table_; | |
| std::optional<Error> db_init_status_; | |
| StoreStatus store_status_; | |
| bool strict_corruption_check_enabled_ = false; | |
| bool simulate_db_failure_for_testing_ = false; | |
| // The number of pages in the write-ahead log file. This is updated by | |
| // `OnCommitCallback` and reset to 0 after a checkpoint. | |
| int wal_pages_ = 0; | |
| bool incremental_vacuum_enabled_ = false; | |
| SEQUENCE_CHECKER(sequence_checker_); | |
| // A hook called during eviction for testing purposes. | |
| base::RepeatingClosure eviction_hook_; | |
| base::WeakPtrFactory<Backend> weak_factory_{this}; | |
| }; | |
| } // namespace disk_cache | |
| #endif // NET_DISK_CACHE_SQL_SQL_PERSISTENT_STORE_BACKEND_H_ |