| // 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. |
| |
| #include "net/disk_cache/sql/sql_backend_impl.h" |
| |
| #include <cstdint> |
| #include <variant> |
| |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/hash/hash.h" |
| #include "base/location.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/pickle.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/run_until.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_file_util.h" |
| #include "base/test/test_future.h" |
| #include "components/performance_manager/scenario_api/performance_scenario_test_support.h" |
| #include "net/base/features.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/network_isolation_key.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/base/test_completion_callback.h" |
| #include "net/disk_cache/backend_cleanup_tracker.h" |
| #include "net/disk_cache/disk_cache_test_util.h" |
| #include "net/disk_cache/sql/mock_shared_cache_client_remote.h" |
| #include "net/disk_cache/sql/sql_async_task_manager.h" |
| #include "net/disk_cache/sql/sql_backend_constants.h" |
| #include "net/disk_cache/sql/sql_entry_impl.h" |
| #include "net/disk_cache/sql/sql_shared_cache.h" |
| #include "net/disk_cache/sql/sql_shared_cache_handle.h" |
| #include "net/disk_cache/sql/sql_shared_cache_isolated_database_reader.h" |
| #include "net/disk_cache/sql/sql_shared_cache_manager.h" |
| #include "net/http/http_cache.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_response_info.h" |
| #include "net/test/gtest_util.h" |
| #include "sql/database.h" |
| #include "sql/statement.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| using net::test::IsError; |
| using net::test::IsOk; |
| |
| using performance_scenarios::InputScenario; |
| using performance_scenarios::LoadingScenario; |
| using performance_scenarios::PerformanceScenarioTestHelper; |
| using performance_scenarios::ScenarioScope; |
| |
| namespace disk_cache { |
| |
| using OperationHandle = ExclusiveOperationCoordinator::OperationHandle; |
| |
| namespace { |
| |
| using testing::ElementsAre; |
| using testing::Pair; |
| using FakeIndexFileError = SqlBackendImpl::FakeIndexFileError; |
| |
| // Default max cache size for tests, 10 MB. |
| inline constexpr int64_t kDefaultMaxBytes = 10 * 1024 * 1024; |
| |
| // Helper to create a new entry and write a data. |
| Entry* CreateEntryAndWriteData(SqlBackendImpl* backend, |
| const std::string& key, |
| const std::string& data) { |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(key, net::HIGHEST, cb_create.callback())); |
| CHECK_EQ(create_result.net_error(), net::OK); |
| auto* entry = create_result.ReleaseEntry(); |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| static_cast<int>(buffer->size())); |
| return entry; |
| } |
| |
| // Helper to read data and verify its content. |
| void ReadAndVerifyData(Entry* entry, std::string_view expected_data) { |
| auto read_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(expected_data.size() + 1); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(expected_data.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), expected_data.size()), |
| expected_data); |
| } |
| |
| size_t GetShardCount() { |
| return std::max(std::min(net::features::kSqlDiskCacheShardCount.Get(), 255), |
| 1); |
| } |
| std::string GetExpectedFakeIndexContents() { |
| base::FieldTrial* backend_field_trial = |
| net::features::kDiskCacheBackendResetCacheOnGroupChange.Get() |
| ? base::FeatureList::GetFieldTrial( |
| net::features::kDiskCacheBackendExperiment) |
| : nullptr; |
| return base::StrCat( |
| {kSqlBackendFakeIndexPrefix, |
| net::features::kSqlDiskCacheWalMode.Get() ? "Wal" : "Truncate", |
| base::NumberToString(GetShardCount()), |
| backend_field_trial ? backend_field_trial->group_name() : ""}); |
| } |
| |
| class SqlBackendImplTest : public testing::Test { |
| public: |
| SqlBackendImplTest() = default; |
| ~SqlBackendImplTest() override = default; |
| |
| // Sets up a temporary directory and a background task runner for each test. |
| void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } |
| |
| protected: |
| void RunSparseDataExceedsMaxFileSizeTest(bool doom_entry); |
| |
| std::unique_ptr<SqlBackendImpl> CreateBackend( |
| scoped_refptr<BackendCleanupTracker> cleanup_tracker = nullptr) { |
| return std::make_unique<SqlBackendImpl>( |
| temp_dir_.GetPath(), kDefaultMaxBytes, net::CacheType::DISK_CACHE, |
| std::move(cleanup_tracker)); |
| } |
| |
| void WaitForCleanup(scoped_refptr<BackendCleanupTracker> cleanup_tracker) { |
| CHECK(cleanup_tracker); |
| base::RunLoop run_loop; |
| cleanup_tracker->AddPostCleanupCallback(run_loop.QuitClosure()); |
| cleanup_tracker = nullptr; |
| run_loop.Run(); |
| } |
| |
| std::unique_ptr<SqlBackendImpl> CreateBackendAndInit( |
| int64_t max_bytes = kDefaultMaxBytes) { |
| auto backend = std::make_unique<SqlBackendImpl>( |
| temp_dir_.GetPath(), max_bytes, net::CacheType::DISK_CACHE, |
| /*cleanup_tracker=*/nullptr); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| CHECK_EQ(future.Get(), net::OK); |
| return backend; |
| } |
| void WaitUntilInitialized(SqlBackendImpl& backend, |
| const scoped_refptr<EntryDbHandle>& db_handle) { |
| CHECK(db_handle); |
| while (!db_handle->IsFinished()) { |
| backend.RunUntilAllTasksCompleteForTest(); |
| } |
| } |
| |
| void FlushQueueInTaskRunners( |
| const std::vector<scoped_refptr<base::SequencedTaskRunner>>& |
| task_runners) { |
| for (auto& runner : task_runners) { |
| base::RunLoop run_loop; |
| runner->PostTask(FROM_HERE, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| } |
| |
| bool LoadInMemoryIndex(SqlBackendImpl& backend) { |
| auto* store = backend.GetSqlStoreForTest(); |
| base::test::TestFuture<SqlPersistentStore::Error> future; |
| store->MaybeLoadInMemoryIndex(future.GetCallback()); |
| return future.Get() == SqlPersistentStore::Error::kOk; |
| } |
| |
| SqlSharedCacheResourceId CreateEntryInSharedCache(SqlBackendImpl& backend, |
| std::string_view key, |
| std::string_view data) { |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| cb_create.GetResult(backend.CreateEntry(std::string(key), net::HIGHEST, |
| cb_create.callback())); |
| EXPECT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| CHECK(entry); |
| |
| net::HttpResponseInfo response_info; |
| response_info.response_time = base::Time::Now(); |
| response_info.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| auto pickle = response_info.MakePickle(false, false); |
| std::string pickle_data(reinterpret_cast<const char*>(pickle->data()), |
| pickle->size()); |
| auto pickle_buffer = base::MakeRefCounted<net::StringIOBuffer>(pickle_data); |
| net::TestCompletionCallback cb_write_pickle; |
| EXPECT_EQ(cb_write_pickle.GetResult( |
| entry->WriteData(0, 0, pickle_buffer.get(), pickle->size(), |
| cb_write_pickle.callback(), false)), |
| static_cast<int>(pickle->size())); |
| |
| auto write_buf = base::MakeRefCounted<net::IOBufferWithSize>(data.size()); |
| write_buf->span().copy_from(base::as_byte_span(data)); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 0, write_buf.get(), data.size(), |
| cb_write.callback(), false)), |
| static_cast<int>(data.size())); |
| entry->Close(); |
| backend.RunUntilAllTasksCompleteForTest(); |
| |
| GURL url("https://example.com"); |
| net::SchemefulSite site(url); |
| net::NetworkIsolationKey nik(site, site); |
| backend.OnEntryEligibleForSharedCache( |
| std::string(key), url, |
| std::make_unique<net::HttpResponseInfo>(response_info), nik); |
| |
| base::RunLoop process_run_loop; |
| backend.ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| base::NullCallback()); |
| process_run_loop.Run(); |
| backend.RunUntilAllTasksCompleteForTest(); |
| |
| CacheEntryKey cache_key{std::string(key)}; |
| auto* store = backend.GetSqlStoreForTest(); |
| base::test::TestFuture<SqlPersistentStore::EntryInfoOrError> open_future; |
| store->OpenEntry(cache_key, open_future.GetCallback()); |
| auto open_res = open_future.Take(); |
| CHECK(open_res.has_value()); |
| return open_res->shared_cache_resource_id.value_or( |
| SqlSharedCacheResourceId{}); |
| } |
| |
| // Gets the total size of all entries. |
| int64_t GetSizeOfAllEntries(SqlBackendImpl& backend) { |
| return backend.GetSqlStoreForTest()->GetSizeOfAllEntries(); |
| } |
| |
| // Opens the database for a specific shard and returns the count of blobs |
| // associated with a given resource ID. |
| int64_t OpenDatabaseAndGetBlobsCount(SqlPersistentStore::ShardId shard_id, |
| SqlPersistentStore::ResId res_id) { |
| auto db = std::make_unique<sql::Database>( |
| sql::DatabaseOptions() |
| #if BUILDFLAG(IS_WIN) |
| .set_exclusive_database_file_lock(true) |
| #endif // IS_WIN |
| .set_wal_mode(true), |
| sql::Database::Tag("HttpCacheDiskCache")); |
| CHECK(db->Open(temp_dir_.GetPath().AppendASCII( |
| base::StrCat({kSqlBackendDatabaseFileNamePrefix, |
| base::NumberToString(shard_id.value())})))); |
| sql::Statement s( |
| db->GetUniqueStatement("SELECT COUNT(*) FROM blobs where res_id = ?")); |
| s.BindInt64(0, res_id.value()); |
| CHECK(s.Step()); |
| return s.ColumnInt64(0); |
| } |
| |
| void RunDelayedPostInitializationTasksTest(); |
| |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| base::ScopedTempDir temp_dir_; |
| }; |
| |
| TEST_F(SqlBackendImplTest, InitWithNoFakeIndexFile) { |
| const std::string expected_contents = GetExpectedFakeIndexContents(); |
| base::HistogramTester histogram_tester; |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kOkNew, 1); |
| |
| base::FilePath file_path = |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName); |
| const std::optional<int64_t> file_size = base::GetFileSize(file_path); |
| ASSERT_TRUE(file_size.has_value()); |
| EXPECT_EQ(*file_size, expected_contents.size()); |
| std::vector<uint8_t> contents(expected_contents.size()); |
| ASSERT_TRUE(base::ReadFile(file_path, contents)); |
| EXPECT_EQ(contents, base::as_byte_span(expected_contents)); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithFakeIndexFile) { |
| const std::string expected_contents = GetExpectedFakeIndexContents(); |
| base::HistogramTester histogram_tester; |
| base::FilePath file_path = |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName); |
| ASSERT_TRUE( |
| base::WriteFile(file_path, base::as_byte_span(expected_contents))); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kOkExisting, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, ExperimentGroupChangeResetsCacheWhenParamSet) { |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitFromCommandLine( |
| "DiskCacheBackendExperiment<DiskCacheBackendExperiment.GroupA:" |
| "DiskCacheBackendResetCacheOnGroupChange/true", |
| "FeatureParamWithCache"); |
| |
| auto cleanup_tracker = BackendCleanupTracker::TryCreate(temp_dir_.GetPath(), |
| base::DoNothing()); |
| CHECK(cleanup_tracker); |
| |
| // Create and init backend. This should create fake index with "GroupA". |
| auto backend = CreateBackend(cleanup_tracker); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| |
| backend.reset(); |
| WaitForCleanup(std::move(cleanup_tracker)); |
| } |
| |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitFromCommandLine( |
| "DiskCacheBackendExperiment<DiskCacheBackendExperiment.GroupB:" |
| "DiskCacheBackendResetCacheOnGroupChange/true", |
| "FeatureParamWithCache"); |
| |
| // Initialize backend on same directory. It should fail because the group |
| // changed and reset on group change is enabled. |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| EXPECT_EQ(future.Get(), net::ERR_FAILED); |
| |
| // "GroupA" and "GroupB" have same length, so it should be |
| // kWrongMagicNumber. |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kWrongMagicNumber, |
| 1); |
| } |
| } |
| |
| TEST_F(SqlBackendImplTest, |
| ExperimentGroupChangeDoesNotResetCacheWhenParamNotSet) { |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitFromCommandLine( |
| "DiskCacheBackendExperiment<TrialC.GroupA:dummy/1", |
| "FeatureParamWithCache"); |
| |
| auto cleanup_tracker = BackendCleanupTracker::TryCreate(temp_dir_.GetPath(), |
| base::DoNothing()); |
| CHECK(cleanup_tracker); |
| |
| // Create and init backend. |
| auto backend = CreateBackend(cleanup_tracker); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| |
| backend.reset(); |
| WaitForCleanup(std::move(cleanup_tracker)); |
| } |
| |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitFromCommandLine( |
| "DiskCacheBackendExperiment<TrialD.GroupB:dummy/1", |
| "FeatureParamWithCache"); |
| |
| // Initialize backend on same directory. It should succeed because reset on |
| // group change is disabled by default. |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| EXPECT_EQ(future.Get(), net::OK); |
| |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kOkExisting, 1); |
| } |
| } |
| |
| TEST_F(SqlBackendImplTest, WalModeChangeResetsCache) { |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{"SqlDiskCacheWalMode", "false"}}); |
| |
| auto cleanup_tracker = BackendCleanupTracker::TryCreate(temp_dir_.GetPath(), |
| base::DoNothing()); |
| CHECK(cleanup_tracker); |
| |
| auto backend = CreateBackend(cleanup_tracker); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| |
| backend.reset(); |
| WaitForCleanup(std::move(cleanup_tracker)); |
| } |
| |
| { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{"SqlDiskCacheWalMode", "true"}}); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| EXPECT_EQ(future.Get(), net::ERR_FAILED); |
| |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kWrongFileSize, 1); |
| } |
| } |
| |
| TEST_F(SqlBackendImplTest, SerialInit) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{"SqlDiskCacheSerialInitialize", "true"}, |
| {"SqlDiskCacheShardCount", "2"}}); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.SuccessTime", 1); |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.FailureTime", 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, SerialInitShardFail) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{"SqlDiskCacheSerialInitialize", "true"}, |
| {"SqlDiskCacheShardCount", "2"}}); |
| |
| auto backend = CreateBackend(); |
| // Fail the second shard. |
| backend->GetSqlStoreForTest()->SetSimulateDbShardFailureForTesting(1, true); |
| |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.SuccessTime", 0); |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.FailureTime", 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, SerialInitFail) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{"SqlDiskCacheSerialInitialize", "true"}}); |
| |
| auto backend = CreateBackend(); |
| // Make fake index file check fail by creating a directory where the file |
| // should be. |
| base::CreateDirectory( |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName)); |
| |
| base::test::TestFuture<int> future; |
| base::HistogramTester histogram_tester; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.SuccessTime", 0); |
| histogram_tester.ExpectTotalCount("Net.SqlDiskCache.Init.FailureTime", 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithCorruptedFakeIndexFile) { |
| std::string corrupted_contents = GetExpectedFakeIndexContents(); |
| base::span<uint8_t> corrupted_contents_span = |
| base::as_writable_bytes(base::span(corrupted_contents)); |
| // Rewrite the last char to 'X'. |
| corrupted_contents_span.subspan(corrupted_contents_span.size() - 1) |
| .copy_from({'X'}); |
| base::HistogramTester histogram_tester; |
| base::FilePath file_path = |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName); |
| ASSERT_TRUE(base::WriteFile(file_path, corrupted_contents_span)); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kWrongMagicNumber, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithWrongSizeFakeIndexFile) { |
| base::HistogramTester histogram_tester; |
| base::FilePath file_path = |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName); |
| const int32_t kWrongMagicNumber = 0xDEADBEEF; |
| ASSERT_TRUE( |
| base::WriteFile(file_path, base::byte_span_from_ref(kWrongMagicNumber))); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kWrongFileSize, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithOpenFileFailed) { |
| const std::string expected_contents = GetExpectedFakeIndexContents(); |
| base::HistogramTester histogram_tester; |
| base::FilePath file_path = |
| temp_dir_.GetPath().Append(kSqlBackendFakeIndexFileName); |
| ASSERT_TRUE( |
| base::WriteFile(file_path, base::as_byte_span(expected_contents))); |
| base::FilePermissionRestorer permission_restorer(file_path); |
| // Make the file unreadable. |
| ASSERT_TRUE(base::MakeFileUnreadable(file_path)); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kOpenFileFailed, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithCreateFileFailed) { |
| base::HistogramTester histogram_tester; |
| base::FilePermissionRestorer permission_restorer(temp_dir_.GetPath()); |
| // Make the directory unwrittable. |
| ASSERT_TRUE(base::MakeFileUnwritable(temp_dir_.GetPath())); |
| |
| auto backend = CreateBackend(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectUniqueSample("Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kCreateFileFailed, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, InitWithFailedToCreateDirectory) { |
| base::HistogramTester histogram_tester; |
| base::FilePath cache_dir = |
| temp_dir_.GetPath().Append(FILE_PATH_LITERAL("cache")); |
| // Create a file where the cache directory is supposed to be, to simulate a |
| // directory creation failure. |
| ASSERT_TRUE(base::WriteFile(cache_dir, "")); |
| |
| auto backend = std::make_unique<SqlBackendImpl>(cache_dir, kDefaultMaxBytes, |
| net::CacheType::DISK_CACHE, |
| /*cleanup_tracker=*/nullptr); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::ERR_FAILED); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SqlDiskCache.FakeIndexFileError", |
| FakeIndexFileError::kFailedToCreateDirectory, 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, MaxFileSizeSmallMax) { |
| const int64_t kMaxBytes = 10 * 1024 * 1024; |
| auto backend = CreateBackendAndInit(kMaxBytes); |
| EXPECT_EQ(backend->MaxFileSize(), kSqlBackendMinFileSizeLimit); |
| } |
| |
| TEST_F(SqlBackendImplTest, MaxFileSizeCalculation) { |
| const int64_t kLargeMaxBytes = 100 * 1024 * 1024; |
| auto backend = CreateBackendAndInit(kLargeMaxBytes); |
| EXPECT_EQ(backend->MaxFileSize(), |
| kLargeMaxBytes / kSqlBackendMaxFileRatioDenominator); |
| } |
| |
| TEST_F(SqlBackendImplTest, GetStats) { |
| auto backend = CreateBackendAndInit(); |
| base::StringPairs stats; |
| backend->GetStats(&stats); |
| EXPECT_THAT(stats, ElementsAre(Pair("Cache type", "SQL Cache"))); |
| } |
| |
| // Tests a race condition where an entry is doomed via `SqlEntryImpl::Doom()` |
| // while an iterator is in the process of opening it. The iterator should still |
| // successfully open the entry, but the entry should be marked as doomed. This |
| // works because `OpenNextEntry` is an exclusive operation that runs before the |
| // normal `Doom` operation. |
| TEST_F(SqlBackendImplTest, IteratorParallelEntryDoom) { |
| auto backend = CreateBackendAndInit(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry1 = create_result.ReleaseEntry(); |
| |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| entry1->Doom(); |
| |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| auto* entry2 = result_iter.ReleaseEntry(); |
| |
| EXPECT_EQ(entry1, entry2); |
| EXPECT_TRUE((static_cast<SqlEntryImpl*>(entry1))->doomed()); |
| |
| entry1->Close(); |
| entry2->Close(); |
| } |
| |
| // Tests a race condition where an entry is doomed and closed while an iterator |
| // is opening it. The iterator should still get a handle to the doomed entry. |
| // This verifies that the backend correctly manages the lifecycle of an entry |
| // that is being operated on by multiple asynchronous calls. |
| TEST_F(SqlBackendImplTest, IteratorParallelEntryDoomAndClose) { |
| auto backend = CreateBackendAndInit(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| entry->Doom(); |
| // Doom() is asynchronous. The entry is not marked as doomed until the |
| // callback of OpenNextEntry is called. |
| EXPECT_FALSE((static_cast<SqlEntryImpl*>(entry))->doomed()); |
| entry->Close(); |
| |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| |
| entry = result_iter.ReleaseEntry(); |
| |
| EXPECT_TRUE((static_cast<SqlEntryImpl*>(entry))->doomed()); |
| entry->Close(); |
| } |
| |
| // Tests that the iterator correctly skips over an entry that is doomed before |
| // the `OpenNextEntry` operation is executed. |
| TEST_F(SqlBackendImplTest, IteratorParallelEntryDoomOpenNext) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create the first entry. |
| TestEntryResultCompletionCallback cb_create1; |
| disk_cache::EntryResult create_result1 = cb_create1.GetResult( |
| backend->CreateEntry("key1", net::HIGHEST, cb_create1.callback())); |
| ASSERT_THAT(create_result1.net_error(), IsOk()); |
| create_result1.ReleaseEntry()->Close(); |
| |
| // Create the second entry. |
| TestEntryResultCompletionCallback cb_create2; |
| disk_cache::EntryResult create_result2 = cb_create2.GetResult( |
| backend->CreateEntry("key2", net::HIGHEST, cb_create2.callback())); |
| ASSERT_THAT(create_result2.net_error(), IsOk()); |
| auto* entry = create_result2.ReleaseEntry(); |
| |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb_iter; |
| |
| entry->Doom(); |
| entry->Close(); |
| |
| // `entry->Doom()` is called before `OpenNextEntry()` is initiated. |
| // The iterator starts from the newest entry, which is `key2`. However, `key2` |
| // is doomed before the iterator's `OpenNextEntry` operation is posted. The |
| // iterator should detect that `key2` is doomed in the database and skip it, |
| // returning `key1` instead. |
| EntryResult result = |
| cb_iter.GetResult(iter->OpenNextEntry(cb_iter.callback())); |
| ASSERT_THAT(result.net_error(), IsOk()); |
| entry = result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetKey(), "key1"); |
| entry->Close(); |
| |
| // There should be no more entries. |
| EntryResult result2 = |
| cb_iter.GetResult(iter->OpenNextEntry(cb_iter.callback())); |
| ASSERT_THAT(result2.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| // Tests a race condition between an iterator opening an entry and a direct call |
| // to `Backend::DoomEntry`. |
| TEST_F(SqlBackendImplTest, IteratorParallelDoom) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry and write some data to it. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry1 = create_result.ReleaseEntry(); |
| const std::string kData = "some data"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry1->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(cb_write.GetResult(rv_write), static_cast<int>(buffer->size())); |
| |
| entry1->Close(); |
| |
| // 2. Start opening the entry via an iterator. This is an async operation. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| // 3. Immediately call `DoomEntry` for the same key. This is also async. |
| net::TestCompletionCallback cb_doom; |
| int rv_doom = backend->DoomEntry("key", net::HIGHEST, cb_doom.callback()); |
| EXPECT_EQ(net::OK, cb_doom.GetResult(rv_doom)); |
| |
| // `OpenNextEntry()` is an exclusive operation, while `DoomEntry()` is a |
| // normal operation. Since `OpenNextEntry()` is posted first, it will run |
| // before the `DoomEntry()` operation, which gets queued. After the iterator |
| // returns the entry, the `DoomEntry()` operation runs and marks the entry as |
| // doomed. |
| // 4. Wait for the iterator to finish opening the entry. |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| auto* entry = result_iter.ReleaseEntry(); |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| |
| // 5. Verify that the data can still be read from the doomed entry. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(kData.size()); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(kData.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), kData.size()), kData); |
| entry->Close(); |
| } |
| |
| // Tests a race condition between an iterator opening an entry and a call to |
| // `Backend::DoomAllEntries`. |
| TEST_F(SqlBackendImplTest, IteratorParallelDoomAll) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry and write some data to it. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry1 = create_result.ReleaseEntry(); |
| const std::string kData = "some data"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry1->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(cb_write.GetResult(rv_write), static_cast<int>(buffer->size())); |
| entry1->Close(); |
| |
| // 2. Start opening the entry via an iterator. This is an async operation. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| // 3. Immediately call `DoomAllEntries`. This is also an async operation. |
| net::TestCompletionCallback cb_doom; |
| int rv_doom = backend->DoomAllEntries(cb_doom.callback()); |
| EXPECT_EQ(net::OK, cb_doom.GetResult(rv_doom)); |
| |
| // Both `DoomAllEntries()` and `OpenNextEntry()` are exclusive operations and |
| // are serialized. Since `OpenNextEntry()` is posted first, it will run |
| // first, retrieving the entry. Then, `DoomAllEntries()` will run and doom all |
| // entries, including the one just opened. |
| // 4. Wait for the iterator to finish opening the entry. |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| auto* entry = result_iter.ReleaseEntry(); |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| |
| // 5. Verify that the data can still be read from the doomed entry. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(kData.size()); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(kData.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), kData.size()), kData); |
| entry->Close(); |
| } |
| |
| // Tests that an entry's `last_used` time is updated correctly when data is |
| // written and the entry is closed, even if an iterator is concurrently active. |
| // Also verifies the written data can be read back. |
| TEST_F(SqlBackendImplTest, IteratorParallelWriteDataAndClose) { |
| auto backend = CreateBackendAndInit(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Advance clock to ensure `last_used` time is distinct from creation. |
| task_environment_.AdvanceClock(base::Minutes(1)); |
| |
| // Create an iterator and attempt to open the entry concurrently. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| // Record the time when data is written. This should be the new `last_used` |
| // time. |
| const base::Time kWriteTime = base::Time::Now(); |
| |
| // Write data to stream 0 and close the entry. |
| const std::string kHeadData = "header_data"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kHeadData); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry->WriteData(0, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| entry->Close(); |
| EXPECT_EQ(cb_write.GetResult(rv_write), buffer->size()); |
| |
| // Get the result from the iterator's open operation. |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| entry = result_iter.ReleaseEntry(); |
| // Verify that the `last_used` time of the opened entry reflects the write |
| // time. |
| EXPECT_THAT(entry->GetLastUsed(), kWriteTime); |
| |
| // Read the data back from the entry opened via the iterator. |
| auto read_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(kHeadData.size() * 2); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(0, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), kHeadData.size()); |
| entry->Close(); |
| } |
| |
| // Tests that an entry's `body_end` is updated correctly when data is written to |
| // stream 1 and the entry is closed, even if an iterator is concurrently active. |
| // Also verifies the written data can be read back. |
| TEST_F(SqlBackendImplTest, IteratorParallelWriteBodyDataAndClose) { |
| auto backend = CreateBackendAndInit(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Create an iterator and attempt to open the entry concurrently. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| // Write data to stream 1 and close the entry. |
| const std::string kBodyData = "body_data"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kBodyData); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| entry->Close(); |
| EXPECT_EQ(cb_write.GetResult(rv_write), static_cast<int>(buffer->size())); |
| |
| // Get the result from the iterator's open operation. |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| entry = result_iter.ReleaseEntry(); |
| // Verify that the `body_end` of the opened entry reflects the write. |
| EXPECT_EQ(entry->GetDataSize(1), static_cast<int32_t>(kBodyData.size())); |
| |
| // Read the data back from the entry opened via the iterator. |
| auto read_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(kBodyData.size() * 2); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(kBodyData.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), kBodyData.size()), kBodyData); |
| entry->Close(); |
| } |
| |
| // Tests that an entry's `last_used` time is updated correctly when data is read |
| // and the entry is closed, even if an iterator is concurrently active. |
| TEST_F(SqlBackendImplTest, IteratorParallelReadDataAndClose) { |
| auto backend = CreateBackendAndInit(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Advance clock to ensure `last_used` time is distinct from creation. |
| task_environment_.AdvanceClock(base::Minutes(1)); |
| |
| // Create an iterator and attempt to open the entry concurrently. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb; |
| EntryResult result_iter = iter->OpenNextEntry(cb.callback()); |
| |
| // Record the time when data is read. This should be the new `last_used` time. |
| const base::Time kReadTime = base::Time::Now(); |
| |
| // Read data from stream 0 and close the entry. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(1); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(0, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), 0); |
| entry->Close(); |
| |
| // Get the result from the iterator's open operation. |
| result_iter = cb.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| entry = result_iter.ReleaseEntry(); |
| |
| // Verify that the `last_used` time of the opened entry reflects the read |
| // time. |
| EXPECT_THAT(entry->GetLastUsed(), kReadTime); |
| entry->Close(); |
| } |
| |
| // Tests a race condition where an entry is opened simultaneously by an iterator |
| // and a direct `OpenEntry` call. The backend should correctly handle this by |
| // returning the same `SqlEntryImpl` instance for both operations, preventing |
| // duplicate in-memory representations of the same cache entry. |
| TEST_F(SqlBackendImplTest, IteratorAndOpenEntryParallelRace) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| create_result.ReleaseEntry()->Close(); |
| |
| base::test::TestFuture<EntryResult> future_iter; |
| base::test::TestFuture<EntryResult> future_open; |
| |
| auto iter = backend->CreateIterator(); |
| // Start opening the entry via the iterator. This posts an async task. |
| ASSERT_EQ(iter->OpenNextEntry(future_iter.GetCallback()).net_error(), |
| net::ERR_IO_PENDING); |
| |
| // Immediately try to open the same entry directly. This also posts an async |
| // task. |
| ASSERT_EQ(backend->OpenEntry("key", net::HIGHEST, future_open.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 3. Wait for both operations to complete. This test is designed to expose a |
| // race condition. The backend should handle this race correctly by ensuring |
| // only one `SqlEntryImpl` is created for the same key. |
| EntryResult iter_res = future_iter.Take(); |
| EntryResult open_res = future_open.Take(); |
| |
| ASSERT_THAT(iter_res.net_error(), IsOk()); |
| ASSERT_THAT(open_res.net_error(), IsOk()); |
| auto* entry1 = iter_res.ReleaseEntry(); |
| auto* entry2 = open_res.ReleaseEntry(); |
| |
| // Both the iterator and the direct open operation should resolve to the same |
| // underlying `SqlEntryImpl` instance. The backend's logic for managing |
| // active entries should prevent the creation of a second instance for the |
| // same key. |
| EXPECT_EQ(entry1, entry2); |
| entry1->Close(); |
| entry2->Close(); |
| } |
| |
| // Tests a race condition where an entry is opened by an iterator, opened by a |
| // direct call, and doomed, all in parallel. |
| // The exclusive `OpenNextEntry` operation runs first. The normal `OpenEntry` |
| // and `DoomEntry` operations are queued and serialized by key. `OpenEntry` |
| // runs next, getting a reference to the active entry. Finally, `DoomEntry` |
| // runs and marks that same entry instance as doomed. |
| TEST_F(SqlBackendImplTest, IteratorAndOpenEntryAndDoomParallelRace) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| create_result.ReleaseEntry()->Close(); |
| |
| base::test::TestFuture<EntryResult> future_iter; |
| base::test::TestFuture<EntryResult> future_open; |
| base::test::TestFuture<int> future_doom; |
| |
| auto iter = backend->CreateIterator(); |
| ASSERT_EQ(iter->OpenNextEntry(future_iter.GetCallback()).net_error(), |
| net::ERR_IO_PENDING); |
| ASSERT_EQ(backend->OpenEntry("key", net::HIGHEST, future_open.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| ASSERT_EQ(backend->DoomEntry("key", net::HIGHEST, future_doom.GetCallback()), |
| net::ERR_IO_PENDING); |
| |
| EntryResult iter_res = future_iter.Take(); |
| ASSERT_THAT(iter_res.net_error(), IsOk()); |
| auto* entry1 = iter_res.ReleaseEntry(); |
| |
| EntryResult open_res = future_open.Take(); |
| ASSERT_THAT(open_res.net_error(), IsOk()); |
| auto* entry2 = open_res.ReleaseEntry(); |
| |
| EXPECT_EQ(entry1, entry2); |
| EXPECT_EQ(future_doom.Take(), net::OK); |
| |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry1)->doomed()); |
| |
| entry1->Close(); |
| entry2->Close(); |
| } |
| |
| // Tests a race condition where an entry is opened via `OpenEntry` while it is |
| // simultaneously being opened and then doomed by an iterator. |
| TEST_F(SqlBackendImplTest, OpenEntryRacesWithIteratorAndDoom) { |
| // This test simulates a race condition to verify the interaction between |
| // opening an entry directly and an iterator that dooms the same entry in its |
| // callback. The exclusive `OpenNextEntry` operation runs first. Its callback |
| // then posts a normal `Doom` operation. The `OpenEntry` call (also a normal |
| // operation) was posted before the `Doom` operation. Due to serialization by |
| // key, `OpenEntry` gets a reference to the active entry first, and then the |
| // `Doom` operation marks that same instance as doomed. |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| create_result.ReleaseEntry()->Close(); |
| |
| auto iter = backend->CreateIterator(); |
| // 2. Start opening the entry via an iterator. This is an async operation. |
| ASSERT_EQ(iter->OpenNextEntry( |
| base::BindLambdaForTesting([&](EntryResult entry_result) { |
| // 4. Once the iterator gets the entry, doom and close it. |
| auto* entry = entry_result.ReleaseEntry(); |
| entry->Doom(); |
| entry->Close(); |
| })) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 3. While the iterator operation is in-flight, attempt to open the same |
| // entry directly. This is also an async operation. |
| base::test::TestFuture<EntryResult> open_future; |
| ASSERT_EQ(backend->OpenEntry("key", net::HIGHEST, open_future.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 5. Wait for the direct `OpenEntry` call to complete. |
| EntryResult open_result = open_future.Take(); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| auto* entry = open_result.ReleaseEntry(); |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| entry->Close(); |
| } |
| |
| // Tests a race condition where an entry is opened via `OpenOrCreateEntry` while |
| // it is simultaneously being opened and then doomed by an iterator. |
| // `OpenOrCreateEntry` should find the existing entry (which is being opened by |
| // the iterator) and not create a new one. The test verifies that the returned |
| // entry is correctly marked as doomed, demonstrating proper serialization and |
| // state management. |
| TEST_F(SqlBackendImplTest, OpenOrCreateEntryEntryRacesWithIteratorAndDoom) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry and record its creation time. |
| base::Time first_entry_creation_time = base::Time::Now(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetLastUsed(), first_entry_creation_time); |
| entry->Close(); |
| |
| task_environment_.AdvanceClock(base::Minutes(1)); |
| |
| auto iter = backend->CreateIterator(); |
| // 2. Start opening the entry via an iterator. This is an async operation. |
| ASSERT_EQ(iter->OpenNextEntry( |
| base::BindLambdaForTesting([&](EntryResult entry_result) { |
| // 4. Once the iterator gets the entry, doom and close it. |
| auto* entry = entry_result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetLastUsed(), |
| first_entry_creation_time); |
| entry->Doom(); |
| entry->Close(); |
| })) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| base::test::TestFuture<EntryResult> open_or_create_future; |
| // 3. While the iterator operation is in-flight, call `OpenOrCreateEntry` for |
| // the same key. |
| ASSERT_EQ(backend |
| ->OpenOrCreateEntry("key", net::HIGHEST, |
| open_or_create_future.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| EntryResult open_or_create_result = open_or_create_future.Take(); |
| ASSERT_THAT(open_or_create_result.net_error(), IsOk()); |
| entry = open_or_create_result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetLastUsed(), first_entry_creation_time); |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| entry->Close(); |
| } |
| |
| // Tests a race condition where an entry is opened via `OpenEntry` while it is |
| // simultaneously being opened, written to, and closed by an iterator. |
| // This test verifies that in-flight modifications (like `last_used` |
| // time and header data updates) that are queued while an entry is not active |
| // are correctly applied to the entry's in-memory representation when it is |
| // next opened. This ensures that subsequent operations on the entry see the |
| // most up-to-date state. |
| TEST_F(SqlBackendImplTest, OpenEntryRacesWithIteratorAndWriteData) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry and record its creation time. |
| base::Time first_entry_creation_time = base::Time::Now(); |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetLastUsed(), first_entry_creation_time); |
| entry->Close(); |
| |
| task_environment_.AdvanceClock(base::Minutes(1)); |
| |
| const std::string kHeadData = "header_data"; |
| // 2. Start opening the entry via an iterator. This is an async operation. |
| auto iter = backend->CreateIterator(); |
| ASSERT_EQ(iter->OpenNextEntry( |
| base::BindLambdaForTesting([&](EntryResult entry_result) { |
| // 4. Once the iterator gets the entry, write data to it |
| // and close it. This updates the entry's `last_used` |
| // time and header data in memory, and queues a write |
| // to the persistent store. |
| auto* entry = entry_result.ReleaseEntry(); |
| EXPECT_EQ(entry->GetLastUsed(), |
| first_entry_creation_time); |
| auto buffer = |
| base::MakeRefCounted<net::StringIOBuffer>(kHeadData); |
| entry->WriteData(0, 0, buffer.get(), buffer->size(), |
| base::DoNothing(), false); |
| entry->Close(); |
| })) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 3. While the iterator operation is in-flight, attempt to open the same |
| // entry directly. |
| base::test::TestFuture<EntryResult> open_future; |
| ASSERT_EQ(backend->OpenEntry("key", net::HIGHEST, open_future.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 5. The `OpenEntry` operation should succeed. The backend should handle the |
| // race by applying the in-flight modifications (from the iterator's |
| // write and close) to the entry data before returning the new entry |
| // handle. |
| EntryResult open_result = open_future.Take(); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| // The `last_used` time should reflect the time of the write. |
| EXPECT_EQ(entry->GetLastUsed(), first_entry_creation_time + base::Minutes(1)); |
| // The data written by the iterator should be readable. |
| auto buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(kHeadData.size() * 2); |
| ASSERT_EQ( |
| entry->ReadData(0, 0, buffer.get(), buffer->size(), base::DoNothing()), |
| kHeadData.size()); |
| EXPECT_EQ(buffer->first(kHeadData.size()), base::as_byte_span(kHeadData)); |
| entry->Close(); |
| } |
| |
| // Tests that OnExternalCacheHit correctly updates the last_used time, even when |
| // an OpenEntry operation is in-flight. |
| TEST_F(SqlBackendImplTest, OnExternalCacheHitRacesWithOpen) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry and close it. |
| const std::string kKey = "my-key"; |
| TestEntryResultCompletionCallback create_cb; |
| disk_cache::EntryResult create_result = create_cb.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, create_cb.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* created_entry = create_result.ReleaseEntry(); |
| base::Time create_time = created_entry->GetLastUsed(); |
| created_entry->Close(); |
| |
| // 2. Advance time. |
| task_environment_.AdvanceClock(base::Minutes(1)); |
| |
| // 3. Start opening the entry. This is an async operation. |
| base::test::TestFuture<EntryResult> open_future; |
| ASSERT_EQ(backend->OpenEntry(kKey, net::HIGHEST, open_future.GetCallback()) |
| .net_error(), |
| net::ERR_IO_PENDING); |
| |
| // 4. Call OnExternalCacheHit. |
| base::Time hit_time = base::Time::Now(); |
| EXPECT_NE(create_time, hit_time); |
| backend->OnExternalCacheHit(kKey); |
| |
| // 5. Wait for OpenEntry to complete. |
| EntryResult open_result = open_future.Take(); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // 6. The entry's last_used time should be the time of the external hit. |
| EXPECT_EQ(entry->GetLastUsed(), hit_time); |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, DoomEntryNonExistent) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| const std::string kNonExistentKey = "non-existent-key"; |
| net::TestCompletionCallback cb_doom; |
| int rv_doom = |
| backend->DoomEntry(kNonExistentKey, net::HIGHEST, |
| base::BindOnce([](int rv) { NOTREACHED(); })); |
| // The operation should complete synchronously due to the in-memory index |
| // check, so the callback should not be reached if the DB operation were to |
| // be attempted. |
| EXPECT_EQ(net::OK, rv_doom); |
| } |
| |
| // Tests that calling Doom() multiple times on the same entry is safe and |
| // idempotent. |
| TEST_F(SqlBackendImplTest, MultipleDoomsOnSameEntry) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Doom the entry multiple times. This should not cause any issues. |
| entry->Doom(); |
| entry->Doom(); |
| |
| // When the entry was created speculatively, the doomed flag is updated |
| // asynchronously. So need to flush the pending database operations. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| entry->Close(); |
| |
| // Verify that the entry is gone after being doomed and closed. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| // Tests that recursive calls to OpenNextEntry from within its callback do not |
| // starve normal operations. The ExclusiveOperationCoordinator's sequence-based |
| // scheduling ensures that the older normal operation (CreateEntry) is executed |
| // before the newer exclusive operation (the second OpenNextEntry). |
| TEST_F(SqlBackendImplTest, RecursiveOpenNextEntry) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create two entries to iterate over. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result1 = cb_create.GetResult( |
| backend->CreateEntry("key1", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result1.net_error(), IsOk()); |
| create_result1.ReleaseEntry()->Close(); |
| |
| disk_cache::EntryResult create_result2 = cb_create.GetResult( |
| backend->CreateEntry("key2", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result2.net_error(), IsOk()); |
| create_result2.ReleaseEntry()->Close(); |
| |
| auto iter = backend->CreateIterator(); |
| |
| base::RunLoop run_loop; |
| Entry* entry3 = nullptr; |
| |
| bool key_1_found = false; |
| bool key_2_found = false; |
| |
| // The first call to OpenNextEntry. Inside its callback, we'll trigger the |
| // second call. |
| ASSERT_THAT( |
| iter->OpenNextEntry(base::BindLambdaForTesting([&](EntryResult result1) { |
| ASSERT_THAT(result1.net_error(), IsOk()); |
| auto* entry1_itr = result1.ReleaseEntry(); |
| key_1_found = entry1_itr->GetKey() == "key1"; |
| key_2_found = entry1_itr->GetKey() == "key2"; |
| EXPECT_TRUE(key_1_found || key_2_found); |
| entry1_itr->Close(); |
| // Now, make the recursive call to OpenNextEntry. |
| ASSERT_THAT( |
| iter->OpenNextEntry( |
| base::BindLambdaForTesting([&](EntryResult result2) { |
| ASSERT_THAT(result2.net_error(), IsOk()); |
| // By this point, the CreateEntry for "key3" should |
| // have completed, proving that the normal operation |
| // was not starved. |
| CHECK(entry3); |
| |
| auto* entry2_itr = result2.ReleaseEntry(); |
| if (entry2_itr->GetKey() == "key3") { |
| EXPECT_EQ(entry2_itr, entry3); |
| } else { |
| if (key_1_found) { |
| EXPECT_EQ(entry2_itr->GetKey(), "key2"); |
| } else { |
| EXPECT_EQ(entry2_itr->GetKey(), "key1"); |
| } |
| } |
| entry2_itr->Close(); |
| entry3->Close(); |
| run_loop.Quit(); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| |
| // While the first OpenNextEntry is in flight, post a normal operation to |
| // create a third entry. This tests that the recursive exclusive operations |
| // do not starve the normal one. |
| ASSERT_THAT( |
| backend |
| ->CreateEntry("key3", net::HIGHEST, |
| base::BindLambdaForTesting([&](EntryResult result3) { |
| ASSERT_THAT(result3.net_error(), IsOk()); |
| entry3 = result3.ReleaseEntry(); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| run_loop.Run(); |
| } |
| |
| // Tests that recursive calls to OpenNextEntry from within its callback do not |
| // starve normal operations, even when one of the iterated entries is already |
| // active. |
| TEST_F(SqlBackendImplTest, RecursiveOpenNextEntryWithActiveEntry) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create one entry and close it. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result1 = cb_create.GetResult( |
| backend->CreateEntry("key1", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result1.net_error(), IsOk()); |
| create_result1.ReleaseEntry()->Close(); |
| |
| // Create a second entry and keep it active. |
| disk_cache::EntryResult create_result2 = cb_create.GetResult( |
| backend->CreateEntry("key2", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result2.net_error(), IsOk()); |
| auto* entry2_active = create_result2.ReleaseEntry(); |
| |
| auto iter = backend->CreateIterator(); |
| |
| base::RunLoop run_loop; |
| Entry* entry3 = nullptr; |
| |
| bool key_1_found = false; |
| bool key_2_found = false; |
| |
| // The first call to OpenNextEntry. Inside its callback, we'll trigger the |
| // second call. |
| ASSERT_THAT( |
| iter->OpenNextEntry(base::BindLambdaForTesting([&](EntryResult result1) { |
| ASSERT_THAT(result1.net_error(), IsOk()); |
| auto* entry1_iter = result1.ReleaseEntry(); |
| key_1_found = entry1_iter->GetKey() == "key1"; |
| key_2_found = entry1_iter->GetKey() == "key2"; |
| EXPECT_TRUE(key_1_found || key_2_found); |
| if (key_2_found) { |
| EXPECT_EQ(entry1_iter, entry2_active); |
| } |
| entry1_iter->Close(); |
| // Now, make the recursive call to OpenNextEntry. |
| ASSERT_THAT( |
| iter->OpenNextEntry( |
| base::BindLambdaForTesting([&](EntryResult result2) { |
| ASSERT_THAT(result2.net_error(), IsOk()); |
| // By this point, the CreateEntry for "key3" should |
| // have completed, proving that the normal operation |
| // was not starved. |
| CHECK(entry3); |
| |
| auto* entry2_itr = result2.ReleaseEntry(); |
| if (entry2_itr->GetKey() == "key3") { |
| EXPECT_EQ(entry2_itr, entry3); |
| } else { |
| if (key_1_found) { |
| EXPECT_EQ(entry2_itr->GetKey(), "key2"); |
| } else { |
| EXPECT_EQ(entry2_itr->GetKey(), "key1"); |
| } |
| } |
| entry2_itr->Close(); |
| entry3->Close(); |
| run_loop.Quit(); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| |
| // While the first OpenNextEntry is in flight, post a normal operation to |
| // create a third entry. |
| ASSERT_THAT( |
| backend |
| ->CreateEntry("key3", net::HIGHEST, |
| base::BindLambdaForTesting([&](EntryResult result3) { |
| ASSERT_THAT(result3.net_error(), IsOk()); |
| entry3 = result3.ReleaseEntry(); |
| })) |
| .net_error(), |
| IsError(net::ERR_IO_PENDING)); |
| run_loop.Run(); |
| |
| // Close the initially active entry. |
| entry2_active->Close(); |
| } |
| |
| // Tests that if a pending ReadData operation is aborted (e.g., due to backend |
| // destruction), the callback is invoked with net::ERR_ABORTED. |
| TEST_F(SqlBackendImplTest, AbortPendingReadData) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback create_cb; |
| disk_cache::EntryResult create_result = create_cb.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, create_cb.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Write some data to stream 1 so that a subsequent read will be pending. |
| const std::string kBodyData = "body_data"; |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>(kBodyData); |
| net::TestCompletionCallback write_cb; |
| int write_rv = |
| entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| write_cb.callback(), false); |
| ASSERT_EQ(write_cb.GetResult(write_rv), |
| static_cast<int>(write_buffer->size())); |
| |
| // Initiate a ReadData operation, which will be pending. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10); |
| base::test::TestFuture<int> read_future; |
| int rv = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| read_future.GetCallback()); |
| ASSERT_THAT(rv, IsError(net::ERR_IO_PENDING)); |
| |
| // Destroy the backend while the read is in flight. |
| backend.reset(); |
| |
| // The callback should be aborted. |
| EXPECT_EQ(read_future.Get(), net::ERR_ABORTED); |
| |
| entry->Close(); |
| } |
| |
| // Tests that if a pending WriteData operation is aborted (e.g., due to backend |
| // destruction), the callback is invoked with net::ERR_ABORTED. |
| TEST_F(SqlBackendImplTest, AbortPendingWriteData) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "0"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback create_cb; |
| disk_cache::EntryResult create_result = create_cb.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, create_cb.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Initiate a WriteData operation, which will be pending. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("data"); |
| base::test::TestFuture<int> write_future; |
| int rv = entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| write_future.GetCallback(), false); |
| ASSERT_THAT(rv, write_buffer->size()); |
| |
| auto task_runners = backend->GetBackgroundTaskRunnersForTest(); |
| |
| // Destroy the backend while the write is in flight. |
| backend.reset(); |
| |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| while (!db_handle->GetError().has_value()) { |
| FlushQueueInTaskRunners(task_runners); |
| } |
| |
| // The db_handle should have been set to aborted. |
| EXPECT_EQ(db_handle->GetError(), SqlPersistentStore::Error::kAborted); |
| |
| entry->Close(); |
| } |
| |
| // Tests that if a pending GetAvailableRange operation is aborted (e.g., due to |
| // backend destruction), the callback is invoked with net::ERR_ABORTED. |
| TEST_F(SqlBackendImplTest, AbortPendingGetAvailableRange) { |
| auto backend = CreateBackendAndInit(); |
| |
| // Create an entry. |
| TestEntryResultCompletionCallback create_cb; |
| disk_cache::EntryResult create_result = create_cb.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, create_cb.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Initiate a GetAvailableRange operation, which will be pending. |
| base::test::TestFuture<const RangeResult&> range_future; |
| RangeResult result = |
| entry->GetAvailableRange(0, 100, range_future.GetCallback()); |
| ASSERT_THAT(result.net_error, IsError(net::ERR_IO_PENDING)); |
| |
| // Destroy the backend while the operation is in flight. |
| backend.reset(); |
| |
| // The callback should be aborted. |
| const RangeResult& aborted_result = range_future.Get(); |
| EXPECT_THAT(aborted_result.net_error, IsError(net::ERR_ABORTED)); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, DoomedEntriesCleanup) { |
| // 1. Create a backend and add three entries with data. |
| auto backend = CreateBackendAndInit(); |
| auto task_runners = backend->GetBackgroundTaskRunnersForTest(); |
| |
| const std::string kKey1 = "key1"; |
| const std::string kKey2 = "key2"; |
| const std::string kKey3 = "key3"; |
| const std::string kData = "some data"; |
| |
| auto* entry1 = CreateEntryAndWriteData(backend.get(), kKey1, kData); |
| auto* entry2 = CreateEntryAndWriteData(backend.get(), kKey2, kData); |
| auto* entry3 = CreateEntryAndWriteData(backend.get(), kKey3, kData); |
| WaitUntilInitialized(*backend, |
| static_cast<SqlEntryImpl*>(entry3)->db_handle()); |
| auto res_id = |
| static_cast<SqlEntryImpl*>(entry3)->db_handle()->GetResId().value(); |
| entry1->Close(); |
| entry2->Close(); |
| entry3->Close(); |
| |
| backend.reset(); |
| |
| FlushQueueInTaskRunners(task_runners); |
| |
| // 2. Open the database directly via SqlPersistentStore and doom the third |
| // entry. |
| { |
| SqlAsyncTaskManager async_task_manager; |
| auto store = std::make_unique<SqlPersistentStore>( |
| temp_dir_.GetPath(), kDefaultMaxBytes, net::CacheType::DISK_CACHE, |
| task_runners, async_task_manager, /*cleanup_tracker=*/nullptr); |
| |
| base::test::TestFuture<disk_cache::SqlPersistentStore::Error> future_init; |
| store->Initialize(future_init.GetCallback()); |
| ASSERT_EQ(future_init.Get(), disk_cache::SqlPersistentStore::Error::kOk); |
| |
| base::test::TestFuture<SqlPersistentStore::Error> future_doom; |
| store->DoomEntry(CacheEntryKey(kKey3), res_id, |
| /*accept_index_mismatch=*/false, |
| future_doom.GetCallback()); |
| EXPECT_EQ(future_doom.Get(), SqlPersistentStore::Error::kOk); |
| |
| store.reset(); |
| } |
| |
| FlushQueueInTaskRunners(task_runners); |
| |
| // 3. Recreate the backend |
| backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| // 4. Open and doom the first and the second entries and let them as active. |
| TestEntryResultCompletionCallback cb_open1; |
| disk_cache::EntryResult open_result1 = cb_open1.GetResult( |
| backend->OpenEntry(kKey1, net::HIGHEST, cb_open1.callback())); |
| entry1 = open_result1.ReleaseEntry(); |
| entry1->Doom(); |
| |
| TestEntryResultCompletionCallback cb_open2; |
| disk_cache::EntryResult open_result2 = cb_open2.GetResult( |
| backend->OpenEntry(kKey2, net::HIGHEST, cb_open2.callback())); |
| entry2 = open_result2.ReleaseEntry(); |
| entry2->Doom(); |
| |
| base::HistogramTester histogram_tester; |
| backend->OnBrowserIdle(); |
| |
| // Flush the queue to ensure that cleanup task is completed. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Verify that `DeleteDoomedEntriesCount` UMA was recorded in the histogram. |
| histogram_tester.ExpectUniqueSample( |
| "Net.SqlDiskCache.DeleteDoomedEntriesCount", 1, 1); |
| |
| // 5. Verify that the data can still be read from the doomed entry. |
| ReadAndVerifyData(entry1, kData); |
| ReadAndVerifyData(entry2, kData); |
| |
| entry1->Close(); |
| entry2->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntry) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. This should return immediately with a speculatively |
| // created entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // The db_handle should be in the initial state |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| // Even after flushing all DB tasks, it should still be in the initial state. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| entry->Close(); |
| |
| // Once closed, the DB side entry creation process starts. |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| // After flushing all DB tasks, it enters the finished (created) state and the |
| // ResID is set. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_TRUE(db_handle->IsFinished()); |
| // Now the res_id should be available. |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| |
| // The entry must be available |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryDoomClose) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| entry->Doom(); |
| entry->Close(); |
| |
| // Verify that the entry is gone. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryClose) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| entry->Close(); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| entry->Close(); |
| |
| // The res_id should be available. |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryAndRead) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10); |
| EXPECT_EQ(entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| base::DoNothing()), |
| 0); |
| |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| entry->Close(); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| backend->RunUntilAllTasksCompleteForTest(); |
| // The res_id should be available now. |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryWriteWithinBufferLimit) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| const std::string kData = "some data"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| net::TestCompletionCallback write_cb; |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| ASSERT_LT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.Get()); |
| EXPECT_EQ(write_cb.GetResult(entry->WriteData(1, 0, write_buffer.get(), |
| write_buffer->size(), |
| write_cb.callback(), false)), |
| static_cast<int>(write_buffer->size())); |
| |
| // If the written data is less than kSqlDiskCacheMaxWriteBufferSizePerEntry, |
| // the DB side write process only starts when the entry is closed. |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| entry->Close(); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| |
| // The res_id should be available. |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| |
| entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10); |
| |
| base::test::TestFuture<int> read_future; |
| int rv = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| read_future.GetCallback()); |
| ASSERT_THAT(rv, IsError(net::ERR_IO_PENDING)); |
| ASSERT_EQ(read_future.Get(), write_buffer->size()); |
| |
| EXPECT_EQ(std::string_view(read_buffer->data(), kData.size()), kData); |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, |
| SpeculativeCreateEntryWriteWithinBufferLimitAndDoom) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| const std::string kData = "some data"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| net::TestCompletionCallback write_cb; |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| ASSERT_LT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.Get()); |
| EXPECT_EQ(write_cb.GetResult(entry->WriteData(1, 0, write_buffer.get(), |
| write_buffer->size(), |
| write_cb.callback(), false)), |
| static_cast<int>(write_buffer->size())); |
| |
| entry->Doom(); |
| entry->Close(); |
| |
| // Verify that the entry is gone. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| |
| // If the written data is less than kSqlDiskCacheMaxWriteBufferSizePerEntry, |
| // the DB side write process does not start if the entry is doomed before |
| // being closed. |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryOptimisticWriteOnBufferFlush) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. This should return immediately with a speculatively |
| // created entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| const std::string k1ByteData = "X"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(k1ByteData); |
| |
| // Write 10 bytes. These are all buffered in memory within the entry. |
| for (int64_t i = 0; i < 10; ++i) { |
| EXPECT_EQ(entry->WriteData(1, i, buffer.get(), buffer->size(), |
| base::DoNothing(), false), |
| 1); |
| } |
| // Even after flushing all DB tasks, it should still be in the initial state. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| // Writing one more byte triggers the buffered content to be passed to |
| // SqlBackendImpl, starting the optimistic write process. Then, `db_handle` |
| // enters the creating state. |
| EXPECT_EQ(entry->WriteData(1, 10, buffer.get(), buffer->size(), |
| base::DoNothing(), false), |
| 1); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| // After flushing all DB tasks, it enters the finished (created) state and the |
| // ResID is set. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_TRUE(db_handle->IsFinished()); |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| |
| // Doom the entry. |
| entry->Doom(); |
| entry->Close(); |
| |
| // Verify that the entry is gone. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryNonOptmisticWrite) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "10"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "my-key"; |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // When WriteData is called with data larger than |
| // kSqlDiskCacheMaxWriteBufferTotalSize, and |
| // kSqlDiskCacheOptimisticWriteBufferSize, SqlBackendImpl starts the |
| // non-optimistic write process, and the `db_handle` enters the creating |
| // state. |
| const std::string kData = "1234567890A"; |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheOptimisticWriteBufferSize.Get()); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferTotalSize.Get()); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| net::TestCompletionCallback write_cb; |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| write_cb.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| EXPECT_EQ(write_cb.WaitForResult(), write_buffer->size()); |
| EXPECT_TRUE(db_handle->IsFinished()); |
| EXPECT_TRUE(db_handle->GetResId().has_value()); |
| entry->Close(); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| |
| entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| auto read_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(kData.size() + 1); |
| |
| base::test::TestFuture<int> read_future; |
| int rv = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| read_future.GetCallback()); |
| ASSERT_THAT(rv, IsError(net::ERR_IO_PENDING)); |
| ASSERT_EQ(read_future.Get(), write_buffer->size()); |
| |
| EXPECT_EQ(std::string_view(read_buffer->data(), kData.size()), kData); |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryWithDbFailure) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. This should return immediately with a speculatively |
| // created entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // The db_handle should be in the initial state |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| entry->Close(); |
| |
| // Once closed, the DB side entry creation process starts. |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // After flushing all DB tasks, the db_handle should hold a kFailedForTesting |
| // error. |
| EXPECT_THAT(db_handle->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| |
| // 6. Verify that the entry is not found. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryDbFailureOnOptmisticWrite) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "20"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry("key", net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // When WriteData is called with data larger than |
| // kSqlDiskCacheMaxWriteBufferTotalSize, and smaller than |
| // kSqlDiskCacheOptimisticWriteBufferSize, SqlBackendImpl starts the |
| // optimistic write process, and the `db_handle` enters the creating state. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("1234567890A"); |
| ASSERT_LE(write_buffer->size(), |
| net::features::kSqlDiskCacheOptimisticWriteBufferSize.Get()); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferTotalSize.Get()); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| base::DoNothing(), false), |
| write_buffer->size()); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| // The second write exceeds the optimistic write limit, so it becomes async. |
| ASSERT_GT(write_buffer->size() * 2, |
| net::features::kSqlDiskCacheOptimisticWriteBufferSize.Get()); |
| net::TestCompletionCallback write_cb; |
| EXPECT_EQ(entry->WriteData(1, write_buffer->size(), write_buffer.get(), |
| write_buffer->size(), write_cb.callback(), false), |
| net::ERR_IO_PENDING); |
| |
| net::TestCompletionCallback read_cb; |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10); |
| EXPECT_EQ(entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| read_cb.callback()), |
| net::ERR_IO_PENDING); |
| |
| base::test::TestFuture<const RangeResult&> range_future; |
| EXPECT_EQ( |
| entry->GetAvailableRange(0, 10, range_future.GetCallback()).net_error, |
| net::ERR_IO_PENDING); |
| |
| EXPECT_THAT(write_cb.WaitForResult(), IsError(net::ERR_FAILED)); |
| EXPECT_THAT(read_cb.WaitForResult(), IsError(net::ERR_FAILED)); |
| EXPECT_THAT(range_future.Get().net_error, IsError(net::ERR_FAILED)); |
| |
| // After finishing all DB tasks, the db_handle should hold a kFailedForTesting |
| // error. |
| EXPECT_THAT(db_handle->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryDbFailureOnNonOptmisticWrite) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "10"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry("key", net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // When WriteData is called with data larger than |
| // kSqlDiskCacheMaxWriteBufferTotalSize, and |
| // kSqlDiskCacheOptimisticWriteBufferSize, SqlBackendImpl starts the |
| // non-optimistic write process, and the `db_handle` enters the creating |
| // state. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("1234567890A"); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheOptimisticWriteBufferSize.Get()); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferTotalSize.Get()); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| net::TestCompletionCallback write_cb; |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| write_cb.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| EXPECT_EQ(write_cb.WaitForResult(), net::ERR_FAILED); |
| EXPECT_THAT(db_handle->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SpeculativeCreateEntryDbFailureDoom) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry("key", net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // When WriteData is called with data larger than |
| // kSqlDiskCacheMaxWriteBufferTotalSize, SqlBackendImpl starts the optimistic |
| // write process, and the `db_handle` enters the creating state. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("1234567890A"); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferTotalSize.Get()); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| EXPECT_EQ( |
| entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| base::BindOnce([](int rv) { NOTREACHED(); }), false), |
| write_buffer->size()); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| // After flushing all DB tasks, the db_handle should hold a kFailedForTesting |
| // error. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_THAT(db_handle->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| |
| entry->Doom(); |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->doomed()); |
| entry->Close(); |
| entry = nullptr; |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| TEST_F(SqlBackendImplTest, OptimisticWriteBufferSize) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "100"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "0"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry("key", net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| // First write is smaller than the buffer size, should be optimistic. |
| auto write_buffer1 = base::MakeRefCounted<net::StringIOBuffer>("data1"); |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer1.get(), write_buffer1->size(), |
| base::DoNothing(), false), |
| static_cast<int>(write_buffer1->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer1->size()); |
| |
| // Second write exceeds the buffer size, should be pending. |
| auto write_buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(100, 'a')); |
| net::TestCompletionCallback write_cb; |
| EXPECT_EQ(entry->WriteData(1, write_buffer1->size(), write_buffer2.get(), |
| write_buffer2->size(), write_cb.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer1->size()); |
| |
| EXPECT_EQ(write_cb.WaitForResult(), static_cast<int>(write_buffer2->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, OptimisticWriteBufferLifecycle) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "100"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "0"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| TestEntryResultCompletionCallback cb; |
| disk_cache::EntryResult entry_result = |
| backend->CreateEntry("key", net::HIGHEST, cb.callback()); |
| ASSERT_THAT(entry_result.net_error(), IsOk()); |
| auto* entry = entry_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| // This write should be optimistic. |
| auto write_buffer1 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(50, 'a')); |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer1.get(), write_buffer1->size(), |
| base::DoNothing(), false), |
| static_cast<int>(write_buffer1->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer1->size()); |
| |
| // This write should also be optimistic, filling the buffer. |
| auto write_buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(50, 'b')); |
| EXPECT_EQ(entry->WriteData(1, 50, write_buffer2.get(), write_buffer2->size(), |
| base::DoNothing(), false), |
| static_cast<int>(write_buffer2->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer1->size() + write_buffer2->size()); |
| |
| // This write should be pending as the buffer is full. |
| auto write_buffer3 = base::MakeRefCounted<net::StringIOBuffer>("c"); |
| net::TestCompletionCallback write_cb3; |
| EXPECT_EQ(entry->WriteData(1, 100, write_buffer3.get(), write_buffer3->size(), |
| write_cb3.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer1->size() + write_buffer2->size()); |
| |
| // Flush the queue. This will ensure the first two optimistic writes complete |
| // on the background thread, which will free up the buffer and allow the |
| // pending write to proceed. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| // Now that the queue is flushed, the pending write should have completed. |
| EXPECT_EQ(write_cb3.WaitForResult(), static_cast<int>(write_buffer3->size())); |
| |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| // The buffer should be free again, so this write should be optimistic. |
| auto write_buffer4 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(50, 'd')); |
| EXPECT_EQ(entry->WriteData(1, 101, write_buffer4.get(), write_buffer4->size(), |
| base::DoNothing(), false), |
| static_cast<int>(write_buffer4->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer4->size()); |
| |
| entry->Close(); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, OptimisticWriteFailure) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "100"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "0"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| const std::string kInitialData = "initial data"; |
| |
| // 1. Create an entry with some data. |
| auto* entry = CreateEntryAndWriteData(backend.get(), kKey, kInitialData); |
| entry->Close(); |
| |
| // 2. Re-open the entry. |
| TestEntryResultCompletionCallback open_cb; |
| disk_cache::EntryResult open_result = open_cb.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, open_cb.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| auto* sql_entry = static_cast<SqlEntryImpl*>(entry); |
| |
| // 3. Simulate a database failure for subsequent operations. |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| |
| // 4. Perform an optimistic write, which should fail in the background. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("new data"); |
| EXPECT_EQ(entry->WriteData(1, kInitialData.size(), write_buffer.get(), |
| write_buffer->size(), base::DoNothing(), false), |
| static_cast<int>(write_buffer->size())); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| write_buffer->size()); |
| |
| // 5. Disable failure simulation. |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(false); |
| |
| // 6. Wait for the background write to fail and update the entry's state. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // 7. Verify that the entry is now in an error state. |
| EXPECT_TRUE(sql_entry->db_handle()->IsFinished()); |
| EXPECT_THAT(sql_entry->db_handle()->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| // 8. Subsequent writes should fail immediately. |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| base::DoNothing(), false), |
| net::ERR_FAILED); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| entry->Close(); |
| |
| // 9. Since the entry should have been deleted from storage, OpenEntry will |
| // fail. |
| TestEntryResultCompletionCallback open_cb2; |
| open_result = open_cb2.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, open_cb2.callback())); |
| ASSERT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| TEST_F(SqlBackendImplTest, IdleTimeEviction) { |
| const int64_t kMaxBytes = 100000; |
| const int64_t kIdleTimeHighWatermark = |
| kMaxBytes * kSqlBackendIdleTimeEvictionHighWaterMarkPermille / |
| 1000; // 92500 |
| auto buffer = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(1000, 'x')); |
| |
| auto backend = CreateBackendAndInit(kMaxBytes); |
| |
| // Add entries to be above idle time watermark. |
| int i = 0; |
| while (GetSizeOfAllEntries(*backend) <= kIdleTimeHighWatermark) { |
| TestEntryResultCompletionCallback cb; |
| EntryResult result = cb.GetResult(backend->CreateEntry( |
| base::StringPrintf("key%d", i++), net::HIGHEST, cb.callback())); |
| ASSERT_THAT(result.net_error(), IsOk()); |
| auto* entry = result.ReleaseEntry(); |
| net::TestCompletionCallback write_cb; |
| EXPECT_EQ( |
| write_cb.GetResult(entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| write_cb.callback(), false)), |
| buffer->size()); |
| entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| auto test_helper = PerformanceScenarioTestHelper::Create(); |
| // Set the state to idle. |
| test_helper->SetLoadingScenario(ScenarioScope::kGlobal, |
| LoadingScenario::kNoPageLoading); |
| test_helper->SetInputScenario(ScenarioScope::kGlobal, |
| InputScenario::kNoInput); |
| |
| // Trigger idle time eviction. |
| backend->OnBrowserIdle(); |
| |
| // The eviction process involves multiple asynchronous steps across different |
| // shards and the EvictionCandidateAggregator. Since all these steps are |
| // tracked by SqlAsyncTaskManager, a single RunUntilAllTasksCompleteForTest() |
| // is sufficient to wait for the entire process to complete. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Eviction should have run and reduced the size. |
| const int64_t kLowWatermark = |
| kMaxBytes * kSqlBackendEvictionLowWaterMarkPermille / 1000; // 9000 |
| EXPECT_LE(GetSizeOfAllEntries(*backend), kLowWatermark); |
| } |
| |
| void SqlBackendImplTest::RunDelayedPostInitializationTasksTest() { |
| auto backend = CreateBackendAndInit(); |
| auto* sql_store = backend->GetSqlStoreForTest(); |
| auto task_runners = backend->GetBackgroundTaskRunnersForTest(); |
| |
| const auto kKey1 = CacheEntryKey("key1"); |
| const auto kKey2 = CacheEntryKey("key2"); |
| const std::string kData = "some data"; |
| const auto shard_id1 = sql_store->GetShardIdForHash(kKey1.hash()); |
| const auto shard_id2 = sql_store->GetShardIdForHash(kKey2.hash()); |
| |
| // Create two entries and write some data to them. |
| auto* entry1 = CreateEntryAndWriteData(backend.get(), kKey1.string(), kData); |
| auto* entry2 = CreateEntryAndWriteData(backend.get(), kKey2.string(), kData); |
| auto db_handle1 = static_cast<SqlEntryImpl*>(entry1)->db_handle(); |
| auto db_handle2 = static_cast<SqlEntryImpl*>(entry1)->db_handle(); |
| entry1->Close(); |
| entry2->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| ASSERT_TRUE(db_handle1->GetResId().has_value()); |
| ASSERT_TRUE(db_handle2->GetResId().has_value()); |
| auto res_id1 = db_handle1->GetResId().value(); |
| auto res_id2 = db_handle2->GetResId().value(); |
| |
| // Close the backend to ensure everything is written to disk. |
| backend.reset(); |
| |
| FlushQueueInTaskRunners(task_runners); |
| |
| // This block simulates a previous session where an entry was doomed but not |
| // fully cleaned up. |
| { |
| SqlAsyncTaskManager async_task_manager; |
| auto store = std::make_unique<SqlPersistentStore>( |
| temp_dir_.GetPath(), kDefaultMaxBytes, net::CacheType::DISK_CACHE, |
| task_runners, async_task_manager, /*cleanup_tracker=*/nullptr); |
| |
| base::test::TestFuture<disk_cache::SqlPersistentStore::Error> future_init; |
| store->Initialize(future_init.GetCallback()); |
| ASSERT_EQ(future_init.Get(), disk_cache::SqlPersistentStore::Error::kOk); |
| |
| // Doom one of the entries. |
| base::test::TestFuture<SqlPersistentStore::Error> future_doom; |
| store->DoomEntry(kKey1, res_id1, /*accept_index_mismatch=*/false, |
| future_doom.GetCallback()); |
| EXPECT_EQ(future_doom.Get(), SqlPersistentStore::Error::kOk); |
| |
| store.reset(); |
| |
| FlushQueueInTaskRunners(task_runners); |
| } |
| |
| // Verify directly in the database that the blobs for the entries still exist. |
| EXPECT_EQ(OpenDatabaseAndGetBlobsCount(shard_id1, res_id1), 1); |
| EXPECT_EQ(OpenDatabaseAndGetBlobsCount(shard_id2, res_id2), 1); |
| |
| // Create and initialize a new backend. |
| backend = CreateBackend(); |
| sql_store = backend->GetSqlStoreForTest(); |
| base::test::TestFuture<int> future; |
| backend->Init(future.GetCallback()); |
| ASSERT_EQ(future.Get(), net::OK); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| if (net::features::kSqlDiskCacheLoadIndexOnInit.Get()) { |
| // When the SqlDiskCacheLoadIndexOnInit is enabled, the index should have |
| // been loaded. The doomed entry should be gone, and the other entry should |
| // be present. |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey1.hash()), |
| SqlPersistentStore::IndexState::kHashNotFound); |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey2.hash()), |
| SqlPersistentStore::IndexState::kHashFound); |
| } else { |
| // At this point, the in-memory index should not be loaded yet. |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey1.hash()), |
| SqlPersistentStore::IndexState::kNotReady); |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey2.hash()), |
| SqlPersistentStore::IndexState::kNotReady); |
| } |
| |
| // Fast forward time to trigger the delayed post-initialization tasks. |
| task_environment_.FastForwardBy(kSqlBackendPostInitializationTasksDelay); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Now, the index should be loaded even if SqlDiskCacheLoadIndexOnInit is |
| // disabled. The doomed entry should be gone, and the other entry should be |
| // present. |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey1.hash()), |
| SqlPersistentStore::IndexState::kHashNotFound); |
| EXPECT_EQ(sql_store->GetIndexStateForHash(kKey2.hash()), |
| SqlPersistentStore::IndexState::kHashFound); |
| |
| task_runners = backend->GetBackgroundTaskRunnersForTest(); |
| backend.reset(); |
| |
| FlushQueueInTaskRunners(task_runners); |
| |
| // Verify directly in the database that the blob for the doomed entry has been |
| // deleted, while the other one still exists. |
| EXPECT_EQ(OpenDatabaseAndGetBlobsCount(shard_id1, res_id1), 0); |
| EXPECT_EQ(OpenDatabaseAndGetBlobsCount(shard_id2, res_id2), 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, DelayedPostInitializationTasks) { |
| RunDelayedPostInitializationTasksTest(); |
| } |
| |
| TEST_F(SqlBackendImplTest, |
| DelayedPostInitializationTasksWithLoadIndexOnInitFeature) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitWithFeaturesAndParameters( |
| {{net::features::kDiskCacheBackendExperiment, |
| {{net::features::kDiskCacheBackendParam.name, "sql"}, |
| {net::features::kSqlDiskCacheLoadIndexOnInit.name, "true"}}}}, |
| {}); |
| RunDelayedPostInitializationTasksTest(); |
| } |
| |
| // Regression test for https://crbug.com/456384561 |
| // Tests that the dangling pointer warning does not occur when the backend is |
| // destroyed with a pending operation that holds the last reference to an entry. |
| // This test reproduces the scenario where the destruction order of |
| // `SqlBackendImpl` members (`exclusive_operation_coordinator_` before |
| // `active_entries_`) could lead to a dangling `raw_ref` in `active_entries_`. |
| TEST_F(SqlBackendImplTest, DestructionWithPendingOperationOnEntry) { |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // 2. Post a separate async exclusive operation. This ensures that the |
| // subsequent `Doom()` call will be queued and not run synchronously. |
| backend->CalculateSizeOfAllEntries(base::DoNothing()); |
| |
| // 3. Call `entry->Doom()`. This queues a `HandleDoomActiveEntryOperation` |
| // task in the `ExclusiveOperationCoordinator`. The task's callback |
| // captures a `scoped_refptr` to the `SqlEntryImpl`. |
| entry->Doom(); |
| |
| // 4. Close the entry. The last owning `scoped_refptr` is now held by the |
| // pending `Doom` operation inside the coordinator. The `active_entries_` |
| // map only holds a non-owning `raw_ref`. |
| entry->Close(); |
| entry = nullptr; |
| |
| // 5. Destroy the backend. This triggers the destruction sequence that could |
| // cause the bug if member declaration order is incorrect. |
| // a. `weak_factory_` is destroyed, invalidating the entry's `backend_` |
| // WeakPtr. |
| // b. `exclusive_operation_coordinator_` is destroyed, which destroys the |
| // pending `Doom` task. This releases the last `scoped_refptr`. |
| // c. `~SqlEntryImpl()` is called. |
| // d. Inside `~SqlEntryImpl()`, the `if (!backend_)` check now passes, |
| // causing `ReleaseActiveEntry()` to be skipped. |
| // e. `active_entries_` is destroyed, but it still contains a `raw_ref` to |
| // the now-deleted entry, causing a dangling pointer issue. |
| // If the bug exists, this test will crash here. |
| auto task_runners = backend->GetBackgroundTaskRunnersForTest(); |
| backend.reset(); |
| |
| // 6. If the bug is fixed, destruction completes safely. Run any remaining |
| // tasks to ensure clean shutdown and prevent leaks. |
| FlushQueueInTaskRunners(task_runners); |
| } |
| |
| TEST_F(SqlBackendImplTest, DoomEntryWithInMemoryIndex) { |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "my-key"; |
| const CacheEntryKey kEntryKey(kKey); |
| |
| // 1. Create an entry and close it. |
| TestEntryResultCompletionCallback create_cb; |
| disk_cache::EntryResult create_result = create_cb.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, create_cb.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| create_result.ReleaseEntry()->Close(); |
| |
| // 2. Load in-memory index. |
| ASSERT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| // 3. Verify that the entry is in the index. |
| EXPECT_EQ( |
| backend->GetSqlStoreForTest()->GetIndexStateForHash(kEntryKey.hash()), |
| SqlPersistentStore::IndexState::kHashFound); |
| |
| // 4. Doom the entry. |
| net::TestCompletionCallback cb_doom; |
| int rv_doom = backend->DoomEntry(kKey, net::HIGHEST, cb_doom.callback()); |
| |
| EXPECT_THAT(cb_doom.GetResult(rv_doom), IsOk()); |
| |
| // 5. Verify that the entry is removed from the in-memory index. |
| EXPECT_EQ( |
| backend->GetSqlStoreForTest()->GetIndexStateForHash(kEntryKey.hash()), |
| SqlPersistentStore::IndexState::kHashNotFound); |
| |
| // 6. Verify that the entry is gone. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| // Tests that dooming a non-existent key whose hash collides with an existing |
| // entry's key does not affect the existing entry. The in-memory index is keyed |
| // by hash only, so a single-entry hash bucket may resolve to a different key's |
| // `res_id`; the backend must still keep the existing entry openable and avoid |
| // creating duplicate rows for it. |
| TEST_F(SqlBackendImplTest, DoomEntryWithInMemoryIndexHashCollision) { |
| // Two distinct keys with the same `CacheEntryKey::hash()`. |
| const std::string kExistingKey = "colliding-key-2018"; |
| const std::string kCollidingKey = "colliding-key-3000"; |
| const CacheEntryKey kExistingEntryKey(kExistingKey); |
| ASSERT_EQ(kExistingEntryKey.hash(), CacheEntryKey(kCollidingKey).hash()); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| // 1. Create an entry for `kExistingKey` and close it so it is no longer |
| // active. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kExistingKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| create_result.ReleaseEntry()->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // 2. Load the in-memory index. `kExistingKey` is the sole occupant of its |
| // hash bucket. |
| ASSERT_TRUE(LoadInMemoryIndex(*backend)); |
| ASSERT_EQ(backend->GetSqlStoreForTest()->GetIndexStateForHash( |
| kExistingEntryKey.hash()), |
| SqlPersistentStore::IndexState::kHashFound); |
| |
| // 3. Doom `kCollidingKey`, which does not exist but shares its hash with |
| // `kExistingKey`. |
| net::TestCompletionCallback cb_doom; |
| EXPECT_THAT(cb_doom.GetResult(backend->DoomEntry(kCollidingKey, net::HIGHEST, |
| cb_doom.callback())), |
| IsOk()); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // 4. The existing entry must remain in the in-memory index. |
| EXPECT_EQ(backend->GetSqlStoreForTest()->GetIndexStateForHash( |
| kExistingEntryKey.hash()), |
| SqlPersistentStore::IndexState::kHashFound); |
| |
| // 5. Opening `kExistingKey` must still succeed. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kExistingKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| open_result.ReleaseEntry()->Close(); |
| |
| // 6. OpenOrCreateEntry must open the existing entry rather than creating a |
| // duplicate row. |
| TestEntryResultCompletionCallback cb_ooc; |
| disk_cache::EntryResult ooc_result = |
| cb_ooc.GetResult(backend->OpenOrCreateEntry(kExistingKey, net::HIGHEST, |
| cb_ooc.callback())); |
| ASSERT_THAT(ooc_result.net_error(), IsOk()); |
| EXPECT_TRUE(ooc_result.opened()); |
| ooc_result.ReleaseEntry()->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::test::TestFuture<int32_t> count_future; |
| EXPECT_EQ(backend->GetEntryCount(count_future.GetCallback()), |
| base::unexpected(net::ERR_IO_PENDING)); |
| EXPECT_EQ(count_future.Get(), 1); |
| } |
| |
| TEST_F(SqlBackendImplTest, SetDataHintsAndDoomAndWriteOptimistically) { |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "my-key"; |
| const uint8_t kUnusableHint = 1; |
| |
| // 1. Create an entry. |
| TestEntryResultCompletionCallback cb_create; |
| EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // 2. Set an in-memory hint. |
| entry->SetEntryInMemoryData(kUnusableHint); |
| EXPECT_EQ(backend->GetEntryInMemoryData(kKey), kUnusableHint); |
| entry->Close(); |
| |
| // 3. While write is in flight, it should still be returned from |
| // `in_flight_entry_modifications_`. |
| EXPECT_EQ(backend->GetEntryInMemoryData(kKey), kUnusableHint); |
| |
| // 4. Call OnBrowserIdle() to trigger in-memory index loading. |
| backend->OnBrowserIdle(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // 5. Verify the hint is set in the backend. |
| EXPECT_EQ(backend->GetEntryInMemoryData(kKey), kUnusableHint); |
| |
| // 6. Doom the entry. |
| base::test::TestFuture<int> doom_future; |
| int doom_rv = |
| backend->DoomEntry(kKey, net::HIGHEST, doom_future.GetCallback()); |
| EXPECT_EQ(doom_rv, net::ERR_IO_PENDING); |
| |
| // 7. OpenOrCreateEntry should create a new entry. |
| TestEntryResultCompletionCallback cb_open_or_create; |
| EntryResult open_or_create_result = |
| cb_open_or_create.GetResult(backend->OpenOrCreateEntry( |
| kKey, net::HIGHEST, cb_open_or_create.callback())); |
| ASSERT_THAT(open_or_create_result.net_error(), IsOk()); |
| EXPECT_FALSE(open_or_create_result.opened()); |
| |
| open_or_create_result.ReleaseEntry()->Close(); |
| EXPECT_EQ(doom_future.Get(), net::OK); |
| } |
| |
| TEST_F(SqlBackendImplTest, SetEntryDataHintsWithSpeculativeCreateEntryFailure) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. This should return immediately with a speculatively |
| // created entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // When WriteData is called with data larger than |
| // kSqlDiskCacheMaxWriteBufferTotalSize, SqlBackendImpl starts the optimistic |
| // write process, and the `db_handle` enters the creating state. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>("1234567890A"); |
| ASSERT_GT(write_buffer->size(), |
| net::features::kSqlDiskCacheMaxWriteBufferTotalSize.Get()); |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| base::DoNothing(), false), |
| write_buffer->size()); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| // After flushing all DB tasks, the db_handle should hold a kFailedForTesting |
| // error. |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_THAT(db_handle->GetError(), |
| SqlPersistentStore::Error::kFailedForTesting); |
| |
| // Set an in-memory hint. This should fail silently because the entry has an |
| // error. |
| const uint8_t kUnusableHint = 1; |
| entry->SetEntryInMemoryData(kUnusableHint); |
| entry->Close(); |
| |
| // Flush the queue to make sure the SetEntryInMemoryData operation is |
| // processed. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Verify the hint is not set in the backend. |
| EXPECT_EQ(backend->GetEntryInMemoryData(kKey), 0); |
| } |
| |
| // Regression test for https://crbug.com/473912285. |
| // Tests that an optimistic write failure does not cause an index mismatch error |
| // (which can lead to a CHECK failure in strict mode) if the entry has already |
| // been doomed by DoomAllEntries. |
| TEST_F(SqlBackendImplTest, OptimisticWriteIndexMismatchAfterDoomAllEntries) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->EnableStrictCorruptionCheckForTesting(); |
| |
| // 1. Create a speculative entry. |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| // 2. Doom all entries. |
| net::TestCompletionCallback doom_cb; |
| int rv_doom = backend->DoomAllEntries(doom_cb.callback()); |
| EXPECT_EQ(rv_doom, net::ERR_IO_PENDING); |
| EXPECT_THAT(doom_cb.WaitForResult(), IsOk()); |
| |
| // 3. Set DB failure to force OptimisticWrite to fail. |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| |
| // 4. Write data optimistically. |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("data"); |
| int rv_write = |
| entry->WriteData(1, 100, buffer.get(), 4, base::DoNothing(), false); |
| EXPECT_EQ(rv_write, 4); |
| |
| // 4. Wait for operations to complete. |
| // Previously, this would trigger an index mismatch error (and a CHECK failure |
| // in RecordIndexMismatch). |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBuffering) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1024"}, |
| {net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "250"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Write small chunk, should be buffered. |
| auto buffer1 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(100, 'a')); |
| EXPECT_EQ(entry->WriteData(1, 0, buffer1.get(), buffer1->size(), |
| base::DoNothing(), false), |
| buffer1->size()); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), buffer1->size()); |
| |
| // Write another small chunk, should be buffered. |
| auto buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(100, 'b')); |
| EXPECT_EQ(entry->WriteData(1, 100, buffer2.get(), buffer2->size(), |
| base::DoNothing(), false), |
| buffer2->size()); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), |
| buffer1->size() + buffer2->size()); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| // Write exceeding per-entry limit, should trigger flush of previous buffer. |
| // The new data is too large to buffer, so it should be written directly. |
| std::string large_data(2000, 'c'); |
| auto buffer3 = base::MakeRefCounted<net::StringIOBuffer>(large_data); |
| net::TestCompletionCallback cb_write; |
| int rv = entry->WriteData(1, 200, buffer3.get(), buffer3->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(rv, net::ERR_IO_PENDING); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), |
| buffer1->size() + buffer2->size()); |
| |
| entry->Close(); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingReadFromBuffer) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1024"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Buffer some data. |
| std::string data = "hello world"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| entry->WriteData(1, 0, buffer.get(), buffer->size(), base::DoNothing(), |
| false); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), data.size()); |
| |
| // Read it back. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(data.size()); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| // Should be synchronous |
| EXPECT_EQ(rv_read, static_cast<int>(data.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), data.size()), data); |
| |
| // Buffer should still be there. |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), data.size()); |
| |
| entry->Close(); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingReadOverlapFlush) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1024"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Buffer some data. |
| std::string data = "hello world"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| // Write at 0 to ensure buffering (sequential). |
| entry->WriteData(1, 0, buffer.get(), buffer->size(), base::DoNothing(), |
| false); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), data.size()); |
| |
| // Read range that overlaps but is larger than buffer (e.g. from 0 to 20) |
| // This should force flush. |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(20); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| // Should return data.size() (11). |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(data.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), data.size()), data); |
| |
| // Buffer should be flushed. |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingGlobalLimit) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "100"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1000"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| // Create entry 1 |
| TestEntryResultCompletionCallback cb_create1; |
| disk_cache::EntryResult create_result1 = cb_create1.GetResult( |
| backend->CreateEntry("key1", net::HIGHEST, cb_create1.callback())); |
| ASSERT_THAT(create_result1.net_error(), IsOk()); |
| auto* entry1 = create_result1.ReleaseEntry(); |
| |
| // Create entry 2 |
| TestEntryResultCompletionCallback cb_create2; |
| disk_cache::EntryResult create_result2 = cb_create2.GetResult( |
| backend->CreateEntry("key2", net::HIGHEST, cb_create2.callback())); |
| ASSERT_THAT(create_result2.net_error(), IsOk()); |
| auto* entry2 = create_result2.ReleaseEntry(); |
| |
| // Write 60 bytes to entry 1. Buffered. |
| auto buffer1 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(60, 'a')); |
| entry1->WriteData(1, 0, buffer1.get(), buffer1->size(), base::DoNothing(), |
| false); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 60); |
| |
| // Write 60 bytes to entry 2. Should flush entry 2 immediately because global |
| // limit (100) would be exceeded (60+60=120). |
| auto buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(60, 'b')); |
| net::TestCompletionCallback cb_write; |
| int rv = entry2->WriteData(1, 0, buffer2.get(), buffer2->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(cb_write.GetResult(rv), 60); |
| |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), |
| 60); // Entry 1 still buffered. |
| |
| entry1->Close(); |
| entry2->Close(); |
| |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 60); |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingFlushOnClose) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1024"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Buffer some data. |
| std::string data = "hello world"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| entry->WriteData(1, 0, buffer.get(), buffer->size(), base::DoNothing(), |
| false); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), data.size()); |
| |
| entry->Close(); |
| |
| // Closing should asynchronously flush buffer. |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), data.size()); |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| |
| // Verify data on disk by opening again. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(data.size()); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 0, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), static_cast<int>(data.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), data.size()), data); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingOptimisticBoundary) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1000"}, |
| {net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "3000"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // 1. Write 500 bytes. Should be buffered. |
| auto buffer1 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(500, 'a')); |
| EXPECT_EQ(entry->WriteData(1, 0, buffer1.get(), buffer1->size(), |
| base::DoNothing(), false), |
| buffer1->size()); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 500); |
| // Optimistic buffer usage: 0 (buffered in entry, not sent to backend) |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| // 2. Write 600 bytes. 500 + 600 > 1000. |
| // Should flush buffer (500) -> Optimistic (size 500). |
| // Then write new data (600) to the new buffer. |
| auto buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(600, 'b')); |
| EXPECT_EQ(entry->WriteData(1, 500, buffer2.get(), buffer2->size(), |
| base::DoNothing(), false), |
| buffer2->size()); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 600); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 500); |
| |
| // 3. Write 2000 bytes. 2000 > 1000. |
| // Since 2000 > 1000 (entry limit), it CANNOT be buffered. |
| // It flushes the previous buffer (600) -> Optimistic: 500 + 600 = 1100 <= |
| // 3000. Then writes 2000 bytes directly (async). |
| auto buffer3 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(2000, 'c')); |
| net::TestCompletionCallback cb_write3; |
| EXPECT_EQ(entry->WriteData(1, 1100, buffer3.get(), buffer3->size(), |
| cb_write3.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 1100); |
| EXPECT_EQ(cb_write3.WaitForResult(), 2000); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| // 4. Write 9000 bytes. |
| // Total buffer usage: 0 + 9000. |
| // Cannot buffer because 9000 > 1000 (entry limit). |
| // Writes 9000 bytes directly. |
| // Optimistic check for new write: 0 + 9000 > 3000. Non-optimistic write. |
| // Should be pending. |
| auto buffer4 = |
| base::MakeRefCounted<net::StringIOBuffer>(std::string(9000, 'd')); |
| net::TestCompletionCallback cb_write4; |
| EXPECT_EQ(entry->WriteData(1, 3100, buffer4.get(), buffer4->size(), |
| cb_write4.callback(), false), |
| net::ERR_IO_PENDING); |
| EXPECT_EQ(cb_write4.WaitForResult(), static_cast<int>(buffer4->size())); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| EXPECT_EQ(backend->GetOptimisticWriteBufferTotalSizeForTesting(), 0); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, WriteBufferingReadAcrossChunks) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "10240"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "1024"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Write chunk 1: "AAAAA" |
| std::string data1 = "AAAAA"; |
| auto buffer1 = base::MakeRefCounted<net::StringIOBuffer>(data1); |
| entry->WriteData(1, 0, buffer1.get(), buffer1->size(), base::DoNothing(), |
| false); |
| |
| // Write chunk 2: "BBBBB" |
| std::string data2 = "BBBBB"; |
| auto buffer2 = base::MakeRefCounted<net::StringIOBuffer>(data2); |
| entry->WriteData(1, 5, buffer2.get(), buffer2->size(), base::DoNothing(), |
| false); |
| |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), |
| data1.size() + data2.size()); |
| |
| // Read across chunks: Offset 3, Length 4. Should get "AABB". |
| auto read_buffer = base::MakeRefCounted<net::IOBufferWithSize>(4); |
| net::TestCompletionCallback cb_read; |
| int rv_read = entry->ReadData(1, 3, read_buffer.get(), read_buffer->size(), |
| cb_read.callback()); |
| EXPECT_EQ(cb_read.GetResult(rv_read), 4); |
| EXPECT_EQ(std::string_view(read_buffer->data(), 4), "AABB"); |
| |
| // Buffer should still be there. |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), |
| data1.size() + data2.size()); |
| |
| entry->Close(); |
| |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), 0); |
| } |
| |
| TEST_F(SqlBackendImplTest, CombinedWriteAndMetadataUpdate) { |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // Write to stream 0 (header) - this will be buffered in SqlEntryImpl. |
| std::string header_data = "header"; |
| auto header_buffer = base::MakeRefCounted<net::StringIOBuffer>(header_data); |
| entry->WriteData(0, 0, header_buffer.get(), header_buffer->size(), |
| base::DoNothing(), false); |
| |
| // Write to stream 1 (body) - this will be buffered in write buffer. |
| std::string body_data = "body"; |
| auto body_buffer = base::MakeRefCounted<net::StringIOBuffer>(body_data); |
| entry->WriteData(1, 0, body_buffer.get(), body_buffer->size(), |
| base::DoNothing(), false); |
| |
| // Close the entry. This should trigger a single WriteEntryDataAndMetadata |
| // call that persists both header and body. |
| entry->Close(); |
| |
| // Flush background tasks. |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Re-open and verify. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| |
| // Check header. |
| auto read_header_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(header_data.size()); |
| net::TestCompletionCallback cb_read_header; |
| int rv_read_header = |
| entry->ReadData(0, 0, read_header_buffer.get(), |
| read_header_buffer->size(), cb_read_header.callback()); |
| EXPECT_EQ(cb_read_header.GetResult(rv_read_header), |
| static_cast<int>(header_data.size())); |
| EXPECT_EQ(std::string_view(read_header_buffer->data(), header_data.size()), |
| header_data); |
| |
| // Check body. |
| auto read_body_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(body_data.size()); |
| net::TestCompletionCallback cb_read_body; |
| int rv_read_body = |
| entry->ReadData(1, 0, read_body_buffer.get(), read_body_buffer->size(), |
| cb_read_body.callback()); |
| EXPECT_EQ(cb_read_body.GetResult(rv_read_body), |
| static_cast<int>(body_data.size())); |
| EXPECT_EQ(std::string_view(read_body_buffer->data(), body_data.size()), |
| body_data); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadCaching) { |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // 1. Write some data to stream 1. |
| std::string data = "0123456789ABCDEF"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(cb_write.GetResult(rv_write), static_cast<int>(data.size())); |
| |
| // Close and re-open the entry to ensure data is written to the DB and we |
| // don't read from the write buffer. |
| entry->Close(); |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| |
| // 2. Read only the first 5 bytes. |
| // The backend should read the whole blob (since it's a single blob in DB) |
| // and cache the remaining 11 bytes. |
| auto read_buffer1 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| net::TestCompletionCallback cb_read1; |
| int rv_read1 = |
| entry->ReadData(1, 0, read_buffer1.get(), 5, cb_read1.callback()); |
| EXPECT_EQ(cb_read1.GetResult(rv_read1), 5); |
| EXPECT_EQ(std::string_view(read_buffer1->data(), 5), "01234"); |
| |
| // Verify that the read_cache_buffer is populated. |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->read_cache_buffer_for_test()); |
| EXPECT_EQ( |
| static_cast<SqlEntryImpl*>(entry)->read_cache_buffer_offset_for_test(), |
| 5); |
| |
| // 3. Read the next 5 bytes. |
| // This should be fulfilled from the cache synchronously (no IO pending). |
| auto read_buffer2 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| int rv_read2 = |
| entry->ReadData(1, 5, read_buffer2.get(), 5, base::DoNothing()); |
| // If it's cached, it returns immediately. |
| EXPECT_EQ(rv_read2, 5); |
| EXPECT_EQ(std::string_view(read_buffer2->data(), 5), "56789"); |
| |
| // 4. Read crossing the cache boundary. |
| // The cache has "56789ABCDEF" (offset 5 to 16). |
| // Request 5 bytes from offset 14: "EF" + 3 more. |
| // It should return 2 bytes synchronously if it uses partial cache, |
| // or return the whole thing if it triggers a new read. |
| // Current implementation returns copy_size = min(buf_len, cache_end - |
| // offset). So it should return 2 bytes synchronously. |
| auto read_buffer3 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| int rv_read3 = |
| entry->ReadData(1, 14, read_buffer3.get(), 5, base::DoNothing()); |
| EXPECT_EQ(rv_read3, 2); |
| EXPECT_EQ(std::string_view(read_buffer3->data(), 2), "EF"); |
| |
| // 5. Write data should invalidate read cache. |
| entry->WriteData(1, 16, buffer.get(), 1, base::DoNothing(), false); |
| auto read_buffer4 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| int rv_read4 = |
| entry->ReadData(1, 5, read_buffer4.get(), 5, base::DoNothing()); |
| // Now it should be pending because cache was invalidated. |
| EXPECT_EQ(rv_read4, net::ERR_IO_PENDING); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadCachingSparse) { |
| auto backend = CreateBackendAndInit(); |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry("key", net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| |
| // 1. Write some data to stream 1. |
| std::string data = "0123456789ABCDEF"; |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| net::TestCompletionCallback cb_write; |
| int rv_write = entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| cb_write.callback(), false); |
| EXPECT_EQ(cb_write.GetResult(rv_write), static_cast<int>(data.size())); |
| |
| // Close and re-open the entry to ensure data is written to the DB and we |
| // don't read from the write buffer. |
| entry->Close(); |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry("key", net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| entry = open_result.ReleaseEntry(); |
| |
| // 2. Read sparse data. |
| auto read_buffer1 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| net::TestCompletionCallback cb_read1; |
| int rv_read1 = |
| entry->ReadSparseData(0, read_buffer1.get(), 5, cb_read1.callback()); |
| EXPECT_EQ(cb_read1.GetResult(rv_read1), 5); |
| EXPECT_EQ(std::string_view(read_buffer1->data(), 5), "01234"); |
| |
| // Verify that the read_cache_buffer IS populated even for sparse reads. |
| EXPECT_TRUE(static_cast<SqlEntryImpl*>(entry)->read_cache_buffer_for_test()); |
| EXPECT_EQ( |
| static_cast<SqlEntryImpl*>(entry)->read_cache_buffer_offset_for_test(), |
| 5); |
| |
| // Subsequent read (could be normal or sparse) should use the cache. |
| auto read_buffer2 = base::MakeRefCounted<net::IOBufferWithSize>(5); |
| int rv_read2 = |
| entry->ReadData(1, 5, read_buffer2.get(), 5, base::DoNothing()); |
| EXPECT_EQ(rv_read2, 5); |
| EXPECT_EQ(std::string_view(read_buffer2->data(), 5), "56789"); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadCachingGlobalLimit) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxReadBufferTotalSize.name, "90"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| |
| std::string data(60, 'a'); |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(data); |
| |
| // Create entry 1 |
| TestEntryResultCompletionCallback cb_create1; |
| disk_cache::EntryResult create_result1 = cb_create1.GetResult( |
| backend->CreateEntry("key1", net::HIGHEST, cb_create1.callback())); |
| ASSERT_THAT(create_result1.net_error(), IsOk()); |
| auto* entry1 = create_result1.ReleaseEntry(); |
| entry1->WriteData(1, 0, buffer.get(), buffer->size(), base::DoNothing(), |
| false); |
| entry1->Close(); |
| |
| // Create entry 2 |
| TestEntryResultCompletionCallback cb_create2; |
| disk_cache::EntryResult create_result2 = cb_create2.GetResult( |
| backend->CreateEntry("key2", net::HIGHEST, cb_create2.callback())); |
| ASSERT_THAT(create_result2.net_error(), IsOk()); |
| auto* entry2 = create_result2.ReleaseEntry(); |
| entry2->WriteData(1, 0, buffer.get(), buffer->size(), base::DoNothing(), |
| false); |
| entry2->Close(); |
| |
| // Open both |
| TestEntryResultCompletionCallback cb_open1; |
| auto* entry1_open = cb_open1 |
| .GetResult(backend->OpenEntry("key1", net::HIGHEST, |
| cb_open1.callback())) |
| .ReleaseEntry(); |
| TestEntryResultCompletionCallback cb_open2; |
| auto* entry2_open = cb_open2 |
| .GetResult(backend->OpenEntry("key2", net::HIGHEST, |
| cb_open2.callback())) |
| .ReleaseEntry(); |
| |
| // Read from entry 1. 10 bytes. 50 bytes cached. Total 50. |
| auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(10); |
| net::TestCompletionCallback cb_read1; |
| int rv1 = |
| entry1_open->ReadData(1, 0, read_buf.get(), 10, cb_read1.callback()); |
| EXPECT_EQ(cb_read1.GetResult(rv1), 10); |
| EXPECT_TRUE( |
| static_cast<SqlEntryImpl*>(entry1_open)->read_cache_buffer_for_test()); |
| |
| // Read from entry 2. 10 bytes. 50 bytes to cache. Total would be 100 > 90. |
| // Should NOT be cached. |
| net::TestCompletionCallback cb_read2; |
| int rv2 = |
| entry2_open->ReadData(1, 0, read_buf.get(), 10, cb_read2.callback()); |
| EXPECT_EQ(cb_read2.GetResult(rv2), 10); |
| EXPECT_FALSE( |
| static_cast<SqlEntryImpl*>(entry2_open)->read_cache_buffer_for_test()); |
| |
| entry1_open->Close(); // Releases 50 bytes. Total 0. |
| |
| // Read again from entry 2 (offset 10). |
| // This will trigger a new read from DB. Since total is 0, the remaining 40 |
| // bytes (60-10-10) should be cached? |
| net::TestCompletionCallback cb_read3; |
| int rv3 = |
| entry2_open->ReadData(1, 10, read_buf.get(), 10, cb_read3.callback()); |
| EXPECT_EQ(cb_read3.GetResult(rv3), 10); |
| EXPECT_TRUE( |
| static_cast<SqlEntryImpl*>(entry2_open)->read_cache_buffer_for_test()); |
| |
| entry2_open->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, GetAvailableRangeWithBufferedWrite) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheOptimisticWriteBufferSize.name, "1024"}, |
| {net::features::kSqlDiskCacheMaxWriteBufferTotalSize.name, "1024"}}); |
| |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "my-key"; |
| const std::string kData = "some data"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| // Write data. This should be buffered because it's small and write buffering |
| // is enabled. |
| auto write_buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| EXPECT_EQ(entry->WriteData(1, 0, write_buffer.get(), write_buffer->size(), |
| base::DoNothing(), false), |
| static_cast<int>(write_buffer->size())); |
| |
| // Verify it is buffered. |
| EXPECT_EQ(backend->GetWriteBufferTotalSizeForTesting(), write_buffer->size()); |
| |
| // Check GetAvailableRange. |
| base::test::TestFuture<const RangeResult&> range_future; |
| RangeResult result = |
| entry->GetAvailableRange(0, kData.size(), range_future.GetCallback()); |
| |
| ASSERT_THAT(result.net_error, IsError(net::ERR_IO_PENDING)); |
| |
| result = range_future.Get(); |
| EXPECT_THAT(result.net_error, IsOk()); |
| EXPECT_EQ(result.start, 0); |
| EXPECT_EQ(result.available_len, static_cast<int>(kData.size())); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, CreateIteratorFlushesBuffers) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| const std::string kData = "data"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| EXPECT_EQ(entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| base::DoNothing(), false), |
| static_cast<int>(buffer->size())); |
| |
| // The entry is in kInitial state and has buffered data. |
| // It is NOT in the DB yet. |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| |
| auto iter = backend->CreateIterator(); |
| |
| // CreateIterator should have triggered FlushBuffer(true). |
| // Which starts the creation in DB. |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| TestEntryResultCompletionCallback cb_iter; |
| EntryResult iter_res = iter->OpenNextEntry(cb_iter.callback()); |
| |
| iter_res = cb_iter.GetResult(std::move(iter_res)); |
| ASSERT_THAT(iter_res.net_error(), IsOk()); |
| auto* entry_from_iter = iter_res.ReleaseEntry(); |
| EXPECT_EQ(entry_from_iter->GetKey(), kKey); |
| EXPECT_EQ(entry_from_iter->GetDataSize(1), static_cast<int>(kData.size())); |
| |
| entry->Close(); |
| entry_from_iter->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, GetEntryCountFlushesBuffers) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // The entry is in kInitial state and has buffered data. |
| // It is NOT in the DB yet. |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| base::test::TestFuture<int32_t> future; |
| |
| EXPECT_EQ(backend->GetEntryCount(future.GetCallback()), |
| base::unexpected(net::ERR_IO_PENDING)); |
| |
| // GetEntryCount should have triggered FlushBuffer(true). |
| // Which starts the creation in DB. |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| CHECK_EQ(future.Get(), 1); |
| EXPECT_TRUE(db_handle->IsFinished()); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, CalculateSizeOfEntriesBetweenFlushesBuffers) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| const std::string kKey = "my-key"; |
| const std::string kData = "data"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| EXPECT_EQ(entry->WriteData(1, 0, buffer.get(), buffer->size(), |
| base::DoNothing(), false), |
| static_cast<int>(buffer->size())); |
| |
| // The entry is in kInitial state and has buffered data. |
| // It is NOT in the DB yet. |
| EXPECT_TRUE(db_handle->IsInitialState()); |
| base::test::TestFuture<int64_t> future; |
| |
| EXPECT_EQ(backend->CalculateSizeOfEntriesBetween( |
| base::Time::Min(), base::Time::Max(), future.GetCallback()), |
| net::ERR_IO_PENDING); |
| |
| // CalculateSizeOfEntriesBetween should have triggered FlushBuffer(true). |
| // Which starts the creation in DB. |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| EXPECT_EQ(future.Get(), |
| kKey.length() + kData.length() + kSqlBackendStaticResourceSize); |
| EXPECT_TRUE(db_handle->IsFinished()); |
| |
| entry->Close(); |
| } |
| |
| // Tests a race condition where Doom runs while a WriteData operation |
| // is pending (blocked by another operation) and the entry is in 'Creating' |
| // state. |
| TEST_F(SqlBackendImplTest, AsyncDoomEntryAndWrite) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "250"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, base::DoNothing()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // Start an exclusive operation (CalculateSizeOfAllEntries). |
| // This will block subsequent operations. |
| net::TestInt64CompletionCallback cb_calculate; |
| EXPECT_EQ(backend->CalculateSizeOfAllEntries(cb_calculate.callback()), |
| net::ERR_IO_PENDING); |
| |
| // Doom the entry. |
| entry->Doom(); |
| |
| // Write data to the entry. |
| // Since it is larger than kSqlDiskCacheMaxWriteBufferSizePerEntry, the task |
| // for writing to the DB (WriteData) is queued. |
| const int kDataSize = 1024; |
| auto kData = std::string(kDataSize, 'a'); |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData); |
| EXPECT_EQ( |
| entry->WriteData(1, 0, buffer.get(), kDataSize, base::DoNothing(), false), |
| kDataSize); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| // The CalculateSizeOfAllEntries should complete. |
| EXPECT_GE(cb_calculate.WaitForResult(), 0); |
| |
| // Verify that the data is readable. |
| ReadAndVerifyData(entry, kData); |
| |
| entry->Close(); |
| |
| // Verify that the entry is not found. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| // Tests a race condition where Doom runs while a WriteEntryDataAndMetadata |
| // operation is pending (blocked by another operation) and the entry is in |
| // 'Creating' state. |
| TEST_F(SqlBackendImplTest, AsyncDoomEntryAndFlushBuffer) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| net::features::kDiskCacheBackendExperiment, |
| {{net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.name, "250"}}); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "my-key"; |
| |
| // Create an entry. |
| disk_cache::EntryResult create_result = |
| backend->CreateEntry(kKey, net::HIGHEST, base::DoNothing()); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| auto db_handle = static_cast<SqlEntryImpl*>(entry)->db_handle(); |
| |
| // Start an exclusive operation (CalculateSizeOfAllEntries). |
| // This will block subsequent operations. |
| net::TestInt64CompletionCallback cb_calculate; |
| EXPECT_EQ(backend->CalculateSizeOfAllEntries(cb_calculate.callback()), |
| net::ERR_IO_PENDING); |
| |
| // Doom the entry. |
| entry->Doom(); |
| |
| // Calling CreateIterator() queues the task for writing to the DB |
| // (WriteEntryDataAndMetadata). |
| auto iter = backend->CreateIterator(); |
| EXPECT_TRUE(db_handle->IsCreatingState()); |
| |
| // The CalculateSizeOfAllEntries should complete. |
| EXPECT_GE(cb_calculate.WaitForResult(), 0); |
| |
| TestEntryResultCompletionCallback cb_iter; |
| EntryResult iter_res = iter->OpenNextEntry(cb_iter.callback()); |
| |
| // The iterator shouldn't see the entry. |
| iter_res = cb_iter.GetResult(std::move(iter_res)); |
| ASSERT_THAT(iter_res.net_error(), IsError(net::ERR_FAILED)); |
| |
| entry->Close(); |
| |
| // Verify that the entry is not found. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| EXPECT_THAT(open_result.net_error(), IsError(net::ERR_FAILED)); |
| } |
| |
| void SqlBackendImplTest::RunSparseDataExceedsMaxFileSizeTest(bool doom_entry) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->EnableStrictCorruptionCheckForTesting(); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| if (doom_entry) { |
| entry->Doom(); |
| } |
| |
| const int chunk_size = 32 * 1024; |
| const int num_chunks = backend->GetSqlStoreForTest()->MaxSize() / chunk_size; |
| auto buf = base::MakeRefCounted<net::IOBufferWithSize>(chunk_size); |
| std::ranges::fill(buf->span(), 'a'); |
| |
| base::HistogramTester histogram_tester; |
| |
| for (int i = 0; i < num_chunks; ++i) { |
| net::TestCompletionCallback cb; |
| EXPECT_EQ(cb.GetResult(entry->WriteSparseData(i * chunk_size, buf.get(), |
| chunk_size, cb.callback())), |
| chunk_size); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| // Truncating older sparse data prevents a single entry from exceeding the |
| // cache size limit and causing excessive evictions on every write. Ensure |
| // eviction was not triggered. |
| histogram_tester.ExpectTotalCount( |
| "Net.SqlDiskCache.Backend.RunEviction.ScannedEntriesCount.Success", 0); |
| |
| // Check if the first chunk is truncated. |
| TestRangeResultCompletionCallback cb_range; |
| EXPECT_EQ(cb_range |
| .GetResult(entry->GetAvailableRange(0, chunk_size, |
| cb_range.callback())) |
| .available_len, |
| 0); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, SparseDataExceedsMaxFileSize) { |
| RunSparseDataExceedsMaxFileSizeTest(/*doom_entry=*/false); |
| } |
| |
| TEST_F(SqlBackendImplTest, SparseDataExceedsMaxFileSizeDoomedEntry) { |
| RunSparseDataExceedsMaxFileSizeTest(/*doom_entry=*/true); |
| } |
| |
| TEST_F(SqlBackendImplTest, SparseDataExceedsMaxFileSizeBackwards) { |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| backend->EnableStrictCorruptionCheckForTesting(); |
| const std::string kKey = "my-key"; |
| |
| TestEntryResultCompletionCallback cb_create; |
| disk_cache::EntryResult create_result = cb_create.GetResult( |
| backend->CreateEntry(kKey, net::HIGHEST, cb_create.callback())); |
| ASSERT_THAT(create_result.net_error(), IsOk()); |
| auto* entry = create_result.ReleaseEntry(); |
| ASSERT_TRUE(entry); |
| |
| const int chunk_size = 32 * 1024; |
| const int num_chunks = backend->GetSqlStoreForTest()->MaxSize() / chunk_size; |
| auto buf = base::MakeRefCounted<net::IOBufferWithSize>(chunk_size); |
| std::ranges::fill(buf->span(), 'a'); |
| |
| base::HistogramTester histogram_tester; |
| |
| // Write chunks in reverse order. |
| for (int i = num_chunks - 1; i >= 0; --i) { |
| net::TestCompletionCallback cb; |
| EXPECT_EQ(cb.GetResult(entry->WriteSparseData(i * chunk_size, buf.get(), |
| chunk_size, cb.callback())), |
| chunk_size); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| histogram_tester.ExpectTotalCount( |
| "Net.SqlDiskCache.Backend.RunEviction.ScannedEntriesCount.Success", 0); |
| |
| // The truncation logic trims data before and after the current write when |
| // the size limit is exceeded. Since we wrote backwards, the last written |
| // chunk was at offset 0. Thus, the previously written chunks at higher |
| // offsets were truncated to keep the total size within the limit. |
| TestRangeResultCompletionCallback cb_range; |
| EXPECT_EQ( |
| cb_range |
| .GetResult(entry->GetAvailableRange((num_chunks - 1) * chunk_size, |
| chunk_size, cb_range.callback())) |
| .available_len, |
| 0); |
| |
| entry->Close(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadFromSharedCacheViaOpenEntry) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| net::features::kRendererAccessibleHttpCache); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "shared-cache-key"; |
| const std::string kData = "Data stored in shared cache"; |
| |
| SqlSharedCacheResourceId expected_resource_id = |
| CreateEntryInSharedCache(*backend, kKey, kData); |
| ASSERT_TRUE(expected_resource_id.db_id.value()); |
| |
| // Now open the entry using OpenEntry. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| auto* opened_entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(opened_entry); |
| |
| // Check db_handle has shared_cache_resource_id. |
| auto* opened_sql_entry = static_cast<SqlEntryImpl*>(opened_entry); |
| EXPECT_TRUE( |
| opened_sql_entry->db_handle()->shared_cache_resource_id().has_value()); |
| EXPECT_EQ(opened_sql_entry->db_handle()->shared_cache_resource_id()->db_id, |
| expected_resource_id.db_id); |
| EXPECT_EQ(opened_sql_entry->db_handle()->shared_cache_resource_id()->row_id, |
| expected_resource_id.row_id); |
| |
| // Read data stream 1 from opened entry and verify content matches kData. |
| auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(kData.size()); |
| net::TestCompletionCallback cb_read; |
| EXPECT_EQ(cb_read.GetResult(opened_entry->ReadData( |
| 1, 0, read_buf.get(), kData.size(), cb_read.callback())), |
| static_cast<int>(kData.size())); |
| EXPECT_EQ(std::string_view(read_buf->data(), kData.size()), kData); |
| |
| opened_entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadFromSharedCacheViaOpenNextEntry) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| net::features::kRendererAccessibleHttpCache); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "shared-cache-iter-key"; |
| const std::string kData = "Data stored in shared cache for iterator"; |
| |
| SqlSharedCacheResourceId expected_resource_id = |
| CreateEntryInSharedCache(*backend, kKey, kData); |
| ASSERT_TRUE(expected_resource_id.db_id.value()); |
| |
| // Open entry via iterator (CreateIterator & OpenNextEntry). |
| auto iterator = backend->CreateIterator(); |
| ASSERT_TRUE(iterator); |
| |
| TestEntryResultCompletionCallback cb_next; |
| disk_cache::EntryResult next_result = |
| cb_next.GetResult(iterator->OpenNextEntry(cb_next.callback())); |
| ASSERT_THAT(next_result.net_error(), IsOk()); |
| auto* next_entry = next_result.ReleaseEntry(); |
| ASSERT_TRUE(next_entry); |
| EXPECT_EQ(next_entry->GetKey(), kKey); |
| |
| // Check db_handle has shared_cache_resource_id. |
| auto* next_sql_entry = static_cast<SqlEntryImpl*>(next_entry); |
| EXPECT_TRUE( |
| next_sql_entry->db_handle()->shared_cache_resource_id().has_value()); |
| EXPECT_EQ(next_sql_entry->db_handle()->shared_cache_resource_id()->db_id, |
| expected_resource_id.db_id); |
| EXPECT_EQ(next_sql_entry->db_handle()->shared_cache_resource_id()->row_id, |
| expected_resource_id.row_id); |
| |
| // Read data stream 1 from iterator-opened entry and verify content matches |
| // kData. |
| auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(kData.size()); |
| net::TestCompletionCallback cb_read; |
| EXPECT_EQ(cb_read.GetResult(next_entry->ReadData( |
| 1, 0, read_buf.get(), kData.size(), cb_read.callback())), |
| static_cast<int>(kData.size())); |
| EXPECT_EQ(std::string_view(read_buf->data(), kData.size()), kData); |
| |
| next_entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadFromSharedCacheWithLargerBuffer) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| net::features::kRendererAccessibleHttpCache); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "shared-cache-drainable-key"; |
| const std::string kData = "0123456789"; |
| |
| SqlSharedCacheResourceId expected_resource_id = |
| CreateEntryInSharedCache(*backend, kKey, kData); |
| ASSERT_TRUE(expected_resource_id.db_id.value()); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| auto* opened_entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(opened_entry); |
| |
| // Pass a buffer larger than kData.size() (20 bytes > 10 bytes) to ensure |
| // buffer->size() != bytes_to_read branch is taken. |
| auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(20); |
| net::TestCompletionCallback cb_read; |
| EXPECT_EQ(cb_read.GetResult(opened_entry->ReadData(1, 0, read_buf.get(), 20, |
| cb_read.callback())), |
| static_cast<int>(kData.size())); |
| EXPECT_EQ(std::string_view(read_buf->data(), kData.size()), kData); |
| |
| opened_entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| TEST_F(SqlBackendImplTest, ReadFromSharedCacheHandleNotFound) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| net::features::kRendererAccessibleHttpCache); |
| auto backend = CreateBackendAndInit(); |
| EXPECT_TRUE(LoadInMemoryIndex(*backend)); |
| |
| const std::string kKey = "shared-cache-not-found-key"; |
| const std::string kData = "Data in shared cache"; |
| |
| SqlSharedCacheResourceId expected_resource_id = |
| CreateEntryInSharedCache(*backend, kKey, kData); |
| ASSERT_TRUE(expected_resource_id.db_id.value()); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_THAT(open_result.net_error(), IsOk()); |
| auto* opened_entry = open_result.ReleaseEntry(); |
| ASSERT_TRUE(opened_entry); |
| |
| // Set a non-existent shared_cache_resource_id so GetCacheByDbId will fail and |
| // pass a null handle. |
| auto* opened_sql_entry = static_cast<SqlEntryImpl*>(opened_entry); |
| opened_sql_entry->db_handle()->set_shared_cache_resource_id( |
| SqlSharedCacheResourceId{SqlSharedCacheDbId(9999), |
| SqlSharedCacheRowId(1)}); |
| |
| auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(kData.size()); |
| net::TestCompletionCallback cb_read; |
| int read_res = opened_entry->ReadData(1, 0, read_buf.get(), kData.size(), |
| cb_read.callback()); |
| if (read_res == net::ERR_IO_PENDING) { |
| backend->RunUntilAllTasksCompleteForTest(); |
| read_res = cb_read.WaitForResult(); |
| } |
| EXPECT_EQ(read_res, net::ERR_FAILED); |
| |
| opened_entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| // Helper base class to ensure `base::test::ScopedFeatureList` is initialized |
| // before `SqlBackendImplTest`'s `TaskEnvironment` starts and destroyed only |
| // after `TaskEnvironment` has shut down and joined all worker threads. |
| // |
| // In C++, base classes are constructed in the order of declaration and |
| // destructed in reverse order. Inheriting `SqlBackendImplFeatureInitializer` |
| // before `SqlBackendImplTest` prevents `base::FeatureList` from being deleted |
| // while background threads in the task environment are still running and |
| // querying feature flags (e.g. on Android during thread creation; see |
| // crbug.com/359904334). |
| class SqlBackendImplFeatureInitializer { |
| public: |
| SqlBackendImplFeatureInitializer() { |
| feature_list_.InitAndEnableFeature( |
| net::features::kRendererAccessibleHttpCache); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| class SqlBackendImplSharedCacheTest : private SqlBackendImplFeatureInitializer, |
| public SqlBackendImplTest { |
| public: |
| SqlBackendImplSharedCacheTest() = default; |
| |
| protected: |
| void WriteResponseInfoToEntry(disk_cache::Entry* entry, |
| const net::HttpResponseInfo& info) { |
| auto pickle = info.MakePickle(false, false); |
| std::string pickle_data(reinterpret_cast<const char*>(pickle->data()), |
| pickle->size()); |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>(pickle_data); |
| net::TestCompletionCallback cb; |
| EXPECT_EQ(cb.GetResult(entry->WriteData(0, 0, buffer.get(), |
| static_cast<int>(pickle->size()), |
| cb.callback(), false)), |
| static_cast<int>(pickle->size())); |
| } |
| |
| // Helper to create an entry in store, write data and response info, |
| // optionally close it, and register it to shared cache. |
| disk_cache::Entry* CreateAndRegisterSharedCacheEntry( |
| SqlBackendImpl* backend, |
| const std::string& key, |
| const std::string& data, |
| const net::NetworkIsolationKey& nik, |
| base::Time response_time = base::Time::Now(), |
| bool close_entry = true) { |
| auto* entry = CreateEntryAndWriteData(backend, key, data); |
| net::HttpResponseInfo info; |
| info.response_time = response_time; |
| info.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| WriteResponseInfoToEntry(entry, info); |
| if (close_entry) { |
| entry->Close(); |
| } |
| GURL url{net::HttpCache::GetResourceURLFromHttpCacheKey(key)}; |
| backend->OnEntryEligibleForSharedCache( |
| key, url, std::make_unique<net::HttpResponseInfo>(info), nik); |
| return close_entry ? nullptr : entry; |
| } |
| |
| void VerifySharedCacheResourceIdExists(SqlBackendImpl* backend, |
| const std::string& key) { |
| base::test::TestFuture<SqlPersistentStore::EntryInfoOrError> future; |
| backend->GetSqlStoreForTest()->OpenEntry(CacheEntryKey(key), |
| future.GetCallback()); |
| auto result = future.Take(); |
| ASSERT_TRUE(result.has_value()); |
| EXPECT_TRUE(result->shared_cache_resource_id.has_value()); |
| } |
| }; |
| |
| TEST_F(SqlBackendImplSharedCacheTest, ProcessSharedCacheEligibleEntries) { |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "0/0/https://example.com/"; |
| const GURL kUrl("https://example.com"); |
| const net::SchemefulSite kSite(GURL("https://example.com")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kData = "Hello Shared Cache"; |
| |
| // 1. Save entry |
| auto* entry = CreateEntryAndWriteData(backend.get(), kKey, kData); |
| |
| base::Time response_time = base::Time::Now(); |
| net::HttpResponseInfo response_info_for_pickle; |
| response_info_for_pickle.response_time = response_time; |
| response_info_for_pickle.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| std::unique_ptr<base::Pickle> pickle = |
| response_info_for_pickle.MakePickle(false, false); |
| std::string pickle_data(reinterpret_cast<const char*>(pickle->data()), |
| pickle->size()); |
| auto pickle_buffer = base::MakeRefCounted<net::StringIOBuffer>(pickle_data); |
| net::TestCompletionCallback cb_write_pickle; |
| EXPECT_EQ(cb_write_pickle.GetResult( |
| entry->WriteData(0, 0, pickle_buffer.get(), pickle->size(), |
| cb_write_pickle.callback(), false)), |
| static_cast<int>(pickle->size())); |
| |
| entry->Close(); |
| |
| // 2. Call OnEntryEligibleForSharedCache |
| auto response_info = |
| std::make_unique<net::HttpResponseInfo>(response_info_for_pickle); |
| backend->OnEntryEligibleForSharedCache(kKey, kUrl, std::move(response_info), |
| kNik); |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| |
| // 3. Register MockSharedCacheClientRemote |
| auto client = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr = client.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client)); |
| |
| // 4. Process entries |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| // 5. Wait for Initialize and OnResourcesAdded |
| client_ptr->WaitUntilInitialized(); |
| client_ptr->WaitUntilOnResourcesAdded(); |
| process_run_loop.Run(); |
| |
| EXPECT_TRUE(client_ptr->initialize_called()); |
| EXPECT_TRUE(client_ptr->on_resources_added_called()); |
| EXPECT_TRUE(client_ptr->has_disconnect_handler()); |
| |
| // 6. Use pending_file_set_ to read |
| SqlSharedCacheIsolatedDatabaseReader reader(client_ptr->TakePendingFileSet()); |
| |
| std::optional<SqlSharedCacheIsolatedDatabaseReader::Response> response = |
| reader.ReadResponse(kUrl.spec()); |
| ASSERT_TRUE(response); |
| EXPECT_GT(response->GetBodySize(), 0); |
| |
| std::vector<uint8_t> buffer(kData.size()); |
| EXPECT_TRUE(response->ReadBody(buffer)); |
| EXPECT_EQ(std::string(buffer.begin(), buffer.end()), kData); |
| |
| // 7. Call disconnect_handler |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u); |
| EXPECT_EQ(manager->GetSharedCachesByDbIdSizeForTest(), 1u); |
| EXPECT_EQ(manager->GetSharedCachesByNikSizeForTest(), 1u); |
| |
| client_ptr->RunDisconnectHandler(); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| |
| // 8. Verify empty |
| EXPECT_EQ(manager->GetSharedCachesByDbIdSizeForTest(), 0u); |
| EXPECT_EQ(manager->GetSharedCachesByNikSizeForTest(), 0u); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesIncremental) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kKey1 = "0/0/https://example.test/1"; |
| const GURL kUrl1("https://example.test/1"); |
| const std::string kKey2 = "0/0/https://example.test/2"; |
| const GURL kUrl2("https://example.test/2"); |
| |
| // 1. Create entry 1 and mark eligible |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik); |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| |
| // 2. Register MockSharedCacheClientRemote |
| auto client = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr = client.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client)); |
| |
| // 3. Process entry 1 |
| base::RunLoop process_run_loop1; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop1.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr->WaitUntilInitialized(); |
| client_ptr->WaitUntilOnResourcesAdded(1); |
| process_run_loop1.Run(); |
| |
| EXPECT_EQ(client_ptr->on_resources_added_call_count(), 1u); |
| EXPECT_THAT(client_ptr->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl1.spec()))); |
| |
| // 4. Create entry 2 and mark eligible |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik); |
| backend->RunUntilAllTasksCompleteForTest(); |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| |
| // 5. Process entry 2 |
| base::RunLoop process_run_loop2; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr->WaitUntilOnResourcesAdded(2); |
| process_run_loop2.Run(); |
| |
| // Verify that OnResourcesAdded was called again with only the newly added |
| // hash. |
| EXPECT_EQ(client_ptr->on_resources_added_call_count(), 2u); |
| EXPECT_THAT(client_ptr->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl2.spec()))); |
| |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u); |
| |
| client_ptr->RunDisconnectHandler(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesDuplicate) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kKey = "0/0/https://example.test/"; |
| const GURL kUrl("https://example.test/"); |
| const std::string kData = "Data"; |
| base::Time response_time = base::Time::Now(); |
| |
| // 1. Create and copy the entry. |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey, kData, kNik, |
| response_time); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| auto client = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr = client.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client)); |
| |
| base::RunLoop process_run_loop1; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop1.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr->WaitUntilInitialized(); |
| client_ptr->WaitUntilOnResourcesAdded(1); |
| process_run_loop1.Run(); |
| |
| EXPECT_EQ(client_ptr->on_resources_added_call_count(), 1u); |
| EXPECT_THAT(client_ptr->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl.spec()))); |
| |
| // 2. Mark the same entry eligible again and process. |
| net::HttpResponseInfo info; |
| info.response_time = response_time; |
| info.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| backend->OnEntryEligibleForSharedCache( |
| kKey, kUrl, std::make_unique<net::HttpResponseInfo>(info), kNik); |
| |
| base::RunLoop process_run_loop2; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| process_run_loop2.Run(); |
| |
| // OnResourcesAdded should NOT be called again because the hash is already |
| // cached. |
| EXPECT_EQ(client_ptr->on_resources_added_call_count(), 1u); |
| |
| client_ptr->RunDisconnectHandler(); |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesMultipleClientsBroadcast) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kKey = "0/0/https://example.test/"; |
| const GURL kUrl("https://example.test/"); |
| const std::string kData = "Data"; |
| |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey, kData, kNik); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Register two clients for the same NIK. |
| auto client1 = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr1 = client1.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client1)); |
| |
| auto client2 = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr2 = client2.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client2)); |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr1->WaitUntilInitialized(); |
| client_ptr1->WaitUntilOnResourcesAdded(1); |
| client_ptr2->WaitUntilInitialized(); |
| client_ptr2->WaitUntilOnResourcesAdded(1); |
| process_run_loop.Run(); |
| |
| // Both clients should receive OnResourcesAdded with the new hash. |
| EXPECT_EQ(client_ptr1->on_resources_added_call_count(), 1u); |
| EXPECT_THAT(client_ptr1->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl.spec()))); |
| |
| EXPECT_EQ(client_ptr2->on_resources_added_call_count(), 1u); |
| EXPECT_THAT(client_ptr2->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl.spec()))); |
| |
| client_ptr1->RunDisconnectHandler(); |
| client_ptr2->RunDisconnectHandler(); |
| |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesPartialClientDisconnect) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kKey1 = "0/0/https://example.test/1"; |
| const GURL kUrl1("https://example.test/1"); |
| const std::string kKey2 = "0/0/https://example.test/2"; |
| const GURL kUrl2("https://example.test/2"); |
| |
| // Register two clients for the same NIK. |
| bool client1_destroyed = false; |
| auto client1 = std::make_unique<MockSharedCacheClientRemote>(); |
| client1->SetOnDestroyHandler( |
| base::BindLambdaForTesting([&]() { client1_destroyed = true; })); |
| auto* client_ptr1 = client1.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client1)); |
| |
| auto client2 = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr2 = client2.get(); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client2)); |
| |
| // Copy entry 1. |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::RunLoop process_run_loop1; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop1.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr1->WaitUntilInitialized(); |
| client_ptr1->WaitUntilOnResourcesAdded(1); |
| client_ptr2->WaitUntilInitialized(); |
| client_ptr2->WaitUntilOnResourcesAdded(1); |
| process_run_loop1.Run(); |
| |
| // Disconnect client 1. |
| client_ptr1->RunDisconnectHandler(); |
| EXPECT_TRUE(client1_destroyed); |
| |
| // Copy entry 2. |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::RunLoop process_run_loop2; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr2->WaitUntilOnResourcesAdded(2); |
| process_run_loop2.Run(); |
| |
| // client 1 was destroyed upon disconnect, and client 2 receives call count 2. |
| EXPECT_EQ(client_ptr2->on_resources_added_call_count(), 2u); |
| EXPECT_THAT(client_ptr2->new_hashes(), |
| testing::ElementsAre(base::PersistentHash(kUrl2.spec()))); |
| |
| client_ptr2->RunDisconnectHandler(); |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| RegisterSharedCacheClientRemoteTransientNik) { |
| auto backend = CreateBackendAndInit(); |
| auto transient_nik = net::NetworkIsolationKey::CreateTransientForTesting(); |
| ASSERT_TRUE(transient_nik.IsTransient()); |
| |
| bool client_destroyed = false; |
| auto client = std::make_unique<MockSharedCacheClientRemote>(); |
| client->SetOnDestroyHandler( |
| base::BindLambdaForTesting([&]() { client_destroyed = true; })); |
| |
| backend->RegisterSharedCacheClientRemote(transient_nik, std::move(client)); |
| |
| // The client should be destroyed immediately because transient NIKs are |
| // ignored. |
| EXPECT_TRUE(client_destroyed); |
| |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesMultipleNiks) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite1(GURL("https://site1.test")); |
| const net::NetworkIsolationKey kNik1(kSite1, kSite1); |
| const net::SchemefulSite kSite2(GURL("https://site2.test")); |
| const net::NetworkIsolationKey kNik2(kSite2, kSite2); |
| |
| const std::string kKey1 = "0/0/https://example1.test"; |
| const std::string kKey2 = "0/0/https://example2.test"; |
| |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik1); |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik2); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 2u); |
| |
| // Register clients |
| auto client1 = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr1 = client1.get(); |
| backend->RegisterSharedCacheClientRemote(kNik1, std::move(client1)); |
| |
| auto client2 = std::make_unique<MockSharedCacheClientRemote>(); |
| auto* client_ptr2 = client2.get(); |
| backend->RegisterSharedCacheClientRemote(kNik2, std::move(client2)); |
| |
| // Process |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| client_ptr1->WaitUntilInitialized(); |
| client_ptr1->WaitUntilOnResourcesAdded(); |
| client_ptr2->WaitUntilInitialized(); |
| client_ptr2->WaitUntilOnResourcesAdded(); |
| |
| process_run_loop.Run(); |
| |
| VerifySharedCacheResourceIdExists(backend.get(), kKey1); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey2); |
| |
| EXPECT_TRUE(client_ptr1->initialize_called()); |
| EXPECT_TRUE(client_ptr2->initialize_called()); |
| |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 2u); |
| |
| client_ptr1->RunDisconnectHandler(); |
| client_ptr2->RunDisconnectHandler(); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesDbFailure) { |
| auto backend = CreateBackendAndInit(); |
| auto* manager = |
| backend->GetSqlStoreForTest()->shared_cache_manager_for_testing(); |
| manager->SetSimulateDbFailureForTesting(true); |
| |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| const std::string kKey = "0/0/https://example.test"; |
| |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey, "Hello Shared Cache", |
| kNik); |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| |
| base::RunLoop destroy_run_loop; |
| auto client = std::make_unique<MockSharedCacheClientRemote>(); |
| client->SetOnDestroyHandler(destroy_run_loop.QuitClosure()); |
| backend->RegisterSharedCacheClientRemote(kNik, std::move(client)); |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| base::NullCallback()); |
| process_run_loop.Run(); |
| |
| // On DB failure, processing fails and the entries are dropped, leaving 0 |
| // pending eligible entries. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 0u); |
| |
| // Due to DB failure, processing fails and cache drops, which triggers client |
| // destruction. |
| destroy_run_loop.Run(); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return manager->GetSharedCachesSizeForTest() == 0u; })); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessSharedCacheEligibleEntriesSkipsActiveEntry) { |
| auto backend = CreateBackendAndInit(); |
| |
| const net::SchemefulSite kSite1(GURL("https://site1.test")); |
| const net::NetworkIsolationKey kNik1(kSite1, kSite1); |
| const net::SchemefulSite kSite2(GURL("https://site2.test")); |
| const net::NetworkIsolationKey kNik2(kSite2, kSite2); |
| |
| const std::string kKey1 = "0/0/https://example1.test"; |
| const std::string kKey2 = "0/0/https://example2.test"; |
| |
| // Create entry1 and entry2 and keep BOTH open (active). |
| auto* entry1 = CreateAndRegisterSharedCacheEntry( |
| backend.get(), kKey1, "Data 1", kNik1, base::Time::Now(), |
| /*close_entry=*/false); |
| auto* entry2 = CreateAndRegisterSharedCacheEntry( |
| backend.get(), kKey2, "Data 2", kNik2, base::Time::Now(), |
| /*close_entry=*/false); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 2u); |
| |
| // 1. When ALL entries are active, HandleProcessSharedCacheEligibleEntries |
| // finds entries_to_process is empty and returns early without processing. |
| base::RunLoop process_run_loop_all_active; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop_all_active.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| process_run_loop_all_active.Run(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Both entries were active, so 0 entries were processed, leaving 2 eligible |
| // entries. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 2u); |
| |
| // 2. Close entry2, keep entry1 open. |
| entry2->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| process_run_loop.Run(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // entry1 was skipped (remains active/eligible), entry2 was processed. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey2); |
| |
| // 3. Close entry1 and process again. |
| entry1->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::RunLoop process_run_loop2; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| |
| process_run_loop2.Run(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // entry1 should now be processed. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 0u); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey1); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| OnEntryEligibleForSharedCacheUpdateWithNewerResponseTime) { |
| auto backend = CreateBackendAndInit(); |
| const std::string kKey = "0/0/https://example.test"; |
| const GURL kUrl("https://example.test"); |
| const net::SchemefulSite kSite(GURL("https://example.test")); |
| const net::NetworkIsolationKey kNik(kSite, kSite); |
| |
| base::Time now = base::Time::Now(); |
| |
| // Register an entry initially. |
| { |
| auto response_info = std::make_unique<net::HttpResponseInfo>(); |
| response_info->response_time = now; |
| response_info->headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n\n"); |
| backend->OnEntryEligibleForSharedCache(kKey, kUrl, std::move(response_info), |
| kNik); |
| } |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| { |
| const auto& map = backend->GetSharedCacheEligibleEntriesForTest(); |
| auto it = map.find(CacheEntryKey(kKey)); |
| ASSERT_NE(it, map.end()); |
| EXPECT_EQ(it->second.response_info->response_time, now); |
| } |
| |
| // Register the same key with an older response_time. It should be ignored. |
| { |
| auto response_info = std::make_unique<net::HttpResponseInfo>(); |
| response_info->response_time = now - base::Seconds(10); |
| response_info->headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n\n"); |
| backend->OnEntryEligibleForSharedCache(kKey, kUrl, std::move(response_info), |
| kNik); |
| } |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| { |
| const auto& map = backend->GetSharedCacheEligibleEntriesForTest(); |
| auto it = map.find(CacheEntryKey(kKey)); |
| ASSERT_NE(it, map.end()); |
| EXPECT_EQ(it->second.response_info->response_time, now); |
| } |
| |
| // Register the same key with a newer response_time. It should execute the if |
| // branch and update the entry. |
| base::Time newer = now + base::Seconds(10); |
| { |
| auto response_info = std::make_unique<net::HttpResponseInfo>(); |
| response_info->response_time = newer; |
| response_info->headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n\n"); |
| backend->OnEntryEligibleForSharedCache(kKey, kUrl, std::move(response_info), |
| kNik); |
| } |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| { |
| const auto& map = backend->GetSharedCacheEligibleEntriesForTest(); |
| auto it = map.find(CacheEntryKey(kKey)); |
| ASSERT_NE(it, map.end()); |
| EXPECT_EQ(it->second.response_info->response_time, newer); |
| } |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| OnProcessSharedCacheEligibleEntriesCompleteReinsertsUnprocessedEntries) { |
| auto backend = CreateBackendAndInit(); |
| |
| const std::string kKey1 = "0/0/https://example1.test"; |
| const std::string kKey2 = "0/0/https://example2.test"; |
| const std::string kKey3 = "0/0/https://example3.test"; |
| |
| const net::SchemefulSite kSite1(GURL("https://site1.test")); |
| const net::NetworkIsolationKey kNik1(kSite1, kSite1); |
| const net::SchemefulSite kSite2(GURL("https://site2.test")); |
| const net::NetworkIsolationKey kNik2(kSite2, kSite2); |
| |
| // kKey1 and kKey2 share kNik1; kKey3 uses kNik2. |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik1); |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik1); |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey3, "Data 3", kNik2); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 3u); |
| |
| std::unique_ptr<OperationHandle> in_flight_handle; |
| |
| base::RunLoop process_run_loop; |
| // When kKey1 finishes copying, post a normal operation so that CopyEntries |
| // aborts remaining entries in the same NIK group (kKey2) and subsequent NIK |
| // groups (kKey3). |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| base::BindRepeating( |
| [](SqlBackendImpl* backend, |
| std::unique_ptr<OperationHandle>* handle_out, |
| const CacheEntryKey& key) { |
| if (!*handle_out) { |
| backend->GetExclusiveOperationCoordinatorForTest() |
| ->PostOrRunNormalOperation( |
| CacheEntryKey("fake_key_for_abort"), |
| base::BindOnce( |
| [](std::unique_ptr<OperationHandle>* out, |
| std::unique_ptr<OperationHandle> handle) { |
| *out = std::move(handle); |
| }, |
| handle_out), |
| /*low_priority=*/false); |
| } |
| }, |
| backend.get(), &in_flight_handle)); |
| |
| process_run_loop.Run(); |
| |
| // kKey1 was processed; kKey2 (same NIK) and kKey3 (different NIK) were |
| // aborted and re-inserted. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 2u); |
| |
| // Release normal operation so backend becomes idle again. |
| in_flight_handle.reset(); |
| |
| base::RunLoop process_run_loop2; |
| backend->ProcessAllSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| process_run_loop2.Run(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // All entries should now be in shared cache. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 0u); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey1); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey2); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey3); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| OnProcessSharedCacheEligibleEntriesCompleteConflictResponseTime) { |
| auto backend = CreateBackendAndInit(); |
| |
| const std::string kKey1 = "0/0/https://example1.test"; |
| const std::string kKey2 = "0/0/https://example2.test"; |
| const net::SchemefulSite kSite1(GURL("https://site1.test")); |
| const net::NetworkIsolationKey kNik1(kSite1, kSite1); |
| const net::SchemefulSite kSite2(GURL("https://site2.test")); |
| const net::NetworkIsolationKey kNik2(kSite2, kSite2); |
| |
| base::Time now = base::Time::Now(); |
| base::Time t1 = now; |
| |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik1, t1); |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik2, t1); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| std::unique_ptr<OperationHandle> in_flight_handle; |
| base::Time t0 = now - base::Seconds(10); |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| base::BindRepeating( |
| [](SqlBackendImpl* backend, |
| std::unique_ptr<OperationHandle>* handle_out, |
| const std::string& key2, const GURL& url, |
| const net::NetworkIsolationKey& nik, base::Time t0, |
| const CacheEntryKey& key) { |
| if (!*handle_out) { |
| backend->GetExclusiveOperationCoordinatorForTest() |
| ->PostOrRunNormalOperation( |
| CacheEntryKey("fake_key_for_abort"), |
| base::BindOnce( |
| [](std::unique_ptr<OperationHandle>* out, |
| std::unique_ptr<OperationHandle> handle) { |
| *out = std::move(handle); |
| }, |
| handle_out), |
| /*low_priority=*/false); |
| auto info = std::make_unique<net::HttpResponseInfo>(); |
| info->response_time = t0; |
| info->headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| "HTTP/1.1 200 OK\n\n"); |
| backend->OnEntryEligibleForSharedCache(key2, url, std::move(info), |
| nik); |
| } |
| }, |
| backend.get(), &in_flight_handle, kKey2, |
| GURL(net::HttpCache::GetResourceURLFromHttpCacheKey(kKey2)), kNik2, |
| t0)); |
| |
| process_run_loop.Run(); |
| in_flight_handle.reset(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // Unprocessed entry t1 for kKey2 is newer than t0, so t1 overwrites t0. |
| const auto& map1 = backend->GetSharedCacheEligibleEntriesForTest(); |
| auto it1 = map1.find(CacheEntryKey(kKey2)); |
| ASSERT_TRUE(it1 != map1.end()); |
| EXPECT_EQ(it1->second.response_info->response_time, t1); |
| |
| // 2. Test t2 > t1 case for kKey2. |
| base::Time t2 = now + base::Seconds(10); |
| |
| // Register a new entry kKey3 instead of re-registering kKey1, because |
| // kKey1 was already copied to the shared cache and will be skipped. |
| const std::string kKey3 = "0/0/https://example3.test"; |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey3, "Data 3", kNik1, t1); |
| |
| base::RunLoop process_run_loop2; |
| backend->ProcessSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop2.QuitClosure()), |
| base::BindRepeating( |
| [](SqlBackendImpl* backend, |
| std::unique_ptr<OperationHandle>* handle_out, |
| const std::string& key2, const GURL& url, |
| const net::NetworkIsolationKey& nik, base::Time t2, |
| const CacheEntryKey& key) { |
| if (!*handle_out) { |
| backend->GetExclusiveOperationCoordinatorForTest() |
| ->PostOrRunNormalOperation( |
| CacheEntryKey("fake_key_for_abort"), |
| base::BindOnce( |
| [](std::unique_ptr<OperationHandle>* out, |
| std::unique_ptr<OperationHandle> handle) { |
| *out = std::move(handle); |
| }, |
| handle_out), |
| /*low_priority=*/false); |
| auto info = std::make_unique<net::HttpResponseInfo>(); |
| info->response_time = t2; |
| info->headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| "HTTP/1.1 200 OK\n\n"); |
| backend->OnEntryEligibleForSharedCache(key2, url, std::move(info), |
| nik); |
| } |
| }, |
| backend.get(), &in_flight_handle, kKey2, |
| GURL(net::HttpCache::GetResourceURLFromHttpCacheKey(kKey2)), kNik2, |
| t2)); |
| |
| process_run_loop2.Run(); |
| in_flight_handle.reset(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| const auto& map2 = backend->GetSharedCacheEligibleEntriesForTest(); |
| auto it2 = map2.find(CacheEntryKey(kKey2)); |
| ASSERT_TRUE(it2 != map2.end()); |
| EXPECT_EQ(it2->second.response_info->response_time, t2); |
| } |
| |
| TEST_F(SqlBackendImplSharedCacheTest, |
| ProcessAllSharedCacheEligibleEntriesForTest) { |
| auto backend = CreateBackendAndInit(); |
| |
| const std::string kKey1 = "0/0/https://example1.test"; |
| const std::string kKey2 = "0/0/https://example2.test"; |
| const net::SchemefulSite kSite1(GURL("https://site1.test")); |
| const net::NetworkIsolationKey kNik1(kSite1, kSite1); |
| const net::SchemefulSite kSite2(GURL("https://site2.test")); |
| const net::NetworkIsolationKey kNik2(kSite2, kSite2); |
| |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey1, "Data 1", kNik1); |
| CreateAndRegisterSharedCacheEntry(backend.get(), kKey2, "Data 2", kNik2); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| auto verify_count_task = base::BindOnce( |
| [](SqlBackendImpl* backend, std::unique_ptr<OperationHandle> handle) { |
| // While `handle` is held, pass 1 has finished and re-inserted the |
| // aborted entry (kKey2) into the queue. |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 1u); |
| }, |
| backend.get()); |
| |
| auto on_normal_op = base::BindOnce( |
| [](base::OnceCallback<void(std::unique_ptr<OperationHandle>)> task, |
| std::unique_ptr<OperationHandle> handle) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(task), std::move(handle))); |
| }, |
| std::move(verify_count_task)); |
| |
| using NormalOpCallback = |
| base::OnceCallback<void(std::unique_ptr<OperationHandle>)>; |
| auto on_normal_op_holder = |
| std::make_unique<NormalOpCallback>(std::move(on_normal_op)); |
| |
| auto on_entry_copied = base::BindRepeating( |
| [](SqlBackendImpl* backend, NormalOpCallback* on_normal_op_holder, |
| const CacheEntryKey& key) { |
| if (*on_normal_op_holder) { |
| backend->GetExclusiveOperationCoordinatorForTest() |
| ->PostOrRunNormalOperation(CacheEntryKey("fake_key_for_abort"), |
| std::move(*on_normal_op_holder), |
| /*low_priority=*/false); |
| } |
| }, |
| backend.get(), base::Owned(on_normal_op_holder.release())); |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessAllSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| std::move(on_entry_copied)); |
| |
| process_run_loop.Run(); |
| |
| EXPECT_EQ(backend->GetSharedCacheEligibleEntriesCountForTest(), 0u); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey1); |
| VerifySharedCacheResourceIdExists(backend.get(), kKey2); |
| } |
| |
| class SqlBackendImplSharedCacheWriteTest |
| : public SqlBackendImplSharedCacheTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| SqlBackendImplSharedCacheWriteTest() |
| : kSite(GURL("https://example.com")), kNik(kSite, kSite) {} |
| |
| static std::string DescribeParams(const testing::TestParamInfo<bool>& info) { |
| return info.param ? "WithOtherEntry" : "SingleEntry"; |
| } |
| |
| protected: |
| static constexpr char kTargetKey[] = "0/0/https://example.com/"; |
| static constexpr char kOtherKey[] = "0/0/https://example.com/2"; |
| |
| const net::SchemefulSite kSite; |
| const net::NetworkIsolationKey kNik; |
| |
| bool WithOtherEntry() const { return GetParam(); } |
| |
| void SetupSharedCacheEntries( |
| std::unique_ptr<SqlBackendImpl>& backend, |
| const std::string& data, |
| std::optional<SqlSharedCacheDbId>& out_db_id, |
| std::optional<SqlSharedCacheRowId>& out_row_id1, |
| std::optional<SqlSharedCacheRowId>& out_row_id2) { |
| // 1. Create first entry |
| auto* entry = CreateEntryAndWriteData(backend.get(), kTargetKey, data); |
| |
| base::Time response_time = base::Time::Now(); |
| |
| // Write headers for the first entry |
| net::HttpResponseInfo response_info_for_pickle; |
| response_info_for_pickle.response_time = response_time; |
| response_info_for_pickle.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| std::unique_ptr<base::Pickle> pickle = |
| response_info_for_pickle.MakePickle(false, false); |
| std::string pickle_data(reinterpret_cast<const char*>(pickle->data()), |
| pickle->size()); |
| auto pickle_buffer = base::MakeRefCounted<net::StringIOBuffer>(pickle_data); |
| net::TestCompletionCallback cb_write_pickle; |
| EXPECT_EQ(cb_write_pickle.GetResult( |
| entry->WriteData(0, 0, pickle_buffer.get(), pickle->size(), |
| cb_write_pickle.callback(), false)), |
| static_cast<int>(pickle->size())); |
| entry->Close(); |
| |
| // 2. Make it eligible |
| auto response_info = std::make_unique<net::HttpResponseInfo>(); |
| response_info->response_time = response_time; |
| response_info->headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| backend->OnEntryEligibleForSharedCache(kTargetKey, |
| GURL("https://example.com/"), |
| std::move(response_info), kNik); |
| |
| // If we want another entry in the same shared cache (same NIK) |
| if (WithOtherEntry()) { |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| base::Time response_time2 = base::Time::Now(); |
| |
| auto* entry2 = CreateEntryAndWriteData(backend.get(), kOtherKey, data); |
| |
| net::HttpResponseInfo response_info_for_pickle2; |
| response_info_for_pickle2.response_time = response_time2; |
| response_info_for_pickle2.headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| std::unique_ptr<base::Pickle> pickle2 = |
| response_info_for_pickle2.MakePickle(false, false); |
| std::string pickle_data2(reinterpret_cast<const char*>(pickle2->data()), |
| pickle2->size()); |
| auto pickle_buffer2 = |
| base::MakeRefCounted<net::StringIOBuffer>(pickle_data2); |
| |
| net::TestCompletionCallback cb_write_pickle2; |
| EXPECT_EQ(cb_write_pickle2.GetResult(entry2->WriteData( |
| 0, 0, pickle_buffer2.get(), pickle2->size(), |
| cb_write_pickle2.callback(), false)), |
| static_cast<int>(pickle2->size())); |
| entry2->Close(); |
| |
| auto response_info2 = std::make_unique<net::HttpResponseInfo>(); |
| response_info2->response_time = response_time2; |
| response_info2->headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\0\0"); |
| backend->OnEntryEligibleForSharedCache(kOtherKey, |
| GURL("https://example.com/2"), |
| std::move(response_info2), kNik); |
| } |
| |
| base::RunLoop process_run_loop; |
| backend->ProcessAllSharedCacheEligibleEntriesForTest( |
| base::ScopedClosureRunner(process_run_loop.QuitClosure()), |
| /*on_entry_copied_callback=*/base::NullCallback()); |
| process_run_loop.Run(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| // After processing, re-open the entries to capture the row IDs. |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* opened_entry = open_result.ReleaseEntry(); |
| |
| auto* sql_entry = static_cast<SqlEntryImpl*>(opened_entry); |
| auto shared_resource = sql_entry->db_handle()->shared_cache_resource_id(); |
| ASSERT_TRUE(shared_resource); |
| out_db_id = shared_resource->db_id; |
| out_row_id1 = shared_resource->row_id; |
| opened_entry->Close(); |
| |
| if (WithOtherEntry()) { |
| TestEntryResultCompletionCallback cb_open2; |
| disk_cache::EntryResult open_result2 = cb_open2.GetResult( |
| backend->OpenEntry(kOtherKey, net::HIGHEST, cb_open2.callback())); |
| ASSERT_EQ(open_result2.net_error(), net::OK); |
| auto* opened_entry2 = open_result2.ReleaseEntry(); |
| |
| auto* sql_entry2 = static_cast<SqlEntryImpl*>(opened_entry2); |
| auto shared_resource2 = |
| sql_entry2->db_handle()->shared_cache_resource_id(); |
| ASSERT_TRUE(shared_resource2); |
| out_row_id2 = shared_resource2->row_id; |
| opened_entry2->Close(); |
| } |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| void VerifySharedCacheDeleted(SqlBackendImpl* backend, |
| std::optional<SqlSharedCacheDbId> db_id, |
| std::optional<SqlSharedCacheRowId> row_id) { |
| backend->RunUntilAllTasksCompleteForTest(); |
| |
| base::FilePath file_path = |
| temp_dir_.GetPath() |
| .AppendASCII( |
| base::StrCat({kSqlBackendSharedCacheIsolatedFileNamePrefix, |
| base::NumberToString(db_id->value())})) |
| .AddExtension(FILE_PATH_LITERAL(".db")); |
| |
| if (WithOtherEntry()) { |
| // DB should still exist |
| EXPECT_TRUE(base::PathExists(file_path)); |
| |
| // Verify that the target entry (key) does not exist. |
| { |
| base::test::TestFuture<SqlPersistentStore::EntryInfoOrError> future; |
| backend->GetSqlStoreForTest()->OpenEntry(CacheEntryKey(kTargetKey), |
| future.GetCallback()); |
| auto result = future.Take(); |
| EXPECT_FALSE(result.has_value()); |
| } |
| |
| // The other remaining entry (key + "2") should still be in shared cache. |
| { |
| base::test::TestFuture<SqlPersistentStore::EntryInfoOrError> future; |
| backend->GetSqlStoreForTest()->OpenEntry(CacheEntryKey(kOtherKey), |
| future.GetCallback()); |
| auto result = future.Take(); |
| ASSERT_TRUE(result.has_value()); |
| EXPECT_TRUE(result->shared_cache_resource_id.has_value()); |
| } |
| } else { |
| // Whole DB should be gone |
| EXPECT_TRUE( |
| base::test::RunUntil([&]() { return !base::PathExists(file_path); })); |
| } |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SqlBackendImplSharedCacheWriteTest, |
| testing::Bool(), |
| SqlBackendImplSharedCacheWriteTest::DescribeParams); |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| WriteDataInternalOffsetZeroTruncate) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Data", db_id, row_id1, row_id2); |
| |
| // Open the entry again |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // WriteData at offset 0 and truncate |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("New Data"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ(cb_write.GetResult(entry->WriteData( |
| 1, 0, buffer.get(), buffer->size(), cb_write.callback(), true)), |
| static_cast<int>(buffer->size())); |
| |
| entry->Close(); |
| |
| VerifySharedCacheDeleted(backend.get(), db_id, row_id1); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, WriteDataInternalCopySmall) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| // Data < 16KB |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // WriteData NOT offset 0 && truncate |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 5, buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| static_cast<int>(buffer->size())); |
| |
| const std::string expected_data = "SmallAppend"; |
| EXPECT_EQ(entry->GetDataSize(1), static_cast<int>(expected_data.size())); |
| auto read_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(expected_data.size()); |
| net::TestCompletionCallback cb_read; |
| EXPECT_EQ( |
| cb_read.GetResult(entry->ReadData( |
| 1, 0, read_buffer.get(), read_buffer->size(), cb_read.callback())), |
| static_cast<int>(expected_data.size())); |
| EXPECT_EQ(std::string_view(read_buffer->data(), read_buffer->size()), |
| expected_data); |
| |
| entry->Close(); |
| |
| VerifySharedCacheDeleted(backend.get(), db_id, row_id1); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, WriteDataInternalCopyLarge) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| // Data > 16KB to trigger multiple copy iterations |
| std::string large_data(20 * 1024, '\0'); |
| for (size_t i = 0; i < large_data.size(); ++i) { |
| large_data[i] = static_cast<char>('a' + (i % 26)); |
| } |
| SetupSharedCacheEntries(backend, large_data, db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // WriteData NOT offset 0 && truncate |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ(cb_write.GetResult(entry->WriteData(1, large_data.size(), |
| buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| static_cast<int>(buffer->size())); |
| |
| const std::string expected_large_data = large_data + "Append"; |
| EXPECT_EQ(entry->GetDataSize(1), |
| static_cast<int>(expected_large_data.size())); |
| auto read_buffer_large = |
| base::MakeRefCounted<net::IOBufferWithSize>(expected_large_data.size()); |
| net::TestCompletionCallback cb_read_large; |
| EXPECT_EQ(cb_read_large.GetResult(entry->ReadData( |
| 1, 0, read_buffer_large.get(), read_buffer_large->size(), |
| cb_read_large.callback())), |
| static_cast<int>(expected_large_data.size())); |
| EXPECT_EQ( |
| std::string_view(read_buffer_large->data(), read_buffer_large->size()), |
| expected_large_data); |
| |
| entry->Close(); |
| |
| VerifySharedCacheDeleted(backend.get(), db_id, row_id1); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| CopySharedCacheToBlobTableInFlightModification) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // Post `OpenNextEntry` on the iterator beforehand. |
| // Then calling `WriteData` creates a scenario where the iterator's DB record |
| // lookup and entry instantiation happen asynchronously while the background |
| // copy operation (`CopySharedCacheToBlobTableAndWrite`) is in progress. |
| auto iter = backend->CreateIterator(); |
| TestEntryResultCompletionCallback cb_next1; |
| EntryResult result_iter = iter->OpenNextEntry(cb_next1.callback()); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| base::test::TestFuture<int> write_future; |
| std::ignore = entry->WriteData(1, 10, buffer.get(), buffer->size(), |
| write_future.GetCallback(), false); |
| entry->Close(); |
| |
| // Retrieve the result of the iterator's open operation. |
| result_iter = cb_next1.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| entry = result_iter.ReleaseEntry(); |
| // When WithOtherEntry() is true, another entry in the shared cache might be |
| // returned first, so advance the iterator until we reach `kTargetKey`. |
| if (entry->GetKey() != kTargetKey) { |
| entry->Close(); |
| TestEntryResultCompletionCallback cb_next2; |
| result_iter = iter->OpenNextEntry(cb_next2.callback()); |
| result_iter = cb_next2.GetResult(std::move(result_iter)); |
| ASSERT_THAT(result_iter.net_error(), IsOk()); |
| entry = result_iter.ReleaseEntry(); |
| } |
| EXPECT_EQ(entry->GetKey(), std::string(kTargetKey)); |
| |
| // Verify that `InFlightEntryModification` is preserved and applied during |
| // `CopySharedCacheToBlobTableAndWrite`, so the `body_end` of the entry opened |
| // by the iterator reflects the written data size (10 + 6 = 16). |
| EXPECT_EQ(entry->GetDataSize(1), 16); |
| |
| entry->Close(); |
| |
| EXPECT_EQ(write_future.Get(), static_cast<int>(buffer->size())); |
| |
| VerifySharedCacheDeleted(backend.get(), db_id, row_id1); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| CopySharedCacheToBlobTableAbortOnBackendDestruction) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| base::test::TestFuture<int> write_future; |
| std::ignore = entry->WriteData(1, 5, buffer.get(), buffer->size(), |
| write_future.GetCallback(), false); |
| |
| entry->Close(); |
| backend.reset(); |
| |
| EXPECT_EQ(write_future.Get(), net::ERR_ABORTED); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| CopySharedCacheToBlobTableDbHandleHasError) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| auto* sql_entry = static_cast<SqlEntryImpl*>(entry); |
| sql_entry->db_handle()->MarkAsErrorOccurred( |
| SqlPersistentStore::Error::kFailedForTesting); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 5, buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| net::ERR_FAILED); |
| |
| entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| CopySharedCacheToBlobTableReadFromSharedCacheFailure) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // Set invalid shared_cache_resource_id so ReadFromSharedCache fails with |
| // kNotFound. |
| auto* sql_entry = static_cast<SqlEntryImpl*>(entry); |
| sql_entry->db_handle()->set_shared_cache_resource_id(SqlSharedCacheResourceId( |
| SqlSharedCacheDbId(99999), SqlSharedCacheRowId(99999))); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 5, buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| net::ERR_FAILED); |
| EXPECT_TRUE(sql_entry->db_handle()->GetError().has_value()); |
| |
| entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| TEST_P(SqlBackendImplSharedCacheWriteTest, |
| CopySharedCacheToBlobTableWriteToBlobTableFailure) { |
| auto backend = CreateBackendAndInit(); |
| |
| std::optional<SqlSharedCacheDbId> db_id; |
| std::optional<SqlSharedCacheRowId> row_id1; |
| std::optional<SqlSharedCacheRowId> row_id2; |
| |
| SetupSharedCacheEntries(backend, "Small Data", db_id, row_id1, row_id2); |
| |
| TestEntryResultCompletionCallback cb_open; |
| disk_cache::EntryResult open_result = cb_open.GetResult( |
| backend->OpenEntry(kTargetKey, net::HIGHEST, cb_open.callback())); |
| ASSERT_EQ(open_result.net_error(), net::OK); |
| auto* entry = open_result.ReleaseEntry(); |
| |
| // Force store to simulate DB failure so WriteEntryData fails. |
| backend->GetSqlStoreForTest()->SetSimulateDbFailureForTesting(true); |
| |
| auto buffer = base::MakeRefCounted<net::StringIOBuffer>("Append"); |
| net::TestCompletionCallback cb_write; |
| EXPECT_EQ( |
| cb_write.GetResult(entry->WriteData(1, 5, buffer.get(), buffer->size(), |
| cb_write.callback(), false)), |
| net::ERR_FAILED); |
| auto* sql_entry = static_cast<SqlEntryImpl*>(entry); |
| EXPECT_TRUE(sql_entry->db_handle()->GetError().has_value()); |
| |
| entry->Close(); |
| backend->RunUntilAllTasksCompleteForTest(); |
| } |
| |
| } // namespace |
| } // namespace disk_cache |