#include <gflags/gflags.h>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "allocator.h"
#include "client.h"
#include "types.h"
#include "utils.h"
#include "test_server_helpers.h"
DEFINE_string(protocol, "tcp", "Transfer protocol: rdma|tcp");
DEFINE_string(device_name, "", "Device name to use, valid if protocol=rdma");
DEFINE_uint64(default_kv_lease_ttl, mooncake::DEFAULT_DEFAULT_KV_LEASE_TTL,
"Default lease time for kv objects, must be set to the "
"same as the master's default_kv_lease_ttl");
namespace mooncake {
namespace testing {
class ClientIntegrationTest : public ::testing::Test {
protected:
static std::shared_ptr<Client> CreateClient(const std::string& host_name) {
auto client_opt =
Client::Create(host_name,
"P2PHANDSHAKE",
FLAGS_protocol,
std::nullopt,
master_address_
);
EXPECT_TRUE(client_opt.has_value())
<< "Failed to create client with host_name: " << host_name;
if (!client_opt.has_value()) {
return nullptr;
}
return client_opt.value();
}
static void SetUpTestSuite() {
google::InitGoogleLogging("ClientIntegrationTest");
FLAGS_logtostderr = 1;
if (getenv("PROTOCOL")) FLAGS_protocol = getenv("PROTOCOL");
if (getenv("DEVICE_NAME")) FLAGS_device_name = getenv("DEVICE_NAME");
LOG(INFO) << "Protocol: " << FLAGS_protocol
<< ", Device name: " << FLAGS_device_name;
if (getenv("DEFAULT_KV_LEASE_TTL")) {
default_kv_lease_ttl_ = std::stoul(getenv("DEFAULT_KV_LEASE_TTL"));
} else {
default_kv_lease_ttl_ = FLAGS_default_kv_lease_ttl;
}
LOG(INFO) << "Default KV lease TTL: " << default_kv_lease_ttl_;
ASSERT_TRUE(master_.Start(InProcMasterConfigBuilder().build()));
master_address_ = master_.master_address();
metadata_url_ = master_.metadata_url();
LOG(INFO) << "Started in-proc master at " << master_address_
<< ", metadata=P2PHANDSHAKE";
InitializeClients();
InitializeSegment();
}
static void TearDownTestSuite() {
CleanupSegment();
CleanupClients();
master_.Stop();
google::ShutdownGoogleLogging();
}
static void InitializeSegment() {
ram_buffer_size_ = 512 * 1024 * 1024;
segment_ptr_ = allocate_buffer_allocator_memory(ram_buffer_size_);
LOG_ASSERT(segment_ptr_);
auto mount_result = segment_provider_client_->MountSegment(
segment_ptr_, ram_buffer_size_);
if (!mount_result.has_value()) {
LOG(ERROR) << "Failed to mount segment: "
<< toString(mount_result.error());
}
LOG(INFO) << "Segment mounted successfully";
}
static void InitializeClients() {
test_client_ = CreateClient("localhost:17813");
ASSERT_TRUE(test_client_ != nullptr);
segment_provider_client_ = CreateClient("localhost:17812");
ASSERT_TRUE(segment_provider_client_ != nullptr);
client_buffer_allocator_ =
std::make_unique<SimpleAllocator>(128 * 1024 * 1024);
auto register_result = test_client_->RegisterLocalMemory(
client_buffer_allocator_->getBase(), 128 * 1024 * 1024, "cpu:0",
false, false);
if (!register_result.has_value()) {
LOG(ERROR) << "Failed to register local memory: "
<< toString(register_result.error());
}
test_client_ram_buffer_size_ = 512 * 1024 * 1024;
test_client_segment_ptr_ =
allocate_buffer_allocator_memory(test_client_ram_buffer_size_);
LOG_ASSERT(test_client_segment_ptr_);
auto test_client_mount_result = test_client_->MountSegment(
test_client_segment_ptr_, test_client_ram_buffer_size_);
if (!test_client_mount_result.has_value()) {
LOG(ERROR) << "Failed to mount segment for test_client_: "
<< toString(test_client_mount_result.error());
}
LOG(INFO) << "Test client segment mounted successfully";
}
static void CleanupClients() {
if (test_client_ && test_client_segment_ptr_) {
if (!test_client_
->UnmountSegment(test_client_segment_ptr_,
test_client_ram_buffer_size_)
.has_value()) {
LOG(ERROR) << "Failed to unmount test client segment";
}
}
if (test_client_) {
test_client_.reset();
}
if (segment_provider_client_) {
segment_provider_client_.reset();
}
if (test_client_segment_ptr_) {
free(test_client_segment_ptr_);
}
if (segment_ptr_) {
free(segment_ptr_);
}
}
static void CleanupSegment() {
if (!segment_provider_client_
->UnmountSegment(segment_ptr_, ram_buffer_size_)
.has_value()) {
LOG(ERROR) << "Failed to unmount segment";
}
}
static std::shared_ptr<Client> test_client_;
static std::shared_ptr<Client> segment_provider_client_;
static std::unique_ptr<SimpleAllocator> client_buffer_allocator_;
static void* segment_ptr_;
static size_t ram_buffer_size_;
static void* test_client_segment_ptr_;
static size_t test_client_ram_buffer_size_;
static uint64_t default_kv_lease_ttl_;
static InProcMaster master_;
static std::string master_address_;
static std::string metadata_url_;
};
std::shared_ptr<Client> ClientIntegrationTest::test_client_ = nullptr;
std::shared_ptr<Client> ClientIntegrationTest::segment_provider_client_ =
nullptr;
void* ClientIntegrationTest::segment_ptr_ = nullptr;
void* ClientIntegrationTest::test_client_segment_ptr_ = nullptr;
std::unique_ptr<SimpleAllocator>
ClientIntegrationTest::client_buffer_allocator_ = nullptr;
size_t ClientIntegrationTest::ram_buffer_size_ = 0;
size_t ClientIntegrationTest::test_client_ram_buffer_size_ = 0;
uint64_t ClientIntegrationTest::default_kv_lease_ttl_ = 0;
InProcMaster ClientIntegrationTest::master_;
std::string ClientIntegrationTest::master_address_;
std::string ClientIntegrationTest::metadata_url_;
TEST_F(ClientIntegrationTest, BasicPutGetOperations) {
const std::string test_data = "Hello, World!";
const std::string key = "test_key";
void* buffer = client_buffer_allocator_->allocate(test_data.size());
memcpy(buffer, test_data.data(), test_data.size());
std::vector<Slice> slices;
slices.emplace_back(Slice{buffer, test_data.size()});
ReplicateConfig config;
config.replica_num = 1;
auto put_result = test_client_->Put(key, slices, config);
ASSERT_TRUE(put_result.has_value())
<< "Put operation failed: " << toString(put_result.error());
client_buffer_allocator_->deallocate(buffer, test_data.size());
buffer = client_buffer_allocator_->allocate(1 * 1024 * 1024);
slices.clear();
slices.emplace_back(Slice{buffer, test_data.size()});
auto get_result = test_client_->Get(key, slices);
ASSERT_TRUE(get_result.has_value())
<< "Get operation failed: " << toString(get_result.error());
ASSERT_EQ(slices.size(), 1);
ASSERT_EQ(slices[0].size, test_data.size());
ASSERT_EQ(slices[0].ptr, buffer);
ASSERT_EQ(memcmp(slices[0].ptr, test_data.data(), test_data.size()), 0);
client_buffer_allocator_->deallocate(buffer, test_data.size());
buffer = client_buffer_allocator_->allocate(test_data.size());
memcpy(buffer, test_data.data(), test_data.size());
slices.clear();
slices.emplace_back(Slice{buffer, test_data.size()});
auto put_result2 = test_client_->Put(key, slices, config);
ASSERT_TRUE(put_result2.has_value())
<< "Second Put operation failed: " << toString(put_result2.error());
std::this_thread::sleep_for(
std::chrono::milliseconds(default_kv_lease_ttl_));
auto remove_result = test_client_->Remove(key);
ASSERT_TRUE(remove_result.has_value())
<< "Remove operation failed: " << toString(remove_result.error());
client_buffer_allocator_->deallocate(buffer, test_data.size());
}
TEST_F(ClientIntegrationTest, RemoveOperation) {
const std::string test_data = "Test data for removal";
const std::string key = "remove_test_key";
void* buffer = client_buffer_allocator_->allocate(test_data.size());
memcpy(buffer, test_data.data(), test_data.size());
std::vector<Slice> slices;
slices.emplace_back(Slice{buffer, test_data.size()});
ReplicateConfig config;
config.replica_num = 1;
auto put_result = test_client_->Put(key, slices, config);
ASSERT_TRUE(put_result.has_value())
<< "Put operation failed: " << toString(put_result.error());
client_buffer_allocator_->deallocate(buffer, test_data.size());
auto remove_result = test_client_->Remove(key);
ASSERT_TRUE(remove_result.has_value())
<< "Remove operation failed: " << toString(remove_result.error());
auto query_result = test_client_->Query(key);
ASSERT_FALSE(query_result.has_value())
<< "Query should not find the removed key: " << key;
auto exist_result = test_client_->IsExist(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value())
<< "IsExist should return false for removed key: " << key;
buffer = client_buffer_allocator_->allocate(test_data.size());
slices.clear();
slices.emplace_back(Slice{buffer, test_data.size()});
auto get_result = test_client_->Get(key, slices);
ASSERT_FALSE(get_result.has_value()) << "Get should fail for removed key";
client_buffer_allocator_->deallocate(buffer, test_data.size());
}
TEST_F(ClientIntegrationTest, LocalPreferredAllocationTest) {
const std::string test_data = "Test data for local preferred allocation";
const std::string key = "local_preferred_test_key";
void* buffer = client_buffer_allocator_->allocate(test_data.size());
memcpy(buffer, test_data.data(), test_data.size());
std::vector<Slice> slices;
slices.emplace_back(Slice{buffer, test_data.size()});
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "localhost:17812";
auto put_result = test_client_->Put(key, slices, config);
ASSERT_TRUE(put_result.has_value())
<< "Put operation failed: " << toString(put_result.error());
client_buffer_allocator_->deallocate(buffer, test_data.size());
buffer = client_buffer_allocator_->allocate(test_data.size());
slices.clear();
slices.emplace_back(Slice{buffer, test_data.size()});
auto query_result = test_client_->Query(key);
ASSERT_TRUE(query_result.has_value())
<< "Query operation failed: " << toString(query_result.error());
auto replica_list = query_result.value().replicas;
ASSERT_EQ(replica_list.size(), 1);
ASSERT_EQ(replica_list[0].get_memory_descriptor().buffer_descriptors.size(),
1);
ASSERT_EQ(replica_list[0]
.get_memory_descriptor()
.buffer_descriptors[0]
.transport_endpoint_,
segment_provider_client_->GetTransportEndpoint());
auto get_result = test_client_->Get(key, query_result.value(), slices);
ASSERT_TRUE(get_result.has_value())
<< "Get operation failed: " << toString(get_result.error());
ASSERT_EQ(slices.size(), 1);
ASSERT_EQ(slices[0].size, test_data.size());
ASSERT_EQ(memcmp(slices[0].ptr, test_data.data(), test_data.size()), 0);
client_buffer_allocator_->deallocate(buffer, test_data.size());
std::this_thread::sleep_for(
std::chrono::milliseconds(default_kv_lease_ttl_));
auto remove_result2 = test_client_->Remove(key);
ASSERT_TRUE(remove_result2.has_value())
<< "Remove operation failed: " << toString(remove_result2.error());
}
TEST_F(ClientIntegrationTest, DISABLED_AllocateTest) {
const size_t data_size = 1 * 1024 * 1024;
std::string large_data(data_size, 'A');
const int num_operations = 13;
ReplicateConfig config;
config.replica_num = 1;
for (int i = 0; i < num_operations; i++) {
std::string key = "heavy_test_key_" + std::to_string(i);
void* buffer = client_buffer_allocator_->allocate(data_size);
ASSERT_TRUE(buffer);
memcpy(buffer, large_data.data(), data_size);
std::vector<Slice> put_slices;
put_slices.emplace_back(Slice{buffer, data_size});
auto put_result = test_client_->Put(key, put_slices, config);
if (!put_result.has_value()) break;
client_buffer_allocator_->deallocate(buffer, data_size);
buffer = client_buffer_allocator_->allocate(data_size);
std::vector<Slice> get_slices;
get_slices.emplace_back(Slice{buffer, data_size});
auto get_result = test_client_->Get(key, get_slices);
ASSERT_TRUE(get_result.has_value())
<< "Get operation failed: " << toString(get_result.error());
ASSERT_EQ(get_slices[0].size, data_size);
std::string retrieved_data(static_cast<const char*>(get_slices[0].ptr),
get_slices[0].size);
EXPECT_EQ(retrieved_data, large_data);
client_buffer_allocator_->deallocate(buffer, data_size);
}
std::string allocate_failed_key = "heavy_test_failed_key";
void* failed_buffer = client_buffer_allocator_->allocate(data_size);
std::vector<Slice> failed_slices;
failed_slices.emplace_back(Slice{failed_buffer, data_size});
memcpy(failed_buffer, large_data.data(), data_size);
auto failed_put_result =
test_client_->Put(allocate_failed_key, failed_slices, config);
ASSERT_FALSE(failed_put_result.has_value())
<< "Put operation should have failed";
client_buffer_allocator_->deallocate(failed_buffer, data_size);
std::this_thread::sleep_for(std::chrono::seconds(2));
void* success_buffer = client_buffer_allocator_->allocate(data_size);
std::vector<Slice> success_slices;
success_slices.emplace_back(Slice{success_buffer, data_size});
memcpy(success_buffer, large_data.data(), data_size);
auto success_put_result =
test_client_->Put(allocate_failed_key, success_slices, config);
ASSERT_TRUE(success_put_result.has_value())
<< "Put operation failed: " << toString(success_put_result.error());
client_buffer_allocator_->deallocate(success_buffer, data_size);
auto success_remove_result = test_client_->Remove(allocate_failed_key);
ASSERT_TRUE(success_remove_result.has_value())
<< "Remove operation failed: "
<< toString(success_remove_result.error());
}
TEST_F(ClientIntegrationTest, LargeAllocateTest) {
const size_t data_size = 1 * 1024 * 1024;
const uint64_t kNumBuffers = 5;
const std::string key = "large_test_key";
ReplicateConfig config;
config.replica_num = 1;
std::vector<void*> buffers(kNumBuffers);
for (size_t i = 0; i < kNumBuffers; ++i) {
buffers[i] = client_buffer_allocator_->allocate(data_size);
ASSERT_NE(buffers[i], nullptr);
std::string large_data(data_size, 'A' + i);
memcpy(buffers[i], large_data.data(), data_size);
}
std::vector<Slice> slices;
for (size_t i = 0; i < kNumBuffers; ++i) {
slices.emplace_back(Slice{buffers[i], data_size});
}
auto put_result = test_client_->Put(key, slices, config);
ASSERT_TRUE(put_result.has_value())
<< "Put operation failed: " << toString(put_result.error());
for (size_t i = 0; i < kNumBuffers; ++i) {
memset(buffers[i], 0, data_size);
}
auto get_result = test_client_->Get(key, slices);
ASSERT_TRUE(get_result.has_value())
<< "Get operation failed: " << toString(get_result.error());
for (size_t i = 0; i < kNumBuffers; ++i) {
ASSERT_EQ(slices[i].size, data_size);
std::string retrieved_data(static_cast<const char*>(slices[i].ptr),
slices[i].size);
std::string expected_data(data_size, 'A' + i);
EXPECT_EQ(
memcmp(retrieved_data.data(), expected_data.data(), data_size), 0);
client_buffer_allocator_->deallocate(buffers[i], data_size);
}
std::this_thread::sleep_for(
std::chrono::milliseconds(default_kv_lease_ttl_));
auto remove_result = test_client_->Remove(key);
ASSERT_TRUE(remove_result.has_value())
<< "Remove operation failed: " << toString(remove_result.error());
}
TEST_F(ClientIntegrationTest, BatchPutGetOperations) {
int batch_sz = 100;
std::vector<std::string> keys;
std::vector<std::string> test_data_list;
std::vector<std::vector<Slice>> batched_slices;
for (int i = 0; i < batch_sz; i++) {
keys.push_back("test_key_batch_put_" + std::to_string(i));
test_data_list.push_back("test_data_" + std::to_string(i));
}
void* buffer = nullptr;
void* target_buffer = nullptr;
batched_slices.reserve(batch_sz);
for (int i = 0; i < batch_sz; i++) {
std::vector<Slice> slices;
buffer = client_buffer_allocator_->allocate(test_data_list[i].size());
memcpy(buffer, test_data_list[i].data(), test_data_list[i].size());
slices.emplace_back(Slice{buffer, test_data_list[i].size()});
batched_slices.push_back(std::move(slices));
}
ReplicateConfig config;
config.replica_num = 1;
auto start = std::chrono::high_resolution_clock::now();
auto batch_put_results =
test_client_->BatchPut(keys, batched_slices, config);
for (const auto& result : batch_put_results) {
ASSERT_TRUE(result.has_value()) << "BatchPut operation failed";
}
auto end = std::chrono::high_resolution_clock::now();
LOG(INFO) << "Time taken for BatchPut: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end -
start)
.count()
<< "us";
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < batch_sz; i++) {
std::vector<Slice> slices;
target_buffer =
client_buffer_allocator_->allocate(test_data_list[i].size());
slices.emplace_back(Slice{target_buffer, test_data_list[i].size()});
auto get_result = test_client_->Get(keys[i], slices);
ASSERT_TRUE(get_result.has_value())
<< "Get operation failed: " << toString(get_result.error());
client_buffer_allocator_->deallocate(target_buffer,
test_data_list[i].size());
}
end = std::chrono::high_resolution_clock::now();
LOG(INFO) << "Time taken for single Get: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end -
start)
.count()
<< "us";
start = std::chrono::high_resolution_clock::now();
std::unordered_map<std::string, std::vector<Slice>> target_batched_slices;
for (int i = 0; i < batch_sz; i++) {
std::vector<Slice> target_slices;
target_buffer =
client_buffer_allocator_->allocate(test_data_list[i].size());
target_slices.emplace_back(
Slice{target_buffer, test_data_list[i].size()});
target_batched_slices.emplace(keys[i], target_slices);
}
auto batch_get_results =
test_client_->BatchGet(keys, target_batched_slices);
for (const auto& result : batch_get_results) {
ASSERT_TRUE(result.has_value()) << "BatchGet operation failed";
}
end = std::chrono::high_resolution_clock::now();
LOG(INFO) << "Time taken for BatchGet: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end -
start)
.count()
<< "us";
for (int i = 0; i < batch_sz; i++) {
ASSERT_EQ(target_batched_slices[keys[i]][0].size,
test_data_list[i].size());
ASSERT_EQ(memcmp(target_batched_slices[keys[i]][0].ptr,
test_data_list[i].data(), test_data_list[i].size()),
0);
client_buffer_allocator_->deallocate(
target_batched_slices[keys[i]][0].ptr, test_data_list[i].size());
}
}
TEST_F(ClientIntegrationTest, BatchIsExistOperations) {
int batch_size = 50;
std::vector<std::string> keys;
std::vector<std::string> test_data_list;
std::vector<std::vector<Slice>> batched_slices;
for (int i = 0; i < batch_size; i++) {
keys.push_back("test_key_batch_exist_" + std::to_string(i));
test_data_list.push_back("test_data_" + std::to_string(i));
}
void* buffer = nullptr;
batched_slices.reserve(batch_size / 2);
for (int i = 0; i < batch_size / 2; i++) {
std::vector<Slice> slices;
buffer = client_buffer_allocator_->allocate(test_data_list[i].size());
memcpy(buffer, test_data_list[i].data(), test_data_list[i].size());
slices.emplace_back(Slice{buffer, test_data_list[i].size()});
batched_slices.push_back(std::move(slices));
}
ReplicateConfig config;
config.replica_num = 1;
std::vector<std::string> existing_keys(keys.begin(),
keys.begin() + batch_size / 2);
auto batch_put_results =
test_client_->BatchPut(existing_keys, batched_slices, config);
for (const auto& result : batch_put_results) {
ASSERT_TRUE(result.has_value()) << "BatchPut operation failed";
}
auto exist_results = test_client_->BatchIsExist(keys);
ASSERT_EQ(keys.size(), exist_results.size());
for (int i = 0; i < batch_size / 2; i++) {
ASSERT_TRUE(exist_results[i].has_value())
<< "BatchIsExist failed for key " << keys[i];
ASSERT_TRUE(exist_results[i].value())
<< "Key " << keys[i] << " should exist";
}
for (int i = batch_size / 2; i < batch_size; i++) {
ASSERT_TRUE(exist_results[i].has_value())
<< "BatchIsExist failed for key " << keys[i];
ASSERT_FALSE(exist_results[i].value())
<< "Key " << keys[i] << " should not exist";
}
std::vector<std::string> empty_keys;
auto empty_results = test_client_->BatchIsExist(empty_keys);
ASSERT_EQ(empty_results.size(), 0);
for (int i = 0; i < batch_size / 2; i++) {
client_buffer_allocator_->deallocate(batched_slices[i][0].ptr,
test_data_list[i].size());
}
std::this_thread::sleep_for(
std::chrono::milliseconds(default_kv_lease_ttl_));
for (int i = 0; i < batch_size / 2; i++) {
auto remove_result = test_client_->Remove(keys[i]);
ASSERT_TRUE(remove_result.has_value())
<< "Remove operation failed: " << toString(remove_result.error());
}
}
TEST_F(ClientIntegrationTest, BatchPutDuplicateKeys) {
const std::string test_data = "test_data_duplicate";
const std::string key = "duplicate_key";
std::vector<std::string> keys = {key, key};
std::vector<std::vector<Slice>> batched_slices;
for (int i = 0; i < 2; i++) {
std::vector<Slice> slices;
void* buffer = client_buffer_allocator_->allocate(test_data.size());
memcpy(buffer, test_data.data(), test_data.size());
slices.emplace_back(Slice{buffer, test_data.size()});
batched_slices.push_back(std::move(slices));
}
ReplicateConfig config;
config.replica_num = 1;
auto batch_put_results =
test_client_->BatchPut(keys, batched_slices, config);
ASSERT_EQ(batch_put_results.size(), 2);
for (const auto& result : batch_put_results) {
ASSERT_TRUE(result.has_value())
<< "BatchPut operation failed: " << toString(result.error());
}
for (const auto& slices : batched_slices) {
for (const auto& slice : slices) {
client_buffer_allocator_->deallocate(slice.ptr, slice.size);
}
}
std::this_thread::sleep_for(
std::chrono::milliseconds(default_kv_lease_ttl_));
auto remove_result = test_client_->Remove(key);
ASSERT_TRUE(remove_result);
}
}
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
gflags::ParseCommandLineFlags(&argc, &argv, false);
easylog::set_min_severity(easylog::Severity::WARNING);
return RUN_ALL_TESTS();
}