blob: 6355f8b4426bd8677cf55be8ed9e3e0353249299 [file] [log] [blame]
// 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