1390 lines
49 KiB
C++
1390 lines
49 KiB
C++
// client_local_hot_cache_test.cpp
|
|
#include "client_service.h"
|
|
#include "client_buffer.hpp"
|
|
#include "count_min_sketch.h"
|
|
#include "local_hot_cache.h"
|
|
#include "replica.h"
|
|
#include "test_server_helpers.h"
|
|
#include "utils.h"
|
|
|
|
#include <glog/logging.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
// Network headers for getting local IP (Linux/Unix)
|
|
#include <ifaddrs.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
#include <arpa/inet.h>
|
|
|
|
namespace mooncake {
|
|
namespace testing {
|
|
|
|
// Helper function to get local IP address
|
|
// Priority: 1) Environment variable MC_TEST_LOCAL_IP, 2) System call to get
|
|
// first non-loopback IP
|
|
std::string getLocalIpAddress() {
|
|
// Try to get IP from environment variable first (for testing flexibility)
|
|
const char* env_ip = std::getenv("MC_TEST_LOCAL_IP");
|
|
if (env_ip && strlen(env_ip) > 0) {
|
|
return std::string(env_ip);
|
|
}
|
|
|
|
// Fallback: get local IP using system calls
|
|
// Get the first non-loopback IPv4 address
|
|
struct ifaddrs *ifaddr, *ifa;
|
|
std::string ip = "127.0.0.1"; // Default fallback
|
|
|
|
if (getifaddrs(&ifaddr) == -1) {
|
|
return ip;
|
|
}
|
|
|
|
for (ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
|
|
if (ifa->ifa_addr == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
// Look for IPv4 address
|
|
if (ifa->ifa_addr->sa_family == AF_INET) {
|
|
// Skip loopback interface
|
|
if (strcmp(ifa->ifa_name, "lo") == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Check if interface is UP
|
|
if (!(ifa->ifa_flags & IFF_UP)) {
|
|
continue;
|
|
}
|
|
|
|
struct sockaddr_in* sin = (struct sockaddr_in*)ifa->ifa_addr;
|
|
char host[NI_MAXHOST];
|
|
if (getnameinfo((struct sockaddr*)sin, sizeof(struct sockaddr_in),
|
|
host, NI_MAXHOST, nullptr, 0,
|
|
NI_NUMERICHOST) == 0) {
|
|
ip = std::string(host);
|
|
break; // Use the first non-loopback interface
|
|
}
|
|
}
|
|
}
|
|
|
|
freeifaddrs(ifaddr);
|
|
return ip;
|
|
}
|
|
|
|
class LocalHotCacheTest : public ::testing::Test {
|
|
protected:
|
|
static void SetUpTestSuite() {
|
|
google::InitGoogleLogging("LocalHotCacheTest");
|
|
FLAGS_logtostderr = 1;
|
|
|
|
// Start in-proc master and metadata servers (non-HA)
|
|
ASSERT_TRUE(master_.Start(InProcMasterConfigBuilder().build()))
|
|
<< "Failed to start in-proc master";
|
|
master_address_ = master_.master_address();
|
|
metadata_url_ = master_.metadata_url();
|
|
LOG(INFO) << "Started in-proc master at " << master_address_
|
|
<< ", metadata="
|
|
<< (metadata_url_.empty() ? "disabled" : metadata_url_);
|
|
}
|
|
|
|
static void TearDownTestSuite() {
|
|
master_.Stop();
|
|
google::ShutdownGoogleLogging();
|
|
}
|
|
|
|
void SetUp() override {}
|
|
|
|
void TearDown() override {}
|
|
|
|
// Helper to create a slice with test data
|
|
Slice CreateSlice(size_t size, char fill_char = 'A') {
|
|
std::vector<char> data(size, fill_char);
|
|
Slice slice;
|
|
slice.ptr = data.data();
|
|
slice.size = size;
|
|
test_data_.push_back(std::move(data)); // Keep data alive
|
|
return slice;
|
|
}
|
|
|
|
// Helper to verify slice data in cache
|
|
void VerifySliceData(HotMemBlock* block, size_t expected_size,
|
|
char expected_char) {
|
|
ASSERT_NE(block, nullptr);
|
|
ASSERT_EQ(block->size, expected_size);
|
|
const char* data = static_cast<const char*>(block->addr);
|
|
for (size_t i = 0; i < expected_size; ++i) {
|
|
EXPECT_EQ(data[i], expected_char)
|
|
<< "Data mismatch at offset " << i;
|
|
}
|
|
}
|
|
|
|
// Helper to put a key-slice pair into cache using the new API
|
|
static bool PutHotKeyHelper(LocalHotCache& cache, const std::string& key,
|
|
const Slice& slice) {
|
|
// Parameter validation
|
|
if (key.empty() || slice.ptr == nullptr || slice.size == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Fast path: if key already exists, just touch LRU but do not
|
|
// overwrite data
|
|
if (cache.TouchHotKey(key)) {
|
|
return true;
|
|
}
|
|
|
|
// Obtain a free block (may evict from LRU tail)
|
|
HotMemBlock* block = cache.GetFreeBlock();
|
|
if (!block) {
|
|
return false;
|
|
}
|
|
|
|
// Check size compatibility with the block's available capacity
|
|
if (slice.size > block->size) {
|
|
// Slice too big for this block, return block to pool
|
|
block->key_.clear();
|
|
cache.PutHotKey(block);
|
|
return false;
|
|
}
|
|
|
|
// Copy data into the block
|
|
std::memcpy(block->addr, slice.ptr, slice.size);
|
|
block->size = slice.size;
|
|
block->key_ = key;
|
|
|
|
// Publish the new mapping using the existing block-based API
|
|
return cache.PutHotKey(block);
|
|
}
|
|
|
|
// Helper to setup client with hot cache enabled and mount segment
|
|
struct TestClientContext {
|
|
std::shared_ptr<Client> client;
|
|
void* segment_ptr;
|
|
size_t segment_size;
|
|
const char* original_env;
|
|
};
|
|
|
|
// Helper to create client with common parameters
|
|
std::optional<std::shared_ptr<Client>> CreateTestClient(
|
|
const std::string& hostname) {
|
|
return Client::Create(hostname,
|
|
"P2PHANDSHAKE", // use in-proc metadata server
|
|
"tcp", std::nullopt,
|
|
master_address_); // master server address
|
|
}
|
|
|
|
// Shared in-proc master for tests
|
|
static mooncake::testing::InProcMaster master_;
|
|
static std::string master_address_;
|
|
static std::string metadata_url_;
|
|
|
|
TestClientContext SetupTestClientWithHotCache() {
|
|
TestClientContext ctx;
|
|
ctx.original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "33554432",
|
|
1); // 32MB = 2 blocks
|
|
|
|
std::string local_ip = getLocalIpAddress();
|
|
std::string local_hostname = local_ip + ":12345";
|
|
|
|
auto client_opt = CreateTestClient(local_hostname);
|
|
if (!client_opt.has_value()) {
|
|
return ctx; // Return empty context
|
|
}
|
|
|
|
ctx.client = client_opt.value();
|
|
if (!ctx.client->IsHotCacheEnabled()) {
|
|
return ctx; // Return context with client but hot cache not enabled
|
|
}
|
|
|
|
ctx.segment_size = 64 * 1024 * 1024; // 64MB
|
|
ctx.segment_ptr = allocate_buffer_allocator_memory(ctx.segment_size);
|
|
if (ctx.segment_ptr == nullptr) {
|
|
return ctx; // Return context without segment
|
|
}
|
|
|
|
auto mount_result =
|
|
ctx.client->MountSegment(ctx.segment_ptr, ctx.segment_size);
|
|
if (!mount_result.has_value()) {
|
|
free_memory("", ctx.segment_ptr);
|
|
ctx.segment_ptr = nullptr;
|
|
return ctx; // Return context with unmounted segment
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
void CleanupTestClient(TestClientContext& ctx) {
|
|
if (ctx.client && ctx.segment_ptr) {
|
|
ctx.client->UnmountSegment(ctx.segment_ptr, ctx.segment_size);
|
|
free_memory("", ctx.segment_ptr);
|
|
} else if (ctx.segment_ptr) {
|
|
free_memory("", ctx.segment_ptr);
|
|
}
|
|
if (ctx.original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", ctx.original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
void PutTestData(Client* client, const std::string& key,
|
|
const std::string& data) {
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
std::vector<char> put_buffer(data.size());
|
|
std::memcpy(put_buffer.data(), data.data(), data.size());
|
|
std::vector<Slice> put_slices;
|
|
put_slices.emplace_back(Slice{put_buffer.data(), data.size()});
|
|
auto put_result = client->Put(key, put_slices, config);
|
|
(void)put_result; // Suppress unused variable warning
|
|
// If Put fails, the test will fail when trying to Get the data
|
|
}
|
|
|
|
private:
|
|
std::vector<std::vector<char>> test_data_; // Keep test data alive
|
|
};
|
|
|
|
// Static member definitions
|
|
mooncake::testing::InProcMaster LocalHotCacheTest::master_;
|
|
std::string LocalHotCacheTest::master_address_;
|
|
std::string LocalHotCacheTest::metadata_url_;
|
|
|
|
// Test LocalHotCache construction
|
|
TEST_F(LocalHotCacheTest, Construction) {
|
|
const size_t cache_size =
|
|
64 * 1024 * 1024; // 64MB = 4 blocks (default 16MB each)
|
|
LocalHotCache cache(cache_size);
|
|
|
|
EXPECT_GT(cache.GetCacheSize(), 0);
|
|
EXPECT_EQ(cache.GetCacheSize(), 4); // 64MB / 16MB = 4 blocks
|
|
EXPECT_EQ(cache.GetBlockSize(), 16 * 1024 * 1024); // Default 16MB
|
|
}
|
|
|
|
// Test LocalHotCache construction with custom block size
|
|
TEST_F(LocalHotCacheTest, ConstructionWithCustomBlockSize) {
|
|
const size_t block_size = 8 * 1024 * 1024; // 8MB
|
|
const size_t cache_size = 32 * 1024 * 1024; // 32MB = 4 blocks (8MB each)
|
|
LocalHotCache cache(cache_size, block_size);
|
|
|
|
EXPECT_GT(cache.GetCacheSize(), 0);
|
|
EXPECT_EQ(cache.GetCacheSize(), 4); // 32MB / 8MB = 4 blocks
|
|
EXPECT_EQ(cache.GetBlockSize(), block_size);
|
|
}
|
|
|
|
// Test LocalHotCache with zero size
|
|
TEST_F(LocalHotCacheTest, ZeroSizeCache) {
|
|
LocalHotCache cache(0);
|
|
EXPECT_EQ(cache.GetCacheSize(), 0);
|
|
}
|
|
|
|
// Test PutHotKey basic functionality
|
|
TEST_F(LocalHotCacheTest, PutHotKeyBasic) {
|
|
const size_t cache_size =
|
|
32 * 1024 * 1024; // 32MB = 2 blocks (default 16MB each)
|
|
LocalHotCache cache(cache_size);
|
|
|
|
const size_t slice_size = 1024;
|
|
Slice slice = CreateSlice(slice_size, 'X');
|
|
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "test_key_0", slice));
|
|
EXPECT_TRUE(cache.HasHotKey("test_key_0"));
|
|
|
|
HotMemBlock* block = cache.GetHotKey("test_key_0");
|
|
VerifySliceData(block, slice_size, 'X');
|
|
}
|
|
|
|
// Test PutHotKey with multiple keys
|
|
TEST_F(LocalHotCacheTest, PutHotKeyMultipleKeys) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 32MB = 2 blocks
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Put first key
|
|
Slice slice1 = CreateSlice(1024, 'A');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key1", slice1));
|
|
|
|
// Put second key
|
|
Slice slice2 = CreateSlice(2048, 'B');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key2", slice2));
|
|
|
|
EXPECT_TRUE(cache.HasHotKey("key1"));
|
|
EXPECT_TRUE(cache.HasHotKey("key2"));
|
|
|
|
HotMemBlock* block1 = cache.GetHotKey("key1");
|
|
VerifySliceData(block1, 1024, 'A');
|
|
|
|
HotMemBlock* block2 = cache.GetHotKey("key2");
|
|
VerifySliceData(block2, 2048, 'B');
|
|
}
|
|
|
|
// Test PutHotKey with existing key (touch LRU)
|
|
TEST_F(LocalHotCacheTest, PutHotKeyTouchExisting) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 32MB = 2 blocks
|
|
LocalHotCache cache(cache_size);
|
|
|
|
Slice slice = CreateSlice(1024, 'X');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "test_key", slice));
|
|
|
|
// Put same key again (should just touch LRU, not copy data)
|
|
Slice slice2 = CreateSlice(1024, 'Y');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "test_key", slice2));
|
|
|
|
// Data should still be 'X' (not updated)
|
|
HotMemBlock* block = cache.GetHotKey("test_key");
|
|
VerifySliceData(block, 1024, 'X');
|
|
}
|
|
|
|
// Test PutHotKey with invalid parameters
|
|
TEST_F(LocalHotCacheTest, PutHotKeyInvalidParams) {
|
|
const size_t cache_size =
|
|
32 * 1024 * 1024; // 32MB = 2 blocks (default 16MB each)
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Test with null pointer
|
|
Slice null_slice;
|
|
null_slice.ptr = nullptr;
|
|
null_slice.size = 1024;
|
|
EXPECT_FALSE(PutHotKeyHelper(cache, "key", null_slice));
|
|
|
|
// Test with zero size
|
|
Slice zero_slice;
|
|
zero_slice.ptr = malloc(1024);
|
|
zero_slice.size = 0;
|
|
EXPECT_FALSE(PutHotKeyHelper(cache, "key", zero_slice));
|
|
free(zero_slice.ptr);
|
|
|
|
// Test with size larger than block size
|
|
Slice large_slice;
|
|
large_slice.ptr =
|
|
malloc(17 * 1024 * 1024); // 17MB > 16MB (default block size)
|
|
large_slice.size = 17 * 1024 * 1024;
|
|
EXPECT_FALSE(PutHotKeyHelper(cache, "key", large_slice));
|
|
free(large_slice.ptr);
|
|
}
|
|
|
|
// Test PutHotKey with invalid parameters (custom block size)
|
|
TEST_F(LocalHotCacheTest, PutHotKeyInvalidParamsWithCustomBlockSize) {
|
|
const size_t block_size = 4 * 1024 * 1024; // 4MB
|
|
const size_t cache_size = 8 * 1024 * 1024; // 8MB = 2 blocks (4MB each)
|
|
LocalHotCache cache(cache_size, block_size);
|
|
|
|
// Test with size larger than custom block size
|
|
Slice large_slice;
|
|
large_slice.ptr = malloc(5 * 1024 * 1024); // 5MB > 4MB (custom block size)
|
|
large_slice.size = 5 * 1024 * 1024;
|
|
EXPECT_FALSE(PutHotKeyHelper(cache, "key", large_slice));
|
|
free(large_slice.ptr);
|
|
}
|
|
|
|
// Test LRU eviction behavior
|
|
TEST_F(LocalHotCacheTest, LRUEviction) {
|
|
const size_t cache_size =
|
|
32 * 1024 * 1024; // 32MB = 2 blocks (default 16MB each)
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Fill cache with 2 blocks
|
|
Slice slice1 = CreateSlice(1024, 'A');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key1", slice1));
|
|
|
|
Slice slice2 = CreateSlice(1024, 'B');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key2", slice2));
|
|
|
|
// Add third key, should evict key1 (LRU)
|
|
Slice slice3 = CreateSlice(1024, 'C');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key3", slice3));
|
|
|
|
EXPECT_FALSE(cache.HasHotKey("key1")); // Should be evicted
|
|
EXPECT_TRUE(cache.HasHotKey("key2"));
|
|
EXPECT_TRUE(cache.HasHotKey("key3"));
|
|
}
|
|
|
|
// Test GetHotKey updates LRU
|
|
TEST_F(LocalHotCacheTest, GetHotKeyUpdatesLRU) {
|
|
const size_t cache_size =
|
|
32 * 1024 * 1024; // 32MB = 2 blocks (default 16MB each)
|
|
LocalHotCache cache(cache_size);
|
|
|
|
Slice slice1 = CreateSlice(1024, 'A');
|
|
PutHotKeyHelper(cache, "key1", slice1);
|
|
|
|
Slice slice2 = CreateSlice(1024, 'B');
|
|
PutHotKeyHelper(cache, "key2", slice2);
|
|
|
|
// Access key1, should move it to front
|
|
HotMemBlock* block1 = cache.GetHotKey("key1");
|
|
ASSERT_NE(block1, nullptr);
|
|
|
|
// Add third key, should evict key2 (not key1, since key1 was accessed)
|
|
Slice slice3 = CreateSlice(1024, 'C');
|
|
PutHotKeyHelper(cache, "key3", slice3);
|
|
|
|
EXPECT_TRUE(cache.HasHotKey("key1")); // Should still be there
|
|
EXPECT_FALSE(cache.HasHotKey("key2")); // Should be evicted
|
|
EXPECT_TRUE(cache.HasHotKey("key3"));
|
|
}
|
|
|
|
// Test GetHotKey with non-existent key
|
|
TEST_F(LocalHotCacheTest, GetHotKeyMiss) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 32MB = 2 blocks
|
|
LocalHotCache cache(cache_size);
|
|
|
|
HotMemBlock* block = cache.GetHotKey("non_existent_key");
|
|
EXPECT_EQ(block, nullptr);
|
|
}
|
|
|
|
// Test that GetHotKey marks block as in_use and prevents PutHotKey from
|
|
// reusing it
|
|
TEST_F(LocalHotCacheTest, GetHotKeyProtectsBlockFromReuse) {
|
|
// Create cache with only 1 block (16MB default block size)
|
|
const size_t cache_size = 16 * 1024 * 1024; // 16MB = 1 block
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Put first key
|
|
Slice slice1 = CreateSlice(1024, 'A');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key1", slice1));
|
|
EXPECT_TRUE(cache.HasHotKey("key1"));
|
|
|
|
// Get the block - this should mark it as in_use
|
|
HotMemBlock* block1 = cache.GetHotKey("key1");
|
|
ASSERT_NE(block1, nullptr);
|
|
VerifySliceData(block1, 1024, 'A');
|
|
|
|
// Try to put a second key - should fail because all blocks are in_use
|
|
Slice slice2 = CreateSlice(1024, 'B');
|
|
EXPECT_FALSE(PutHotKeyHelper(cache, "key2", slice2))
|
|
<< "PutHotKey should fail when all blocks are in_use";
|
|
EXPECT_FALSE(cache.HasHotKey("key2"));
|
|
|
|
// Release the block
|
|
cache.ReleaseHotKey("key1");
|
|
|
|
// Now PutHotKey should succeed
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key2", slice2));
|
|
EXPECT_TRUE(cache.HasHotKey("key2"));
|
|
|
|
// Verify key2 data
|
|
HotMemBlock* block2 = cache.GetHotKey("key2");
|
|
ASSERT_NE(block2, nullptr);
|
|
VerifySliceData(block2, 1024, 'B');
|
|
cache.ReleaseHotKey("key2");
|
|
|
|
// key1 should be evicted since we only have 1 block
|
|
EXPECT_FALSE(cache.HasHotKey("key1"));
|
|
}
|
|
|
|
// Token check and publish must be atomic: a generation bump that races after
|
|
// the token was captured must cancel the fill instead of resurrecting it.
|
|
TEST_F(LocalHotCacheTest, PutHotKeyWithTokenRejectsStaleFill) {
|
|
const size_t cache_size = 16 * 1024 * 1024; // 1 block
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Stale: token captured, then key generation bumped (as Remove would).
|
|
HotCachePutToken stale = cache.AcquirePutToken("k");
|
|
cache.BumpKeyGeneration("k");
|
|
HotMemBlock* b1 = cache.GetFreeBlock();
|
|
ASSERT_NE(b1, nullptr);
|
|
b1->key_ = "k";
|
|
b1->size = 1024;
|
|
EXPECT_FALSE(cache.PutHotKey(b1, stale));
|
|
EXPECT_FALSE(cache.HasHotKey("k"));
|
|
EXPECT_EQ(cache.GetCacheSize(), 1)
|
|
<< "Stale fill must be returned to the pool, not published";
|
|
|
|
// Valid: token still current -> published.
|
|
HotCachePutToken fresh = cache.AcquirePutToken("k");
|
|
HotMemBlock* b2 = cache.GetFreeBlock();
|
|
ASSERT_NE(b2, nullptr);
|
|
b2->key_ = "k";
|
|
b2->size = 1024;
|
|
EXPECT_TRUE(cache.PutHotKey(b2, fresh));
|
|
EXPECT_TRUE(cache.HasHotKey("k"));
|
|
}
|
|
|
|
// Test LocalHotCacheHandler basic functionality
|
|
TEST_F(LocalHotCacheTest, LocalHotCacheHandlerBasic) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 32MB = 2 blocks
|
|
auto cache = std::make_shared<LocalHotCache>(cache_size);
|
|
LocalHotCacheHandler handler(cache, 2, 1024);
|
|
|
|
const size_t slice_size = 1024;
|
|
std::vector<char> data(slice_size, 'Z');
|
|
Slice slice;
|
|
slice.ptr = data.data();
|
|
slice.size = slice_size;
|
|
|
|
// Submit task
|
|
EXPECT_TRUE(handler.SubmitPutTask("async_key", slice));
|
|
|
|
// Wait a bit for async processing
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
|
// Verify data was cached
|
|
EXPECT_TRUE(cache->HasHotKey("async_key"));
|
|
HotMemBlock* block = cache->GetHotKey("async_key");
|
|
VerifySliceData(block, slice_size, 'Z');
|
|
}
|
|
|
|
// Test LocalHotCacheHandler with null cache
|
|
TEST_F(LocalHotCacheTest, LocalHotCacheHandlerNullCache) {
|
|
LocalHotCacheHandler handler(nullptr, 2);
|
|
|
|
std::vector<char> data(1024, 'X');
|
|
Slice slice;
|
|
slice.ptr = data.data();
|
|
slice.size = 1024;
|
|
|
|
// Should return false since hot_cache_ is null
|
|
EXPECT_FALSE(handler.SubmitPutTask("key", slice));
|
|
}
|
|
|
|
// Regression test: a block recycled from the LRU must still accept objects up
|
|
// to the full block capacity, even if it previously held a smaller object.
|
|
// Before the fix, SubmitPutTask compared the new slice against block->size
|
|
// (the previous object's logical length, which GetFreeBlock does not reset)
|
|
// instead of the block capacity, so a recycled block permanently rejected any
|
|
// object larger than the one it last held.
|
|
TEST_F(LocalHotCacheTest, RecycledBlockAcceptsLargerObject) {
|
|
const size_t block_size = 64 * 1024; // 64KB
|
|
// Single-block cache, so the second Put must recycle the first block.
|
|
auto cache = std::make_shared<LocalHotCache>(block_size, block_size);
|
|
ASSERT_EQ(cache->GetCacheSize(), 1u);
|
|
ASSERT_EQ(cache->GetBlockSize(), block_size);
|
|
|
|
LocalHotCacheHandler handler(cache, /*num_worker_threads=*/1,
|
|
/*max_queue_capacity=*/16);
|
|
|
|
// Wait until an async Put has been published into the cache.
|
|
auto wait_for_key = [&](const std::string& key) {
|
|
for (int i = 0; i < 400; ++i) { // up to ~2s
|
|
if (cache->HasHotKey(key)) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
}
|
|
return cache->HasHotKey(key);
|
|
};
|
|
|
|
// 1) Cache a small object. The block's logical size shrinks to small_size.
|
|
const size_t small_size = 1024; // 1KB
|
|
Slice small_slice = CreateSlice(small_size, 'S');
|
|
ASSERT_TRUE(handler.SubmitPutTask("small_key", small_slice));
|
|
ASSERT_TRUE(wait_for_key("small_key"));
|
|
|
|
// 2) Cache a larger object that still fits the block. This forces
|
|
// GetFreeBlock() to recycle the block that held "small_key". Before the
|
|
// fix this returned false (large_size > stale block->size of 1KB).
|
|
const size_t large_size = 32 * 1024; // 32KB: > small_size, <= block_size
|
|
Slice large_slice = CreateSlice(large_size, 'L');
|
|
EXPECT_TRUE(handler.SubmitPutTask("large_key", large_slice));
|
|
ASSERT_TRUE(wait_for_key("large_key"));
|
|
|
|
// The larger object is fully cached with the correct length and data, and
|
|
// the evicted small object is gone.
|
|
HotMemBlock* block = cache->GetHotKey("large_key");
|
|
ASSERT_NE(block, nullptr);
|
|
VerifySliceData(block, large_size, 'L');
|
|
cache->ReleaseHotKey("large_key");
|
|
EXPECT_FALSE(cache->HasHotKey("small_key"));
|
|
}
|
|
|
|
// Test concurrent access to LocalHotCache
|
|
TEST_F(LocalHotCacheTest, ConcurrentAccess) {
|
|
const size_t cache_size = 128 * 1024 * 1024; // 128MB = 8 blocks
|
|
LocalHotCache cache(cache_size);
|
|
|
|
const int num_threads = 4;
|
|
const int keys_per_thread = 2; // Total: 8 keys, cache has 8 blocks
|
|
std::vector<std::thread> threads;
|
|
std::atomic<int> successful_puts(0);
|
|
std::atomic<int> successful_gets(0);
|
|
|
|
// Each thread puts and gets keys
|
|
for (int t = 0; t < num_threads; ++t) {
|
|
threads.emplace_back([&cache, t, &successful_puts, &successful_gets]() {
|
|
for (int i = 0; i < keys_per_thread; ++i) {
|
|
std::string key =
|
|
"thread_" + std::to_string(t) + "_key_" + std::to_string(i);
|
|
std::vector<char> data(1024, static_cast<char>('A' + t));
|
|
Slice slice;
|
|
slice.ptr = data.data();
|
|
slice.size = 1024;
|
|
|
|
// Put the key
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, key, slice));
|
|
successful_puts++;
|
|
|
|
// Get it back - should succeed since cache has enough capacity
|
|
HotMemBlock* block = cache.GetHotKey(key);
|
|
EXPECT_NE(block, nullptr) << "Block should not be nullptr with "
|
|
"sufficient cache capacity";
|
|
successful_gets++;
|
|
|
|
// Verify data integrity
|
|
const char* cached_data = static_cast<const char*>(block->addr);
|
|
EXPECT_EQ(cached_data[0], static_cast<char>('A' + t));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all threads
|
|
for (auto& thread : threads) {
|
|
thread.join();
|
|
}
|
|
|
|
// All puts and gets should succeed with sufficient capacity
|
|
EXPECT_EQ(successful_puts.load(), num_threads * keys_per_thread);
|
|
EXPECT_EQ(successful_gets.load(), num_threads * keys_per_thread);
|
|
}
|
|
|
|
/**
|
|
* Test InitLocalHotCache via Client::Create and IsHotCacheEnabled
|
|
* Note: InitLocalHotCache is a private function called by Client::Create
|
|
*/
|
|
|
|
// Test 1: Valid environment variable - hot cache should be enabled
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_ValidSize) {
|
|
// Save original env var
|
|
const char* original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "33554432",
|
|
1); // 32MB = 2 blocks (16MB each)
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
if (client_opt.has_value()) {
|
|
EXPECT_TRUE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 2);
|
|
}
|
|
|
|
// Restore original env var
|
|
if (original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 2: Invalid environment variable - InitLocalHotCache returns
|
|
// INVALID_PARAMS
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_InvalidEnvVar) {
|
|
// Save original env var
|
|
const char* original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "invalid", 1);
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
// Restore original env var
|
|
if (original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 3: Zero value - InitLocalHotCache returns INVALID_PARAMS
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_ZeroSize) {
|
|
// Save original env var
|
|
const char* original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "0", 1);
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
// Restore original env var
|
|
if (original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 4: Negative value - InitLocalHotCache returns INVALID_PARAMS
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_NegativeSize) {
|
|
// Save original env var
|
|
const char* original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "-1", 1);
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
if (original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 5: Size less than 1 block (default 16MB) - hot cache should not be
|
|
// enabled
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_LessThanOneBlock) {
|
|
// Save original env vars
|
|
const char* original_cache_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "8388608",
|
|
1); // 8MB < 16MB (default block size)
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
// If client creation succeeded, hot cache should be disabled
|
|
// (because 8MB < 16MB results in 0 blocks, causing InitLocalHotCache to
|
|
// reset hot_cache_)
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
// Restore original env vars
|
|
if (original_cache_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_cache_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 6: Size less than 1 block (custom 4MB) - hot cache should not be enabled
|
|
TEST_F(LocalHotCacheTest,
|
|
InitLocalHotCacheViaClientCreate_LessThanOneBlock_CustomBlockSize) {
|
|
// Save original env vars
|
|
const char* original_cache_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
const char* original_block_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", "4194304",
|
|
1); // 4MB custom block size
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "2097152",
|
|
1); // 2MB < 4MB (custom block size)
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
// If client creation succeeded, hot cache should be disabled
|
|
// (because 2MB < 4MB results in 0 blocks, causing InitLocalHotCache to
|
|
// reset hot_cache_)
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
// Restore original env vars
|
|
if (original_cache_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_cache_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
if (original_block_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", original_block_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test 7: No environment variable - hot cache should be disabled (valid case)
|
|
TEST_F(LocalHotCacheTest, InitLocalHotCacheViaClientCreate_NoEnvVar) {
|
|
// Save original env var
|
|
const char* original_env = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
if (client_opt.has_value()) {
|
|
EXPECT_FALSE(client_opt.value()->IsHotCacheEnabled());
|
|
EXPECT_EQ(client_opt.value()->GetLocalHotCacheBlockCount(), 0);
|
|
}
|
|
|
|
// Restore original env var
|
|
if (original_env) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_env, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test RedirectToHotCache and ProcessSlicesAsync indirectly
|
|
* through Client::Get and Client::BatchGet
|
|
*
|
|
* IMPORTANT: With a single client, data is stored locally, so:
|
|
* - ProcessSlicesAsync won't cache local transfers (transport_endpoint_ ==
|
|
* local_hostname_)
|
|
* - RedirectToHotCache won't find cached data (nothing was
|
|
* cached)
|
|
* - we need multiple clients (one Put, another Get from non-local) to test
|
|
* cache hit scenarios
|
|
*
|
|
* These tests verify the functions work correctly when hot cache is disabled,
|
|
* but cannot fully test cache hit scenarios with a single client setup.
|
|
*/
|
|
TEST_F(LocalHotCacheTest, GetWithHotCacheEnabled) {
|
|
// Save original env vars
|
|
const char* original_cache_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
const char* original_block_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
|
|
// Enable hot cache with custom block size (4MB)
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", "4194304", 1); // 4MB
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "8388608",
|
|
1); // 8MB = 2 blocks (4MB each)
|
|
|
|
// Get local IP address instead of using "localhost" to avoid hostname
|
|
// resolution issues
|
|
std::string local_ip = getLocalIpAddress();
|
|
std::string local_hostname =
|
|
local_ip + ":12345"; // Use a fixed port for testing
|
|
|
|
// Use in-proc master server via CreateTestClient helper
|
|
auto client_opt = CreateTestClient(local_hostname);
|
|
ASSERT_TRUE(client_opt.has_value())
|
|
<< "Failed to create client with in-proc master server";
|
|
|
|
auto client = client_opt.value();
|
|
|
|
// Verify hot cache is enabled
|
|
ASSERT_TRUE(client->IsHotCacheEnabled());
|
|
ASSERT_GT(client->GetLocalHotCacheBlockCount(), 0);
|
|
|
|
// Mount a segment to provide storage space
|
|
size_t segment_size = 64 * 1024 * 1024; // 64MB
|
|
void* segment_ptr = allocate_buffer_allocator_memory(segment_size);
|
|
ASSERT_NE(segment_ptr, nullptr) << "Failed to allocate segment memory";
|
|
|
|
auto mount_result = client->MountSegment(segment_ptr, segment_size);
|
|
if (!mount_result.has_value()) {
|
|
free_memory("", segment_ptr);
|
|
GTEST_SKIP() << "Failed to mount segment: "
|
|
<< toString(mount_result.error());
|
|
}
|
|
|
|
const std::string test_key = "test_hot_cache_key";
|
|
const std::string test_data = "Test data for hot cache";
|
|
|
|
// Put data first
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
// Allocate buffer for Put
|
|
std::vector<char> put_buffer(test_data.size());
|
|
std::memcpy(put_buffer.data(), test_data.data(), test_data.size());
|
|
std::vector<Slice> put_slices;
|
|
put_slices.emplace_back(Slice{put_buffer.data(), test_data.size()});
|
|
|
|
auto put_result = client->Put(test_key, put_slices, config);
|
|
if (!put_result.has_value()) {
|
|
client->UnmountSegment(segment_ptr, segment_size);
|
|
free_memory("", segment_ptr);
|
|
GTEST_SKIP() << "Put operation failed, skipping Get test";
|
|
}
|
|
|
|
std::vector<char> get_buffer(test_data.size());
|
|
std::vector<Slice> get_slices;
|
|
get_slices.emplace_back(Slice{get_buffer.data(), test_data.size()});
|
|
|
|
auto get_result = client->Get(test_key, get_slices);
|
|
ASSERT_TRUE(get_result.has_value()) << "Get should succeed";
|
|
|
|
// Verify data integrity
|
|
ASSERT_GE(get_slices.size(), 1) << "At least one slice should exist";
|
|
ASSERT_GE(get_slices[0].size, test_data.size())
|
|
<< "Slice size should be at least expected data size";
|
|
EXPECT_EQ(
|
|
std::memcmp(get_slices[0].ptr, test_data.data(), test_data.size()), 0)
|
|
<< "Retrieved data should match original data";
|
|
|
|
// Cleanup: unmount segment and free memory
|
|
client->UnmountSegment(segment_ptr, segment_size);
|
|
free_memory("", segment_ptr);
|
|
|
|
// Restore original env vars
|
|
if (original_cache_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_cache_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
if (original_block_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", original_block_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
}
|
|
}
|
|
|
|
TEST_F(LocalHotCacheTest, BatchGetWithHotCacheEnabled) {
|
|
// Save original env vars
|
|
const char* original_cache_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
const char* original_block_size =
|
|
std::getenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
|
|
// Enable hot cache with custom block size (4MB)
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", "4194304", 1); // 4MB
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "8388608",
|
|
1); // 8MB = 2 blocks (4MB each)
|
|
|
|
// Get local IP address instead of using "localhost" to avoid hostname
|
|
// resolution issues
|
|
std::string local_ip = getLocalIpAddress();
|
|
std::string local_hostname =
|
|
local_ip + ":12345"; // Use a fixed port for testing
|
|
|
|
// Use in-proc master server via CreateTestClient helper
|
|
auto client_opt = CreateTestClient(local_hostname);
|
|
ASSERT_TRUE(client_opt.has_value())
|
|
<< "Failed to create client with in-proc master server";
|
|
|
|
auto client = client_opt.value();
|
|
|
|
// Verify hot cache is enabled
|
|
ASSERT_TRUE(client->IsHotCacheEnabled());
|
|
|
|
// Mount a segment to provide storage space
|
|
size_t segment_size = 64 * 1024 * 1024; // 64MB
|
|
void* segment_ptr = allocate_buffer_allocator_memory(segment_size);
|
|
ASSERT_NE(segment_ptr, nullptr) << "Failed to allocate segment memory";
|
|
|
|
auto mount_result = client->MountSegment(segment_ptr, segment_size);
|
|
if (!mount_result.has_value()) {
|
|
free_memory("", segment_ptr);
|
|
GTEST_SKIP() << "Failed to mount segment: "
|
|
<< toString(mount_result.error());
|
|
}
|
|
|
|
const std::vector<std::string> test_keys = {"batch_key1", "batch_key2"};
|
|
const std::vector<std::string> test_data = {"Data1", "Data2"};
|
|
|
|
// Put data first
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
for (size_t i = 0; i < test_keys.size(); ++i) {
|
|
std::vector<char> put_buffer(test_data[i].size());
|
|
std::memcpy(put_buffer.data(), test_data[i].data(),
|
|
test_data[i].size());
|
|
std::vector<Slice> put_slices;
|
|
put_slices.emplace_back(Slice{put_buffer.data(), test_data[i].size()});
|
|
|
|
auto put_result = client->Put(test_keys[i], put_slices, config);
|
|
if (!put_result.has_value()) {
|
|
client->UnmountSegment(segment_ptr, segment_size);
|
|
free_memory("", segment_ptr);
|
|
GTEST_SKIP() << "Put operation failed, skipping BatchGet test";
|
|
}
|
|
}
|
|
|
|
std::unordered_map<std::string, std::vector<Slice>> batch_slices;
|
|
std::vector<std::vector<char>> batch_buffers; // Keep buffers alive
|
|
for (size_t i = 0; i < test_keys.size(); ++i) {
|
|
const auto& key = test_keys[i];
|
|
const auto& expected_data = test_data[i];
|
|
// Allocate buffer with the same size as the data we put
|
|
batch_buffers.emplace_back(expected_data.size());
|
|
batch_slices[key].emplace_back(
|
|
Slice{batch_buffers.back().data(), expected_data.size()});
|
|
}
|
|
|
|
// Use BatchGet without query_results parameter (it will query internally)
|
|
auto batch_get_result = client->BatchGet(test_keys, batch_slices);
|
|
ASSERT_EQ(batch_get_result.size(), test_keys.size());
|
|
for (const auto& result : batch_get_result) {
|
|
ASSERT_TRUE(result.has_value()) << "BatchGet should succeed";
|
|
}
|
|
|
|
// Verify data integrity for each key
|
|
for (size_t i = 0; i < test_keys.size(); ++i) {
|
|
const auto& key = test_keys[i];
|
|
const auto& expected_data = test_data[i];
|
|
|
|
auto slices_it = batch_slices.find(key);
|
|
ASSERT_NE(slices_it, batch_slices.end())
|
|
<< "Slices should exist for key: " << key;
|
|
ASSERT_GE(slices_it->second.size(), 1)
|
|
<< "At least one slice should exist for key: " << key;
|
|
|
|
// Verify the first slice contains the expected data
|
|
const auto& slice = slices_it->second[0];
|
|
ASSERT_GE(slice.size, expected_data.size())
|
|
<< "Slice size should be at least expected data size";
|
|
EXPECT_EQ(
|
|
std::memcmp(slice.ptr, expected_data.data(), expected_data.size()),
|
|
0)
|
|
<< "Data should match for key: " << key;
|
|
}
|
|
|
|
// Cleanup: unmount segment and free memory
|
|
client->UnmountSegment(segment_ptr, segment_size);
|
|
free_memory("", segment_ptr);
|
|
|
|
// Restore original env vars
|
|
if (original_cache_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", original_cache_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
}
|
|
if (original_block_size) {
|
|
setenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE", original_block_size, 1);
|
|
} else {
|
|
unsetenv("MC_STORE_LOCAL_HOT_BLOCK_SIZE");
|
|
}
|
|
}
|
|
|
|
// Test deferred LRU touch ordering: accessed blocks survive eviction
|
|
TEST_F(LocalHotCacheTest, DeferredLRUTouchOrdering) {
|
|
// 3-block cache (48MB / 16MB = 3 blocks)
|
|
const size_t cache_size = 48 * 1024 * 1024;
|
|
LocalHotCache cache(cache_size);
|
|
|
|
// Put 3 keys to fill the cache
|
|
Slice slice1 = CreateSlice(1024, 'A');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key1", slice1));
|
|
|
|
Slice slice2 = CreateSlice(1024, 'B');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key2", slice2));
|
|
|
|
Slice slice3 = CreateSlice(1024, 'C');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key3", slice3));
|
|
|
|
// Access key1 to set its accessed flag (deferred LRU touch)
|
|
HotMemBlock* blk = cache.GetHotKey("key1");
|
|
ASSERT_NE(blk, nullptr);
|
|
cache.ReleaseHotKey("key1");
|
|
|
|
// Insert key4, which triggers eviction. key1 was accessed so it should
|
|
// survive. key2 is LRU among unaccessed keys and should be evicted.
|
|
Slice slice4 = CreateSlice(1024, 'D');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "key4", slice4));
|
|
|
|
EXPECT_TRUE(cache.HasHotKey("key1")); // Survived (accessed)
|
|
EXPECT_FALSE(cache.HasHotKey("key2")); // Evicted (LRU, unaccessed)
|
|
EXPECT_TRUE(cache.HasHotKey("key3"));
|
|
EXPECT_TRUE(cache.HasHotKey("key4"));
|
|
}
|
|
|
|
// Test concurrent GetHotKey with shared lock (no crashes)
|
|
TEST_F(LocalHotCacheTest, ConcurrentGetHotKeySharedLock) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 2 blocks
|
|
LocalHotCache cache(cache_size);
|
|
|
|
Slice slice = CreateSlice(1024, 'X');
|
|
EXPECT_TRUE(PutHotKeyHelper(cache, "shared_key", slice));
|
|
|
|
const int num_threads = 8;
|
|
std::atomic<int> successful_gets(0);
|
|
std::vector<std::thread> threads;
|
|
|
|
for (int t = 0; t < num_threads; ++t) {
|
|
threads.emplace_back([&cache, &successful_gets]() {
|
|
for (int i = 0; i < 100; ++i) {
|
|
HotMemBlock* blk = cache.GetHotKey("shared_key");
|
|
if (blk) {
|
|
successful_gets++;
|
|
cache.ReleaseHotKey("shared_key");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& thread : threads) {
|
|
thread.join();
|
|
}
|
|
|
|
// All gets should succeed since we have enough capacity and key exists
|
|
EXPECT_EQ(successful_gets.load(), num_threads * 100);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HotMemBlock ref_count and BufferHandle view mode test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Test that GetHotKey + BufferHandle view mode ref_count lifecycle works
|
|
// correctly.
|
|
TEST_F(LocalHotCacheTest, HotCacheRefCountViewMode) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 2 blocks
|
|
auto cache = std::make_shared<LocalHotCache>(cache_size);
|
|
|
|
// Pre-fill a key
|
|
Slice slice = CreateSlice(1024, 'V');
|
|
EXPECT_TRUE(PutHotKeyHelper(*cache, "view_key", slice));
|
|
|
|
// GetHotKey increments ref_count, then we create a BufferHandle in view
|
|
// mode whose release_fn decrements ref_count.
|
|
HotMemBlock* blk = cache->GetHotKey("view_key");
|
|
ASSERT_NE(blk, nullptr);
|
|
EXPECT_EQ(blk->ref_count.load(), 1);
|
|
|
|
{
|
|
// Create a BufferHandle in view mode
|
|
auto handle = std::make_shared<BufferHandle>(
|
|
blk->addr, blk->size, [blk, cache]() {
|
|
blk->ref_count.fetch_sub(1, std::memory_order_release);
|
|
});
|
|
|
|
EXPECT_NE(handle->ptr(), nullptr);
|
|
EXPECT_EQ(handle->size(), blk->size);
|
|
|
|
// Verify data through the handle
|
|
const char* data = static_cast<const char*>(handle->ptr());
|
|
EXPECT_EQ(data[0], 'V');
|
|
|
|
// ref_count should still be 1 while handle is alive
|
|
EXPECT_EQ(blk->ref_count.load(), 1);
|
|
|
|
// Block should not be evictable (ref_count > 0): inserting a new key
|
|
// into a 1-block sub-cache should fail.
|
|
const size_t small_cache_size = 16 * 1024 * 1024; // 1 block
|
|
LocalHotCache small_cache(small_cache_size);
|
|
Slice s2 = CreateSlice(512, 'W');
|
|
EXPECT_TRUE(PutHotKeyHelper(small_cache, "sk1", s2));
|
|
|
|
// Get the block to hold a ref
|
|
HotMemBlock* held = small_cache.GetHotKey("sk1");
|
|
ASSERT_NE(held, nullptr);
|
|
|
|
// Now try inserting another key → all blocks in use
|
|
Slice s3 = CreateSlice(512, 'X');
|
|
EXPECT_FALSE(PutHotKeyHelper(small_cache, "sk2", s3));
|
|
|
|
small_cache.ReleaseHotKey("sk1");
|
|
}
|
|
|
|
// After handle destruction, release_fn should have decremented ref_count
|
|
EXPECT_EQ(blk->ref_count.load(), 0);
|
|
}
|
|
|
|
// Test GetFreeBlock → direct write → PutHotKey → GetHotKey → BufferHandle
|
|
// view mode (exercises the low-level cache block lifecycle).
|
|
TEST_F(LocalHotCacheTest, CacheBlockWriteAndRetrieve) {
|
|
const size_t cache_size = 32 * 1024 * 1024; // 2 blocks
|
|
auto cache = std::make_shared<LocalHotCache>(cache_size);
|
|
|
|
// Step 1: Simulate cache miss — get a free block
|
|
HotMemBlock* block = cache->GetFreeBlock();
|
|
ASSERT_NE(block, nullptr);
|
|
|
|
// Step 2: Write data directly into the block (simulating TransferRead
|
|
// writing into cache block)
|
|
const size_t data_size = 4096;
|
|
std::memset(block->addr, 'Z', data_size);
|
|
|
|
// Step 3: Set metadata and insert into LRU
|
|
block->key_ = "zc_write_key";
|
|
block->size = data_size;
|
|
EXPECT_TRUE(cache->PutHotKey(block));
|
|
|
|
// Step 4: Re-acquire ref via GetHotKey
|
|
HotMemBlock* blk = cache->GetHotKey("zc_write_key");
|
|
ASSERT_NE(blk, nullptr);
|
|
EXPECT_EQ(blk->ref_count.load(), 1);
|
|
EXPECT_EQ(blk->size, data_size);
|
|
|
|
// Step 5: Create BufferHandle in view mode
|
|
{
|
|
auto handle = std::make_shared<BufferHandle>(
|
|
blk->addr, blk->size, [blk, cache]() {
|
|
blk->ref_count.fetch_sub(1, std::memory_order_release);
|
|
});
|
|
|
|
EXPECT_NE(handle->ptr(), nullptr);
|
|
EXPECT_EQ(handle->size(), data_size);
|
|
|
|
// Verify data through the handle
|
|
const char* data = static_cast<const char*>(handle->ptr());
|
|
for (size_t i = 0; i < data_size; ++i) {
|
|
ASSERT_EQ(data[i], 'Z') << "Data mismatch at offset " << i;
|
|
}
|
|
|
|
// ref_count should still be 1 while handle is alive
|
|
EXPECT_EQ(blk->ref_count.load(), 1);
|
|
}
|
|
|
|
// After handle destruction, ref_count should be 0
|
|
EXPECT_EQ(blk->ref_count.load(), 0);
|
|
}
|
|
|
|
TEST_F(LocalHotCacheTest, AdmissionSketchNotIncrementedOnCacheHit) {
|
|
class EnvGuard {
|
|
public:
|
|
explicit EnvGuard(const char* key) : key_(key) {
|
|
if (const char* value = std::getenv(key_)) {
|
|
old_value_ = value;
|
|
}
|
|
}
|
|
|
|
~EnvGuard() {
|
|
if (old_value_.has_value()) {
|
|
setenv(key_, old_value_->c_str(), 1);
|
|
} else {
|
|
unsetenv(key_);
|
|
}
|
|
}
|
|
|
|
private:
|
|
const char* key_;
|
|
std::optional<std::string> old_value_;
|
|
};
|
|
|
|
EnvGuard cache_size_guard("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
EnvGuard memcpy_guard("MC_STORE_MEMCPY");
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", "33554432", 1); // 32MB
|
|
setenv("MC_STORE_MEMCPY", "1", 1);
|
|
|
|
auto client_opt = CreateTestClient("localhost");
|
|
ASSERT_TRUE(client_opt.has_value());
|
|
auto client = client_opt.value();
|
|
ASSERT_TRUE(client->IsHotCacheEnabled());
|
|
|
|
const std::string key = "admission_skip_on_cache_hit_key";
|
|
const std::string cached_data = "cache-hit-data";
|
|
|
|
// Pre-fill local hot cache entry.
|
|
Slice cache_slice{const_cast<char*>(cached_data.data()),
|
|
cached_data.size()};
|
|
ASSERT_TRUE(PutHotKeyHelper(*client->GetHotCache(), key, cache_slice));
|
|
ASSERT_TRUE(client->GetHotCache()->HasHotKey(key));
|
|
|
|
// Build a synthetic COMPLETE memory replica.
|
|
// RedirectToHotCache() should rewrite this descriptor to local hot cache.
|
|
Replica::Descriptor replica;
|
|
replica.id = 1;
|
|
replica.status = ReplicaStatus::COMPLETE;
|
|
MemoryDescriptor mem_desc;
|
|
mem_desc.buffer_descriptor.transport_endpoint_ = "remote:9999";
|
|
mem_desc.buffer_descriptor.buffer_address_ = 0;
|
|
mem_desc.buffer_descriptor.size_ = cached_data.size();
|
|
replica.descriptor_variant = mem_desc;
|
|
|
|
std::vector<Replica::Descriptor> replicas;
|
|
replicas.emplace_back(replica);
|
|
QueryResult query_result(
|
|
std::move(replicas),
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(60));
|
|
|
|
const uint8_t count_before = client->GetAdmissionCount(key);
|
|
EXPECT_EQ(count_before, 0);
|
|
|
|
auto do_cache_hit_get = [&]() {
|
|
std::vector<char> out(cached_data.size(), '\0');
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(Slice{out.data(), out.size()});
|
|
auto get_result = client->Get(key, query_result, slices);
|
|
EXPECT_TRUE(get_result.has_value());
|
|
EXPECT_EQ(
|
|
std::memcmp(out.data(), cached_data.data(), cached_data.size()), 0);
|
|
};
|
|
|
|
// Execute cache-hit Get path multiple times.
|
|
do_cache_hit_get();
|
|
do_cache_hit_get();
|
|
do_cache_hit_get();
|
|
|
|
// Admission sketch must not be incremented on cache hits.
|
|
EXPECT_EQ(client->GetAdmissionCount(key), count_before);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CountMinSketch basic tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LocalHotCacheTest, CountMinSketchBasic) {
|
|
CountMinSketch sketch(64, 4);
|
|
|
|
// First increment returns 1
|
|
EXPECT_EQ(sketch.increment("key_a"), 1);
|
|
// Second increment returns 2
|
|
EXPECT_EQ(sketch.increment("key_a"), 2);
|
|
// Third increment returns 3
|
|
EXPECT_EQ(sketch.increment("key_a"), 3);
|
|
|
|
// A different key starts at 1
|
|
EXPECT_EQ(sketch.increment("key_b"), 1);
|
|
|
|
// Read-only count matches
|
|
EXPECT_EQ(sketch.count("key_a"), 3);
|
|
EXPECT_EQ(sketch.count("key_b"), 1);
|
|
// Never-seen key has count 0
|
|
EXPECT_EQ(sketch.count("key_c"), 0);
|
|
|
|
// Decay halves all counters
|
|
sketch.decay();
|
|
EXPECT_EQ(sketch.count("key_a"), 1); // 3 >> 1 = 1
|
|
EXPECT_EQ(sketch.count("key_b"), 0); // 1 >> 1 = 0
|
|
}
|
|
|
|
TEST_F(LocalHotCacheTest, CountMinSketchAutoDecay) {
|
|
// Small sketch: width=8, depth=2 → auto-decay threshold = 16
|
|
CountMinSketch sketch(8, 2);
|
|
|
|
// Increment one key 15 times (below threshold)
|
|
for (int i = 0; i < 15; ++i) {
|
|
sketch.increment("hot_key");
|
|
}
|
|
EXPECT_EQ(sketch.count("hot_key"), 15);
|
|
|
|
// The 16th increment triggers auto-decay: increment returns the
|
|
// pre-decay count (16), but afterwards counters are halved.
|
|
uint8_t ret = sketch.increment("hot_key");
|
|
EXPECT_EQ(ret, 16);
|
|
EXPECT_EQ(sketch.count("hot_key"), 8); // 16 >> 1 = 8
|
|
}
|
|
|
|
TEST_F(LocalHotCacheTest, CountMinSketchZeroDimensions) {
|
|
CountMinSketch sketch(0, 0);
|
|
|
|
EXPECT_EQ(sketch.count("zero_key"), 0);
|
|
EXPECT_EQ(sketch.increment("zero_key"), 1);
|
|
EXPECT_EQ(sketch.count("zero_key"), 1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admission helpers when hot cache / sketch is disabled
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LocalHotCacheTest, AdmissionHelpersWithoutHotCache) {
|
|
// Create a client without hot cache (no MC_STORE_LOCAL_HOT_CACHE_SIZE)
|
|
const char* prev = std::getenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
unsetenv("MC_STORE_LOCAL_HOT_CACHE_SIZE");
|
|
|
|
auto result = CreateTestClient("no_hot_cache_host:9999");
|
|
ASSERT_TRUE(result.has_value());
|
|
auto client = result.value();
|
|
|
|
// Hot cache should be disabled
|
|
EXPECT_FALSE(client->IsHotCacheEnabled());
|
|
|
|
// GetAdmissionCount returns 0 when sketch is null
|
|
EXPECT_EQ(client->GetAdmissionCount("any_key"), 0);
|
|
|
|
// ShouldAdmitToHotCache returns false when hot_cache_ is null
|
|
EXPECT_FALSE(client->ShouldAdmitToHotCache("any_key", false));
|
|
EXPECT_FALSE(client->ShouldAdmitToHotCache("any_key", true));
|
|
|
|
// Restore env
|
|
if (prev) {
|
|
setenv("MC_STORE_LOCAL_HOT_CACHE_SIZE", prev, 1);
|
|
}
|
|
}
|
|
|
|
} // namespace testing
|
|
} // namespace mooncake
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|