| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/persistent_cache/persistent_cache.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| |
| #include "base/containers/span.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/types/expected.h" |
| #include "components/persistent_cache/backend_storage.h" |
| #include "components/persistent_cache/backend_type.h" |
| #include "components/persistent_cache/mock/mock_backend.h" |
| #include "components/persistent_cache/pending_backend.h" |
| #include "components/persistent_cache/sqlite/sqlite_backend_impl.h" |
| #include "components/persistent_cache/test_utils.h" |
| #include "components/persistent_cache/transaction_error.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/container/flat_hash_map.h" |
| |
| namespace { |
| |
| constexpr const char* kKey = "foo"; |
| |
| using base::test::ErrorIs; |
| using base::test::HasValue; |
| using base::test::ValueIs; |
| using testing::_; |
| using testing::AllOf; |
| using testing::Eq; |
| using testing::Field; |
| using testing::Ge; |
| using testing::Le; |
| using testing::Ne; |
| using testing::Optional; |
| using testing::Return; |
| |
| class PersistentCacheMockedBackendTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| backend_ = std::make_unique<persistent_cache::MockBackend>(); |
| } |
| |
| void CreateCache() { |
| cache_ = std::make_unique<persistent_cache::PersistentCache>( |
| std::move(backend_)); |
| } |
| |
| persistent_cache::MockBackend* GetBackend() { |
| // Can't be called without a cache. |
| CHECK(cache_); |
| return static_cast<persistent_cache::MockBackend*>( |
| cache_->GetBackendForTesting()); |
| } |
| |
| std::unique_ptr<persistent_cache::MockBackend> backend_; |
| std::unique_ptr<persistent_cache::PersistentCache> cache_; |
| }; |
| |
| } // namespace |
| |
| namespace persistent_cache { |
| |
| TEST_F(PersistentCacheMockedBackendTest, CacheFindCallsBackendFind) { |
| CreateCache(); |
| EXPECT_CALL(*GetBackend(), Find(kKey, _)) |
| .WillOnce(Return(base::ok(std::nullopt))); |
| |
| EXPECT_THAT(cache_->Find(kKey, [](size_t) { return base::span<uint8_t>(); }), |
| ValueIs(Eq(std::nullopt))); |
| } |
| |
| TEST_F(PersistentCacheMockedBackendTest, FindReturnsBackendError) { |
| CreateCache(); |
| EXPECT_CALL(*GetBackend(), Find(kKey, _)) |
| .WillOnce(Return(base::unexpected(TransactionError::kTransient))); |
| EXPECT_THAT(cache_->Find(kKey, [](size_t) { return base::span<uint8_t>(); }), |
| ErrorIs(TransactionError::kTransient)); |
| } |
| |
| TEST_F(PersistentCacheMockedBackendTest, InsertReturnsBackendError) { |
| CreateCache(); |
| EXPECT_CALL(*GetBackend(), Insert(_, _, _)) |
| .WillOnce(Return(base::unexpected(TransactionError::kTransient))); |
| EXPECT_THAT(cache_->Insert(kKey, base::byte_span_from_cstring("1")), |
| ErrorIs(TransactionError::kTransient)); |
| } |
| |
| TEST_F(PersistentCacheMockedBackendTest, CacheInsertCallsBackendInsert) { |
| CreateCache(); |
| EXPECT_CALL(*GetBackend(), Insert(kKey, _, _)); |
| EXPECT_THAT(cache_->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| } |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| |
| class PersistentCacheTest : public testing::Test, |
| public testing::WithParamInterface<BackendType> { |
| protected: |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| backend_storage_.emplace(GetParam(), temp_dir_.GetPath()); |
| } |
| |
| // Returns the cache base name for a new cache and the new cache itself. |
| std::pair<base::FilePath, std::unique_ptr<PersistentCache>> OpenCache( |
| bool single_connection = false, |
| bool journal_mode_wal = false) { |
| auto [cache_name, pending_backend] = |
| MakePendingBackend(single_connection, journal_mode_wal); |
| auto cache = PersistentCache::Bind(*std::move(pending_backend)); |
| if (!cache) { |
| ADD_FAILURE() << "Failed to bind PersistentCache"; |
| return {}; |
| } |
| return {std::move(cache_name), std::move(cache)}; |
| } |
| |
| // Returns the cache base name for a new cache and a pending backend for it. |
| std::pair<base::FilePath, std::optional<PendingBackend>> MakePendingBackend( |
| bool single_connection, |
| bool journal_mode_wal) { |
| auto cache_name = base::FilePath::FromASCII( |
| base::StrCat({"Cache", base::NumberToString(next_backend_index_++)})); |
| auto pending_backend = backend_storage_->MakePendingBackend( |
| cache_name, single_connection, journal_mode_wal); |
| if (!pending_backend) { |
| ADD_FAILURE() << "Failed to make PendingBackend"; |
| return {}; |
| } |
| return {std::move(cache_name), std::move(pending_backend)}; |
| } |
| |
| BackendStorage& backend_storage() { return *backend_storage_; } |
| |
| private: |
| base::ScopedTempDir temp_dir_; |
| std::optional<BackendStorage> backend_storage_; |
| int next_backend_index_ = 0; |
| }; |
| |
| TEST_P(PersistentCacheTest, FindReturnsNullWhenEmpty) { |
| auto [cache_name, cache] = OpenCache(); |
| EXPECT_THAT(cache->Find(kKey, [](size_t) { return base::span<uint8_t>(); }), |
| ValueIs(Eq(std::nullopt))); |
| } |
| |
| TEST_P(PersistentCacheTest, FindReturnsValueWhenPresent) { |
| auto [cache_name, cache] = OpenCache(); |
| for (int i = 0; i < 20; ++i) { |
| std::string key = base::NumberToString(i); |
| auto value = base::as_byte_span(key); |
| |
| EXPECT_THAT(FindEntry(*cache, key), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(cache->Insert(key, value), HasValue()); |
| ASSERT_THAT(FindEntry(*cache, key), ValueIs(Optional(ContentEq(value)))); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, EmptyValueIsStorable) { |
| auto [cache_name, cache] = OpenCache(); |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("")), |
| HasValue()); |
| ASSERT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional(ContentEq(base::span<uint8_t>())))); |
| } |
| |
| TEST_P(PersistentCacheTest, ValueContainingNullCharIsStorable) { |
| auto [cache_name, cache] = OpenCache(); |
| constexpr std::array<std::uint8_t, 5> value_array{'\0', 'a', 'b', 'c', '\0'}; |
| const base::span<const std::uint8_t> value_span(value_array); |
| CHECK_EQ(value_span.size(), value_array.size()) |
| << "All characters must be included in span"; |
| |
| EXPECT_THAT(cache->Insert(kKey, value_span), HasValue()); |
| ASSERT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional(ContentEq(value_span)))); |
| } |
| |
| TEST_P(PersistentCacheTest, ValueContainingInvalidUtf8IsStorable) { |
| auto [cache_name, cache] = OpenCache(); |
| constexpr std::array<std::uint8_t, 4> value_array{0x20, 0x0F, 0xFF, 0xFF}; |
| const base::span<const std::uint8_t> value_span(value_array); |
| CHECK( |
| !base::IsStringUTF8(std::string(value_array.begin(), value_array.end()))) |
| << "Test needs invalid utf8"; |
| |
| EXPECT_THAT(cache->Insert(kKey, value_span), HasValue()); |
| ASSERT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional(ContentEq(value_span)))); |
| } |
| |
| TEST_P(PersistentCacheTest, OverwritingChangesValue) { |
| auto [cache_name, cache] = OpenCache(); |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("2")), |
| HasValue()); |
| |
| ASSERT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional(ContentEq(base::byte_span_from_cstring("2"))))); |
| } |
| |
| TEST_P(PersistentCacheTest, OverwritingChangesValueVaryingSizes) { |
| auto [cache_name, cache] = OpenCache(); |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| EXPECT_THAT( |
| cache->Insert(kKey, base::as_byte_span(std::string(1024 * 7, 'b'))), |
| HasValue()); |
| |
| ASSERT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional( |
| ContentEq(base::as_byte_span(std::string(1024 * 7, 'b')))))); |
| } |
| |
| TEST_P(PersistentCacheTest, MetadataIsRetrievable) { |
| EntryMetadata metadata{.input_signature = |
| base::Time::Now().InMillisecondsSinceUnixEpoch()}; |
| |
| auto [cache_name, cache] = OpenCache(); |
| |
| int64_t seconds_since_epoch = |
| base::Time::Now().InMillisecondsSinceUnixEpoch() / 1000; |
| |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1"), metadata), |
| HasValue()); |
| |
| EXPECT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional(Field( |
| &Entry::metadata, |
| AllOf(Field(&EntryMetadata::input_signature, |
| metadata.input_signature), |
| Field(&EntryMetadata::write_timestamp, |
| AllOf(Ge(seconds_since_epoch), |
| // The test is supposed to time out before |
| // it takes this long to insert a value. |
| Le(seconds_since_epoch + 30)))))))); |
| } |
| |
| TEST_P(PersistentCacheTest, OverwritingChangesMetadata) { |
| EntryMetadata metadata{.input_signature = |
| base::Time::Now().InMillisecondsSinceUnixEpoch()}; |
| |
| auto [cache_name, cache] = OpenCache(); |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1"), metadata), |
| HasValue()); |
| |
| EXPECT_THAT(FindEntry(*cache, kKey), |
| ValueIs(Optional( |
| Field(&Entry::metadata, Field(&EntryMetadata::input_signature, |
| metadata.input_signature))))); |
| |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| HasValue()); |
| |
| EXPECT_THAT( |
| FindEntry(*cache, kKey), |
| ValueIs(Optional( |
| Field(&Entry::metadata, Field(&EntryMetadata::input_signature, 0))))); |
| } |
| |
| TEST_P(PersistentCacheTest, MultipleEphemeralCachesAreIndependent) { |
| for (int i = 0; i < 3; ++i) { |
| auto [cache_name, cache] = OpenCache(); |
| |
| // `kKey` never inserted in this cache so not found. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| |
| // `kKey` now present. |
| EXPECT_THAT(FindEntry(*cache, kKey), HasValue()); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, MultipleLiveCachesAreIndependent) { |
| std::vector<std::unique_ptr<PersistentCache>> caches; |
| for (int i = 0; i < 3; ++i) { |
| auto [cache_name, new_cache] = OpenCache(); |
| caches.push_back(std::move(new_cache)); |
| std::unique_ptr<PersistentCache>& cache = caches.back(); |
| |
| // `kKey` never inserted in this cache so not found. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| // `kKey` now present. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Ne(std::nullopt))); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, EphemeralCachesSharingParamsShareData) { |
| auto [cache_name, main_cache] = OpenCache(); |
| ASSERT_TRUE(main_cache); |
| for (int i = 0; i < 3; ++i) { |
| ASSERT_OK_AND_ASSIGN( |
| auto pending_backend, |
| backend_storage().ShareReadWriteConnection(cache_name, *main_cache)); |
| auto cache = PersistentCache::Bind(std::move(pending_backend)); |
| ASSERT_TRUE(cache); |
| |
| // First run, setup. |
| if (i == 0) { |
| // `kKey` never inserted so not found. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| |
| // `kKey` now present. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Ne(std::nullopt))); |
| } else { |
| // `kKey` is present because data is shared. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Ne(std::nullopt))); |
| } |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, LiveCachesSharingParamsShareData) { |
| auto [cache_name, main_cache] = OpenCache(); |
| std::vector<std::unique_ptr<PersistentCache>> caches; |
| |
| for (int i = 0; i < 3; ++i) { |
| ASSERT_OK_AND_ASSIGN( |
| auto pending_backend, |
| backend_storage().ShareReadWriteConnection(cache_name, *main_cache)); |
| caches.push_back(PersistentCache::Bind(std::move(pending_backend))); |
| std::unique_ptr<PersistentCache>& cache = caches.back(); |
| ASSERT_TRUE(cache); |
| |
| // First run, setup. |
| if (i == 0) { |
| // `kKey` never inserted so not found. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| |
| // `kKey` now present. |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Ne(std::nullopt))); |
| } else { |
| EXPECT_THAT(FindEntry(*cache, kKey), ValueIs(Ne(std::nullopt))); |
| } |
| } |
| } |
| |
| // Create an instance and share it for read-only access to others. |
| TEST_P(PersistentCacheTest, MultipleInstancesShareData) { |
| // The main read-write instance. |
| auto [cache_name, main_cache] = OpenCache(); |
| |
| std::vector<std::unique_ptr<PersistentCache>> caches; |
| for (int i = 0; i < 3; ++i) { |
| // Export a read-only view to the main instance. |
| ASSERT_OK_AND_ASSIGN( |
| auto pending_backend, |
| backend_storage().ShareReadOnlyConnection(cache_name, *main_cache)); |
| // Create a new instance that will read from the original. |
| caches.push_back(PersistentCache::Bind(std::move(pending_backend))); |
| std::unique_ptr<PersistentCache>& ro_cache = caches.back(); |
| ASSERT_TRUE(ro_cache); |
| |
| if (i == 0) { |
| // The db is empty when the first client connects. |
| EXPECT_THAT(FindEntry(*ro_cache, kKey), ValueIs(Eq(std::nullopt))); |
| |
| // Insert a value via the read-write instance. |
| EXPECT_THAT(main_cache->Insert(kKey, base::byte_span_from_cstring("1")), |
| HasValue()); |
| |
| // It should be there. |
| EXPECT_THAT(FindEntry(*ro_cache, kKey), ValueIs(Ne(std::nullopt))); |
| } |
| |
| // The new read-only client should see the value that was previously |
| // inserted. |
| EXPECT_THAT(FindEntry(*ro_cache, kKey), ValueIs(Ne(std::nullopt))); |
| } |
| } |
| |
| // Create an instance and share it for read-write access to others. |
| TEST_P(PersistentCacheTest, MultipleInstancesCanWriteData) { |
| static constexpr char kThisKeyPrefix[] = "thiskey-"; |
| static constexpr char kOtherKeyPrefix[] = "otherkey-"; |
| |
| // The main read-write instance. |
| auto [cache_name, main_cache] = OpenCache(); |
| |
| std::vector<std::unique_ptr<PersistentCache>> caches; |
| for (int i = 0; i < 3; ++i) { |
| // Share a read-write view to the main instance. |
| ASSERT_OK_AND_ASSIGN( |
| auto pending_backend, |
| backend_storage().ShareReadWriteConnection(cache_name, *main_cache)); |
| // Create a new instance that will read/write from/to the original. |
| caches.push_back(PersistentCache::Bind(std::move(pending_backend))); |
| std::unique_ptr<PersistentCache>& rw_cache = caches.back(); |
| ASSERT_TRUE(rw_cache); |
| |
| // This new cache has access to all previous values. |
| for (int j = 0; j < i; ++j) { |
| std::string value = base::NumberToString(j); |
| |
| EXPECT_THAT(FindEntry(*rw_cache, base::StrCat({kThisKeyPrefix, value})), |
| ValueIs(Ne(std::nullopt))); |
| |
| EXPECT_THAT(FindEntry(*rw_cache, base::StrCat({kOtherKeyPrefix, value})), |
| ValueIs(Ne(std::nullopt))); |
| } |
| |
| // A new value added from the original is seen here. |
| std::string value = base::NumberToString(i); |
| std::string other_key = base::StrCat({kOtherKeyPrefix, value}); |
| |
| EXPECT_THAT(FindEntry(*main_cache, other_key), ValueIs(Eq(std::nullopt))); |
| EXPECT_THAT(FindEntry(*rw_cache, other_key), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(main_cache->Insert(other_key, base::as_byte_span(value)), |
| HasValue()); |
| |
| EXPECT_THAT(FindEntry(*main_cache, other_key), ValueIs(Ne(std::nullopt))); |
| EXPECT_THAT(FindEntry(*rw_cache, other_key), ValueIs(Ne(std::nullopt))); |
| |
| // A new value added here is seen in the original. |
| std::string this_key = base::StrCat({kThisKeyPrefix, value}); |
| |
| EXPECT_THAT(FindEntry(*main_cache, this_key), ValueIs(Eq(std::nullopt))); |
| EXPECT_THAT(FindEntry(*rw_cache, this_key), ValueIs(Eq(std::nullopt))); |
| |
| EXPECT_THAT(rw_cache->Insert(this_key, base::as_byte_span(value)), |
| HasValue()); |
| |
| EXPECT_THAT(FindEntry(*main_cache, this_key), ValueIs(Ne(std::nullopt))); |
| EXPECT_THAT(FindEntry(*rw_cache, this_key), ValueIs(Ne(std::nullopt))); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, ThreadSafeAccess) { |
| base::test::TaskEnvironment env; |
| |
| // Create the cache and insert on this sequence. |
| auto value = base::byte_span_from_cstring("1"); |
| auto [cache_name, main_cache] = OpenCache(); |
| EXPECT_THAT(main_cache->Insert(kKey, value), HasValue()); |
| |
| // FindEntry() on ThreadPool. Result should be expected and there are no |
| // sequence checkers tripped. |
| base::test::TestFuture<base::expected<std::optional<Entry>, TransactionError>> |
| future_entry; |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce( |
| [](PersistentCache* cache, |
| base::OnceCallback<void( |
| base::expected<std::optional<Entry>, TransactionError>)> |
| on_entry) { |
| auto entry = FindEntry(*cache, kKey); |
| std::move(on_entry).Run(std::move(entry)); |
| }, |
| main_cache.get(), future_entry.GetSequenceBoundCallback())); |
| |
| // Wait for result availability and check. |
| ASSERT_OK_AND_ASSIGN(std::optional<Entry> entry, future_entry.Take()); |
| ASSERT_THAT(entry, Optional(ContentEq(value))); |
| } |
| |
| TEST_P(PersistentCacheTest, MultipleLiveEntries) { |
| auto [cache_name, cache] = OpenCache(); |
| absl::flat_hash_map<std::string, std::optional<Entry>> entries; |
| |
| for (size_t i = 0; i < 20; ++i) { |
| std::string key = base::NumberToString(i); |
| auto value = base::as_byte_span(key); |
| EXPECT_THAT(cache->Insert(key, value), HasValue()); |
| // Create an entry where the value is equal to the key. |
| ASSERT_OK_AND_ASSIGN(entries[key], FindEntry(*cache, key)); |
| } |
| |
| // Verify that entries have the expected content. |
| for (auto& [key, entry] : entries) { |
| ASSERT_THAT(entry, Optional(ContentEq(base::as_byte_span(key)))); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, MultipleLiveEntriesWithVaryingLifetime) { |
| static constexpr size_t kNumberOfEntries = 40; |
| |
| auto [cache_name, cache] = OpenCache(); |
| absl::flat_hash_map<std::string, std::optional<Entry>> entries; |
| |
| for (size_t i = 0; i < kNumberOfEntries; ++i) { |
| std::string key = base::NumberToString(i); |
| auto value = base::as_byte_span(key); |
| EXPECT_THAT(cache->Insert(key, value), HasValue()); |
| // Create an entry where the value is equal to the key. |
| ASSERT_OK_AND_ASSIGN(entries[key], FindEntry(*cache, key)); |
| |
| // Every other iteration delete an entry that came before. |
| if (i && i % 2 == 0) { |
| entries.erase(base::NumberToString(i / 2)); |
| } |
| } |
| |
| // Assert that some entries remain to be verified in the next loop. |
| ASSERT_GE(entries.size(), kNumberOfEntries / 2); |
| |
| // Verify that entries have the expected content. |
| for (auto& [key, entry] : entries) { |
| ASSERT_THAT(entry, Optional(ContentEq(base::as_byte_span(key)))); |
| } |
| } |
| |
| TEST_P(PersistentCacheTest, AbandonementDetected) { |
| auto [cache_name, cache] = OpenCache(); |
| |
| // Value is correctly inserted. |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| HasValue()); |
| ASSERT_OK_AND_ASSIGN(auto entry, FindEntry(*cache, kKey)); |
| EXPECT_NE(entry, std::nullopt); |
| |
| // Abandon cache, no further operations will succeed. |
| EXPECT_EQ(cache->Abandon(), LockState::kNotHeld); |
| |
| // Calling FindEntry() is no longer successful. |
| EXPECT_THAT(FindEntry(*cache, kKey), |
| ErrorIs(TransactionError::kConnectionError)); |
| |
| // Calling Insert() is no longer successful. |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| ErrorIs(TransactionError::kConnectionError)); |
| } |
| |
| TEST_P(PersistentCacheTest, RecoveryFromTransientError) { |
| auto [cache_name, cache] = OpenCache(); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto pending_reader, |
| backend_storage().ShareReadOnlyConnection(cache_name, *cache)); |
| |
| // Baseline insert works. |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| HasValue()); |
| |
| // Lock the db file in shared mode. |
| ASSERT_OK_AND_ASSIGN( |
| auto reader_vfs_file_set, |
| SqliteBackendImpl::BindToFileSet(std::move(pending_reader))); |
| SandboxedFile* reader_db_file = reader_vfs_file_set.GetSandboxedDbFile(); |
| reader_db_file->OnFileOpened( |
| reader_db_file->TakeUnderlyingFile(SandboxedFile::FileType::kMainDb)); |
| ASSERT_EQ(reader_db_file->Lock(SQLITE_LOCK_SHARED), SQLITE_OK); |
| |
| // Held lock causes transient error. |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| ErrorIs(TransactionError::kTransient)); |
| |
| // Unlock works. |
| ASSERT_EQ(reader_db_file->Unlock(SQLITE_LOCK_NONE), SQLITE_OK); |
| ASSERT_EQ(reader_db_file->LockModeForTesting(), SQLITE_LOCK_NONE); |
| reader_db_file->Close(); |
| |
| // Insert now succeeds. |
| EXPECT_THAT( |
| cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}), |
| HasValue()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| PersistentCacheTest, |
| testing::Values(BackendType::kSqlite)); |
| #endif |
| |
| } // namespace persistent_cache |