blob: c1899e6fcff751530a23c2edd57f1b92875ab0f2 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "blob-cache.h"
#include <zxtest/zxtest.h>
#include "cache-node.h"
#include "utils.h"
namespace blobfs {
namespace {
// A mock Node, comparable to Blob.
//
// "ShouldCache" mimics the internal Vnode state machine.
// "UsingMemory" mimics the storage of pages and mappings, which may be evicted
// from memory when references are closed.
class TestNode : public CacheNode, fbl::Recyclable<TestNode> {
public:
explicit TestNode(const Digest& digest, BlobCache* cache) : CacheNode(digest), cache_(cache) {}
void fbl_recycle() final { CacheNode::fbl_recycle(); }
BlobCache& Cache() final { return *cache_; }
bool ShouldCache() const final { return should_cache_; }
void ActivateLowMemory() final { using_memory_ = false; }
bool UsingMemory() { return using_memory_; }
void SetCache(bool should_cache) { should_cache_ = should_cache; }
void SetHighMemory() { using_memory_ = true; }
fs::VnodeProtocolSet GetProtocols() const final { return fs::VnodeProtocol::kFile; }
zx_status_t GetNodeInfoForProtocol(fs::VnodeProtocol protocol, fs::Rights rights,
fs::VnodeRepresentation* representation) {
*representation = fs::VnodeRepresentation::File();
return ZX_OK;
}
private:
BlobCache* cache_;
bool should_cache_ = true;
bool using_memory_ = false;
};
Digest GenerateDigest(size_t seed) {
Digest digest;
digest.Init();
digest.Update(&seed, sizeof(seed));
digest.Final();
return digest;
}
void CheckNothingOpenHelper(BlobCache* cache) {
ASSERT_TRUE(cache);
cache->ForAllOpenNodes([](fbl::RefPtr<CacheNode>) { ZX_ASSERT(false); });
}
TEST(BlobCacheTest, Null) {
BlobCache cache;
ASSERT_NO_FAILURES(CheckNothingOpenHelper(&cache));
cache.Reset();
ASSERT_NO_FAILURES(CheckNothingOpenHelper(&cache));
Digest digest = GenerateDigest(0);
fbl::RefPtr<CacheNode> missing_node;
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, &missing_node));
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Evict(node));
node->SetCache(false);
}
TEST(BlobCacheTest, AddLookupEvict) {
// Add a node to the cache.
BlobCache cache;
Digest digest = GenerateDigest(0);
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
ASSERT_EQ(ZX_ERR_ALREADY_EXISTS, cache.Add(node));
// Observe that we can access the node inside the cache.
fbl::RefPtr<CacheNode> found_node;
ASSERT_OK(cache.Lookup(digest, nullptr));
ASSERT_OK(cache.Lookup(digest, &found_node));
ASSERT_EQ(found_node.get(), node.get());
// Observe that evicting the node removes it from the cache.
ASSERT_OK(cache.Evict(node));
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
// ShouldCache = false, Evicted = false.
//
// This results in the node being deleted from the cache.
TEST(BlobCacheTest, StopCaching) {
BlobCache cache;
Digest digest = GenerateDigest(0);
// The node is also deleted if we stop caching it, instead of just evicting.
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
ASSERT_OK(cache.Lookup(digest, nullptr));
node->SetCache(false);
}
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
// ShouldCache = false, Evicted = True.
//
// This results in the node being deleted from the cache.
TEST(BlobCacheTest, EvictNoCache) {
BlobCache cache;
Digest digest = GenerateDigest(0);
// The node is also deleted if we stop caching it, instead of just evicting.
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
ASSERT_OK(cache.Lookup(digest, nullptr));
ASSERT_OK(cache.Evict(node));
}
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
// ShouldCache = true, Evicted = true.
//
// This results in the node being deleted from the cache.
TEST(BlobCacheTest, EvictWhileCaching) {
BlobCache cache;
Digest digest = GenerateDigest(0);
// The node is automatically deleted if it wants to be cached, but has been
// evicted.
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
ASSERT_OK(cache.Lookup(digest, nullptr));
ASSERT_OK(cache.Evict(node));
node->SetCache(true);
}
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
// This helper function only operates correctly when a single node is open in the cache.
void CheckExistsAloneInOpenCache(BlobCache* cache, void* node_ptr) {
ASSERT_TRUE(cache);
size_t node_count = 0;
cache->ForAllOpenNodes([&node_count, &node_ptr](fbl::RefPtr<CacheNode> node) {
node_count++;
ZX_ASSERT(node.get() == node_ptr);
});
ASSERT_EQ(1, node_count);
}
TEST(BlobCacheTest, CacheAfterRecycle) {
BlobCache cache;
Digest digest = GenerateDigest(0);
void* node_ptr = nullptr;
// Add a node to the cache.
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
node_ptr = node.get();
ASSERT_OK(cache.Add(node));
ASSERT_OK(cache.Lookup(digest, nullptr));
// Observe the node is in the set of open nodes.
ASSERT_NO_FAILURES(CheckExistsAloneInOpenCache(&cache, node_ptr));
}
// Observe the node is in no longer in the set of open nodes, now that it has
// run out of strong references.
ASSERT_NO_FAILURES(CheckNothingOpenHelper(&cache));
// Observe that although the node in in the "closed set", it still exists in the cache,
// and can be re-acquired.
ASSERT_OK(cache.Lookup(digest, nullptr));
// Letting the node go out of scope puts it back in the cache.
{
fbl::RefPtr<CacheNode> node;
ASSERT_OK(cache.Lookup(digest, &node));
ASSERT_EQ(node_ptr, node.get());
ASSERT_NO_FAILURES(CheckExistsAloneInOpenCache(&cache, node_ptr));
}
ASSERT_OK(cache.Lookup(digest, nullptr));
// However, if we stop caching the node, it will be deleted when all references
// go out of scope.
{
fbl::RefPtr<CacheNode> cache_node;
ASSERT_OK(cache.Lookup(digest, &cache_node));
auto vnode = fbl::RefPtr<TestNode>::Downcast(std::move(cache_node));
ASSERT_OK(cache.Evict(vnode));
}
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
TEST(BlobCacheTest, ResetClosed) {
BlobCache cache;
// Create a node which exists in the closed cache.
Digest digest = GenerateDigest(0);
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
}
ASSERT_OK(cache.Lookup(digest, nullptr));
// After resetting, the node should no longer exist.
cache.Reset();
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
TEST(BlobCacheTest, ResetOpen) {
BlobCache cache;
// Create a node which exists in the open cache.
Digest digest = GenerateDigest(0);
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
ASSERT_OK(cache.Add(node));
// After resetting, the node should no longer exist.
cache.Reset();
ASSERT_EQ(ZX_ERR_NOT_FOUND, cache.Lookup(digest, nullptr));
}
TEST(BlobCacheTest, Destructor) {
fbl::RefPtr<TestNode> open_node;
{
BlobCache cache;
Digest open_digest = GenerateDigest(0);
open_node = fbl::AdoptRef(new TestNode(open_digest, &cache));
open_node->SetHighMemory();
Digest closed_digest = GenerateDigest(1);
auto closed_node = fbl::AdoptRef(new TestNode(closed_digest, &cache));
ASSERT_OK(cache.Add(open_node));
ASSERT_OK(cache.Add(closed_node));
}
ASSERT_TRUE(open_node->UsingMemory());
}
TEST(BlobCacheTest, ForAllOpenNodes) {
BlobCache cache;
// Add a bunch of open nodes to the cache.
fbl::RefPtr<TestNode> open_nodes[10];
for (size_t i = 0; i < fbl::count_of(open_nodes); i++) {
open_nodes[i] = fbl::AdoptRef(new TestNode(GenerateDigest(i), &cache));
ASSERT_OK(cache.Add(open_nodes[i]));
}
// For fun, add some nodes to the cache which will become non-open:
// One which runs out of strong references, and another which is evicted.
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(GenerateDigest(0xDEAD), &cache));
ASSERT_OK(cache.Add(node));
}
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(GenerateDigest(0xBEEF), &cache));
ASSERT_OK(cache.Add(node));
ASSERT_OK(cache.Evict(node));
// Double check that the nodes which should be open are open, and that the nodes
// which aren't open aren't visible.
size_t node_index = 0;
cache.ForAllOpenNodes([&open_nodes, &node_index](fbl::RefPtr<CacheNode> node) {
ZX_ASSERT(node_index < fbl::count_of(open_nodes));
for (size_t i = 0; i < fbl::count_of(open_nodes); i++) {
// We should be able to find this node in the set of open nodes -- but only once.
if (open_nodes[i] && open_nodes[i].get() == node.get()) {
open_nodes[i] = nullptr;
node_index++;
return;
}
}
ZX_ASSERT_MSG(false, "Found open node not contained in expected open set");
});
ASSERT_EQ(fbl::count_of(open_nodes), node_index);
}
TEST(BlobCacheTest, CachePolicyEvictImmediately) {
BlobCache cache;
Digest digest = GenerateDigest(0);
cache.SetCachePolicy(CachePolicy::EvictImmediately);
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
node->SetHighMemory();
ASSERT_OK(cache.Add(node));
ASSERT_TRUE(node->UsingMemory());
}
fbl::RefPtr<CacheNode> cache_node;
ASSERT_OK(cache.Lookup(digest, &cache_node));
auto node = fbl::RefPtr<TestNode>::Downcast(std::move(cache_node));
ASSERT_FALSE(node->UsingMemory());
}
TEST(BlobCacheTest, CachePolicyNeverEvict) {
BlobCache cache;
Digest digest = GenerateDigest(0);
cache.SetCachePolicy(CachePolicy::NeverEvict);
{
fbl::RefPtr<TestNode> node = fbl::AdoptRef(new TestNode(digest, &cache));
node->SetHighMemory();
ASSERT_OK(cache.Add(node));
ASSERT_TRUE(node->UsingMemory());
}
fbl::RefPtr<CacheNode> cache_node;
ASSERT_OK(cache.Lookup(digest, &cache_node));
auto node = fbl::RefPtr<TestNode>::Downcast(std::move(cache_node));
ASSERT_TRUE(node->UsingMemory());
}
} // namespace
} // namespace blobfs