//------------------------------------------------------------------------------
// File: ShardedCacheTests.cc
// Author: Abhishek Lekshmanan - CERN
//------------------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2022 CERN/Switzerland *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see .*
************************************************************************/
#include
#include "common/ShardedCache.hh"
TEST(ShardedCache, Construction)
{
ShardedCache cache(8, 100);
ASSERT_EQ(cache.num_shards(), 256);
ASSERT_EQ(cache.num_content_shards(), 256);
EXPECT_EQ(cache.num_entries(), 0);
}
TEST(ShardedCache, CalculateShard)
{
ShardedCache cache(8, 100);
std::hash hasher{};
ASSERT_GE(cache.calculateShard("hello"), 0);
ASSERT_LT(cache.calculateShard("hello"), 256);
EXPECT_EQ(cache.calculateShard("hello"),
hasher("hello") % 256);
}
TEST(ShardedCache, NoGC)
{
ShardedCache cache(8);
ASSERT_EQ(cache.num_shards(), 256);
ASSERT_EQ(cache.num_content_shards(), 256);
EXPECT_EQ(cache.num_entries(), 0);
std::hash hasher{};
ASSERT_GE(cache.calculateShard("hello"), 0);
ASSERT_LT(cache.calculateShard("hello"), 256);
EXPECT_EQ(cache.calculateShard("hello"),
hasher("hello") % 256);
}
TEST(ShardedCache, EmptyRetrieve)
{
ShardedCache cache(8, 100);
auto result = cache.retrieve("hello");
EXPECT_EQ(result, nullptr);
}
TEST(ShardedCache, ValueRetrieve)
{
ShardedCache cache(8, 100);
auto val = std::make_unique(5);
ASSERT_TRUE(cache.store("hello", std::move(val)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
}
TEST(ShardedCache, ValueNonExpiry)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
// Since we hold a valid reference to the entry it never expires
std::this_thread::sleep_for(std::chrono::milliseconds(30));
auto result2 = cache.retrieve("hello");
EXPECT_EQ(result2, result);
// now release ownership. The entry should expire
result.reset();
result2.reset();
std::this_thread::sleep_for(std::chrono::milliseconds(30));
auto result3 = cache.retrieve("hello");
EXPECT_EQ(result3, nullptr);
}
TEST(ShardedCache, ValueExpiry)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
result.reset();
// The entry takes between 2*ttl and 3*ttl to completely expire
// as the first round just marks the entry as expired and the second round
// actually deletes it.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto result2 = cache.retrieve("hello");
EXPECT_EQ(result2, nullptr);
}
TEST(ShardedCache, NoGCRetrieve)
{
ShardedCache cache(8);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
}
TEST(ShardedCache, LateGCRun)
{
ShardedCache cache(8);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
result.reset();
cache.reset_cleanup_thread(10);
std::this_thread::sleep_for(std::chrono::milliseconds(30));
ASSERT_EQ(cache.num_entries(), 0);
auto result2 = cache.retrieve("hello");
EXPECT_EQ(result2, nullptr);
}
TEST(ShardedCache, get_shard_except)
{
ShardedCache cache(8, 10);
std::unordered_map empty_map;
for (unsigned int i = 0; i < cache.num_shards(); i++) {
ASSERT_EQ(cache.get_shard(i), empty_map);
}
ASSERT_THROW(cache.get_shard(cache.num_shards()), std::out_of_range);
ASSERT_THROW(cache.get_shard(cache.num_shards() + 1), std::out_of_range);
ASSERT_THROW(cache.get_shard(-1), std::out_of_range);
}
TEST(ShardedCache, get_shard)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_EQ(cache.num_entries(), 1);
auto shard_copy = cache.get_shard(cache.calculateShard("hello"));
ASSERT_EQ(shard_copy.size(), 1);
ASSERT_EQ(shard_copy["hello"], 5);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ASSERT_EQ(cache.num_entries(), 0);
}
TEST(ShardedCache, clear)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_EQ(cache.num_entries(), 1);
cache.clear();
ASSERT_EQ(cache.num_entries(), 0);
auto result = cache.retrieve("hello");
EXPECT_EQ(result, nullptr);
}
TEST(ShardedCache, reuse_after_clear)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_EQ(cache.num_entries(), 1);
cache.clear();
ASSERT_EQ(cache.num_entries(), 0);
auto result = cache.retrieve("hello");
EXPECT_EQ(result, nullptr);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_EQ(cache.num_entries(), 1);
result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
}
TEST(ShardedCache, value_after_clear)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_EQ(cache.num_entries(), 1);
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
ASSERT_EQ(result.use_count(), 2);
cache.clear();
// result is a shared_ptr, so regardless it is still valid
// however no longer present in the cache map
ASSERT_EQ(cache.num_entries(), 0);
ASSERT_EQ(result.use_count(), 1);
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
}
TEST(ShardedCache, fetch_add)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
auto result = cache.retrieve("hello");
ASSERT_NE(result, nullptr);
EXPECT_EQ(*result, 5);
auto old_val = cache.fetch_add("hello", 1);
EXPECT_EQ(old_val, 5);
auto result2 = cache.retrieve("hello");
ASSERT_NE(result2, nullptr);
EXPECT_EQ(*result2, 6);
}
TEST(ShardedCache, fetch_add_empty_key)
{
ShardedCache cache(8, 10);
auto null_result = cache.retrieve("hello");
ASSERT_EQ(null_result, nullptr);
cache.fetch_add("hello", 1);
auto result3 = cache.retrieve("hello");
ASSERT_NE(result3, nullptr);
EXPECT_EQ(*result3, 1);
}
TEST(ShardedCache, fetch_add_multithreaded)
{
// Give a minute of GC instead of 10ms, so that we'd not expire any caches
// before the function completes! Otherwise with 10ms it can so happen that
// in small env by the time all the threads are run, GC would just run before we
// retrieve the cache.
ShardedCache cache(8, 60*1000);
std::vector threads;
for (int i=0; i<200; i++) {
threads.emplace_back([&cache]() {
cache.fetch_add("mykey",1);
});
}
for (auto& t : threads) {
t.join();
}
ASSERT_EQ(cache.num_entries(), 1);
auto result = cache.retrieve("mykey");
ASSERT_EQ(*result, 200);
}
TEST(ShardedCache, invalidate)
{
ShardedCache cache(8, 10);
ASSERT_TRUE(cache.store("hello", std::make_unique(5)));
ASSERT_TRUE(cache.store("hello2", std::make_unique(6)));
ASSERT_EQ(cache.num_entries(), 2);
bool ret = cache.invalidate("hello");
ASSERT_EQ(ret, true);
ASSERT_EQ(cache.num_entries(), 1);
ret = cache.invalidate("hello");
ASSERT_EQ(ret, false);
auto result = cache.retrieve("hello");
EXPECT_EQ(result, nullptr);
}