forked from mooncake-track/Mooncake
1751 lines
69 KiB
C++
1751 lines
69 KiB
C++
#include <gflags/gflags.h>
|
||
#include <glog/logging.h>
|
||
#include <gtest/gtest.h>
|
||
|
||
#include <cstdint>
|
||
#include <filesystem>
|
||
#include <memory>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <regex>
|
||
#include <unordered_set>
|
||
#include <unordered_map>
|
||
#include <thread>
|
||
#include <chrono>
|
||
|
||
#include "allocator.h"
|
||
#include "client_service.h"
|
||
#include "types.h"
|
||
#include "utils.h"
|
||
#include "test_server_helpers.h"
|
||
#include "default_config.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 {
|
||
|
||
// Helper functions for client_id parsing
|
||
std::string FormatClientId(const UUID& client_id) {
|
||
return std::to_string(client_id.first) + "-" +
|
||
std::to_string(client_id.second);
|
||
}
|
||
|
||
UUID ParseClientId(const std::string& client_id_str) {
|
||
UUID client_id{0, 0};
|
||
size_t dash_pos = client_id_str.find('-');
|
||
if (dash_pos != std::string::npos) {
|
||
try {
|
||
client_id.first = std::stoull(client_id_str.substr(0, dash_pos));
|
||
client_id.second = std::stoull(client_id_str.substr(dash_pos + 1));
|
||
} catch (const std::exception& e) {
|
||
LOG(ERROR) << "Failed to parse client_id: " << e.what();
|
||
}
|
||
} else {
|
||
LOG(ERROR) << "Invalid client_id format. Expected format: first-second";
|
||
}
|
||
return client_id;
|
||
}
|
||
|
||
class ClientIdCaptureSink : public google::LogSink {
|
||
public:
|
||
std::string captured_client_id;
|
||
|
||
void send(google::LogSeverity severity, const char* full_filename,
|
||
const char* base_filename, int line, const struct ::tm* tm_time,
|
||
const char* message, size_t message_len) override {
|
||
(void)severity;
|
||
(void)full_filename;
|
||
(void)base_filename;
|
||
(void)line;
|
||
(void)tm_time;
|
||
|
||
std::string msg(message, message_len);
|
||
|
||
size_t pos = msg.find("client_id=");
|
||
if (pos != std::string::npos) {
|
||
std::string client_id_str = msg.substr(pos + 10);
|
||
client_id_str.erase(0, client_id_str.find_first_not_of(" \t\n\r"));
|
||
client_id_str.erase(client_id_str.find_last_not_of(" \t\n\r") + 1);
|
||
|
||
std::regex uuid_pattern(R"((\d+)-(\d+))");
|
||
std::smatch match;
|
||
if (std::regex_search(client_id_str, match, uuid_pattern)) {
|
||
captured_client_id = match[0].str();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
class ClientIntegrationTest : public ::testing::Test {
|
||
protected:
|
||
static std::shared_ptr<Client> CreateClient(const std::string& host_name) {
|
||
auto client_opt =
|
||
Client::Create(host_name, // Local hostname
|
||
"P2PHANDSHAKE", // Metadata connection string
|
||
FLAGS_protocol, // Transfer protocol
|
||
std::nullopt, // RDMA device names (auto-discovery)
|
||
master_address_ // Master server address (non-HA)
|
||
);
|
||
|
||
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() {
|
||
// Initialize glog
|
||
google::InitGoogleLogging("ClientIntegrationTest");
|
||
|
||
FLAGS_logtostderr = 1;
|
||
|
||
// Override flags from environment variables if present
|
||
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_;
|
||
|
||
// Start an in-process non-HA master without HTTP metadata server
|
||
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; // 512 MB
|
||
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_, FLAGS_protocol);
|
||
if (!mount_result.has_value()) {
|
||
LOG(ERROR) << "Failed to mount segment: "
|
||
<< toString(mount_result.error());
|
||
}
|
||
LOG(INFO) << "Segment mounted successfully";
|
||
}
|
||
|
||
static void InitializeClients() {
|
||
// This client is used for testing purposes.
|
||
// Capture test_client_ client_id from logs
|
||
ClientIdCaptureSink* test_client_sink = new ClientIdCaptureSink();
|
||
google::AddLogSink(test_client_sink);
|
||
|
||
test_client_ = CreateClient("localhost:17813");
|
||
ASSERT_TRUE(test_client_ != nullptr);
|
||
|
||
// Wait for logs to flush
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||
google::RemoveLogSink(test_client_sink);
|
||
|
||
if (!test_client_sink->captured_client_id.empty()) {
|
||
UUID extracted_id =
|
||
ParseClientId(test_client_sink->captured_client_id);
|
||
if (extracted_id.first != 0 || extracted_id.second != 0) {
|
||
test_client_id_ = extracted_id;
|
||
LOG(INFO) << "Captured test_client_id: "
|
||
<< FormatClientId(test_client_id_);
|
||
}
|
||
}
|
||
delete test_client_sink;
|
||
|
||
// This client is used to provide segments.
|
||
// Capture segment_provider_client_ client_id from logs
|
||
ClientIdCaptureSink* provider_client_sink = new ClientIdCaptureSink();
|
||
google::AddLogSink(provider_client_sink);
|
||
|
||
segment_provider_client_ = CreateClient("localhost:17812");
|
||
ASSERT_TRUE(segment_provider_client_ != nullptr);
|
||
|
||
// Wait for logs to flush
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||
google::RemoveLogSink(provider_client_sink);
|
||
|
||
if (!provider_client_sink->captured_client_id.empty()) {
|
||
UUID extracted_id =
|
||
ParseClientId(provider_client_sink->captured_client_id);
|
||
if (extracted_id.first != 0 || extracted_id.second != 0) {
|
||
segment_provider_client_id_ = extracted_id;
|
||
LOG(INFO) << "Captured segment_provider_client_id: "
|
||
<< FormatClientId(segment_provider_client_id_);
|
||
}
|
||
}
|
||
delete provider_client_sink;
|
||
|
||
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());
|
||
}
|
||
|
||
// Mount segment for test_client_ as well
|
||
test_client_ram_buffer_size_ = 512 * 1024 * 1024; // 512 MB
|
||
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_,
|
||
FLAGS_protocol);
|
||
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() {
|
||
// Unmount test client segment first
|
||
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();
|
||
}
|
||
|
||
// Free segment memory
|
||
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_;
|
||
// Here we use a simple allocator for the client buffer. In a real
|
||
// application, user should manage the memory allocation and deallocation
|
||
// themselves.
|
||
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_;
|
||
static UUID test_client_id_;
|
||
static UUID segment_provider_client_id_;
|
||
};
|
||
|
||
// Static members initialization
|
||
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_;
|
||
UUID ClientIntegrationTest::test_client_id_{0, 0};
|
||
UUID ClientIntegrationTest::segment_provider_client_id_{0, 0};
|
||
|
||
// Test basic Put/Get operations through the client
|
||
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());
|
||
|
||
// write
|
||
memcpy(buffer, test_data.data(), test_data.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buffer, test_data.size()});
|
||
|
||
// Test Put operation
|
||
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()});
|
||
// Verify data through Get operation
|
||
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());
|
||
|
||
// Put again with the same key, should succeed
|
||
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 Remove operation
|
||
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());
|
||
|
||
// Put data first
|
||
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());
|
||
|
||
// Remove the data
|
||
auto remove_result = test_client_->Remove(key);
|
||
ASSERT_TRUE(remove_result.has_value())
|
||
<< "Remove operation failed: " << toString(remove_result.error());
|
||
|
||
// Verify that the data is removed using Query operation
|
||
auto query_result = test_client_->Query(key);
|
||
ASSERT_FALSE(query_result.has_value())
|
||
<< "Query should not find the removed key: " << key;
|
||
|
||
// Check if the key exists using IsExist
|
||
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;
|
||
|
||
// Try to get the removed data - should fail
|
||
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 local preferred allocation strategy
|
||
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());
|
||
|
||
// Put data with preferred segment set to local hostname
|
||
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;
|
||
// Although there is only one segment now, in order to test the preferred
|
||
// allocation logic, we still set it. This will prevent potential
|
||
// compatibility issues in the future.
|
||
config.preferred_segment = "localhost:17812"; // Local segment
|
||
|
||
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());
|
||
|
||
// Verify data through Get operation
|
||
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_descriptor.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());
|
||
|
||
// Clean up
|
||
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 heavy workload operations
|
||
TEST_F(ClientIntegrationTest, DISABLED_AllocateTest) {
|
||
const size_t data_size = 1 * 1024 * 1024; // 1MB
|
||
std::string large_data(data_size, 'A'); // Fill with 'A's
|
||
const int num_operations = 13;
|
||
|
||
// Configure with 1 replicas for high availability
|
||
ReplicateConfig config;
|
||
config.replica_num = 1;
|
||
|
||
// Perform multiple Put/Get operations
|
||
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);
|
||
|
||
// Put operation with large data
|
||
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);
|
||
// Get and verify data
|
||
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);
|
||
|
||
// sleep for 2 seconds to ensure the object is marked for GC
|
||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||
|
||
// After removing all keys, we should be able to allocate the failed key
|
||
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 large allocation operations
|
||
TEST_F(ClientIntegrationTest, LargeAllocateTest) {
|
||
const size_t data_size = 1 * 1024 * 1024; // 1MB
|
||
const uint64_t kNumBuffers = 5;
|
||
const std::string key = "large_test_key";
|
||
|
||
// Configure with 1 replicas for high availability
|
||
ReplicateConfig config;
|
||
config.replica_num = 1;
|
||
|
||
// Allocate buffers and fill with data
|
||
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);
|
||
}
|
||
|
||
// Create slices from buffers
|
||
std::vector<Slice> slices;
|
||
for (size_t i = 0; i < kNumBuffers; ++i) {
|
||
slices.emplace_back(Slice{buffers[i], data_size});
|
||
}
|
||
|
||
// Put operation
|
||
auto put_result = test_client_->Put(key, slices, config);
|
||
ASSERT_TRUE(put_result.has_value())
|
||
<< "Put operation failed: " << toString(put_result.error());
|
||
|
||
// Clear buffers before Get
|
||
for (size_t i = 0; i < kNumBuffers; ++i) {
|
||
memset(buffers[i], 0, data_size);
|
||
}
|
||
|
||
// Get operation
|
||
auto get_result = test_client_->Get(key, slices);
|
||
ASSERT_TRUE(get_result.has_value())
|
||
<< "Get operation failed: " << toString(get_result.error());
|
||
|
||
// Verify data and deallocate buffers
|
||
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);
|
||
}
|
||
|
||
// Remove the key
|
||
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 batch Put/Get operations through the client
|
||
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));
|
||
}
|
||
// Test Batch Put operation
|
||
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);
|
||
// Check that all operations succeeded
|
||
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 batch IsExist operations through the client
|
||
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;
|
||
|
||
// Create test keys and data
|
||
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));
|
||
}
|
||
|
||
// Put only the first half of the keys
|
||
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;
|
||
|
||
// Put the first half of keys
|
||
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);
|
||
// Check that all operations succeeded
|
||
for (const auto& result : batch_put_results) {
|
||
ASSERT_TRUE(result.has_value()) << "BatchPut operation failed";
|
||
}
|
||
|
||
// Test BatchIsExist with mixed existing and non-existing keys
|
||
auto exist_results = test_client_->BatchIsExist(keys);
|
||
|
||
// Verify results
|
||
ASSERT_EQ(keys.size(), exist_results.size());
|
||
|
||
// First half should exist
|
||
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";
|
||
}
|
||
|
||
// Second half should not 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";
|
||
}
|
||
|
||
// Test with empty keys vector
|
||
std::vector<std::string> empty_keys;
|
||
auto empty_results = test_client_->BatchIsExist(empty_keys);
|
||
ASSERT_EQ(empty_results.size(), 0);
|
||
|
||
// Clean up
|
||
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 batch QueryIp operations through the client
|
||
TEST_F(ClientIntegrationTest, BatchQueryIpOperations) {
|
||
// Skip test if we couldn't capture client_ids
|
||
if ((test_client_id_.first == 0 && test_client_id_.second == 0) ||
|
||
(segment_provider_client_id_.first == 0 &&
|
||
segment_provider_client_id_.second == 0)) {
|
||
GTEST_SKIP()
|
||
<< "Could not capture client_ids, skipping BatchQueryIp test";
|
||
}
|
||
|
||
// Test 1: Query IP for test_client_
|
||
std::vector<UUID> client_ids = {test_client_id_};
|
||
auto result = test_client_->BatchQueryIp(client_ids);
|
||
|
||
ASSERT_TRUE(result.has_value())
|
||
<< "BatchQueryIp failed: " << toString(result.error());
|
||
|
||
const auto& results = result.value();
|
||
ASSERT_FALSE(results.empty()) << "BatchQueryIp returned empty results";
|
||
|
||
auto it = results.find(test_client_id_);
|
||
ASSERT_NE(it, results.end()) << "test_client_id not found in results";
|
||
|
||
const auto& ip_addresses = it->second;
|
||
ASSERT_FALSE(ip_addresses.empty())
|
||
<< "test_client_ should have at least one IP address";
|
||
|
||
LOG(INFO) << "test_client_ IP addresses (" << ip_addresses.size() << "):";
|
||
for (size_t i = 0; i < ip_addresses.size(); ++i) {
|
||
LOG(INFO) << " [" << (i + 1) << "] " << ip_addresses[i];
|
||
}
|
||
|
||
// Verify IP addresses are valid (should contain "127.0.0.1" or "localhost")
|
||
bool has_valid_ip = false;
|
||
for (const auto& ip : ip_addresses) {
|
||
if (ip == "127.0.0.1" || ip.find("127.0.0.1") != std::string::npos ||
|
||
ip == "localhost" || ip.find("localhost") != std::string::npos) {
|
||
has_valid_ip = true;
|
||
break;
|
||
}
|
||
}
|
||
EXPECT_TRUE(has_valid_ip) << "Expected at least one valid IP address";
|
||
|
||
// Test 2: Query IP for multiple client_ids
|
||
std::vector<UUID> multiple_ids = {test_client_id_,
|
||
segment_provider_client_id_};
|
||
auto multi_result = test_client_->BatchQueryIp(multiple_ids);
|
||
|
||
ASSERT_TRUE(multi_result.has_value())
|
||
<< "BatchQueryIp failed for multiple client_ids: "
|
||
<< toString(multi_result.error());
|
||
|
||
const auto& multi_results = multi_result.value();
|
||
|
||
// Verify test_client_id_ is in results
|
||
auto test_it = multi_results.find(test_client_id_);
|
||
if (test_it != multi_results.end()) {
|
||
EXPECT_FALSE(test_it->second.empty())
|
||
<< "test_client_ should have IP addresses";
|
||
}
|
||
|
||
// Verify segment_provider_client_id_ is in results
|
||
auto provider_it = multi_results.find(segment_provider_client_id_);
|
||
if (provider_it != multi_results.end()) {
|
||
EXPECT_FALSE(provider_it->second.empty())
|
||
<< "segment_provider_client_ should have IP addresses";
|
||
LOG(INFO) << "segment_provider_client_ IP addresses ("
|
||
<< provider_it->second.size() << "):";
|
||
for (size_t i = 0; i < provider_it->second.size(); ++i) {
|
||
LOG(INFO) << " [" << (i + 1) << "] " << provider_it->second[i];
|
||
}
|
||
}
|
||
|
||
// Test 3: Query with empty client_ids list
|
||
std::vector<UUID> empty_client_ids;
|
||
auto empty_result = test_client_->BatchQueryIp(empty_client_ids);
|
||
|
||
ASSERT_TRUE(empty_result.has_value());
|
||
EXPECT_TRUE(empty_result.value().empty())
|
||
<< "Empty client_ids should return empty results";
|
||
|
||
// Test 4: Query with non-existent client_id (should be silently skipped)
|
||
UUID non_existent_client_id = generate_uuid();
|
||
std::vector<UUID> non_existent_ids = {non_existent_client_id};
|
||
auto non_existent_result = test_client_->BatchQueryIp(non_existent_ids);
|
||
|
||
ASSERT_TRUE(non_existent_result.has_value());
|
||
// Non-existent client_id should not be in results (silently skipped)
|
||
EXPECT_TRUE(non_existent_result.value().empty() ||
|
||
non_existent_result.value().find(non_existent_client_id) ==
|
||
non_existent_result.value().end())
|
||
<< "Non-existent client_id should not be in results";
|
||
}
|
||
|
||
// Test batch put with duplicate keys
|
||
TEST_F(ClientIntegrationTest, BatchPutDuplicateKeys) {
|
||
const std::string test_data = "test_data_duplicate";
|
||
const std::string key = "duplicate_key";
|
||
|
||
// Create two identical keys
|
||
std::vector<std::string> keys = {key, key};
|
||
std::vector<std::vector<Slice>> batched_slices;
|
||
|
||
// Prepare data for both keys
|
||
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;
|
||
|
||
// Test batch put with duplicate keys
|
||
auto batch_put_results =
|
||
test_client_->BatchPut(keys, batched_slices, config);
|
||
|
||
// Check that we got results for both operations
|
||
ASSERT_EQ(batch_put_results.size(), 2);
|
||
|
||
// Both of them should success
|
||
// Because we currently consider `OBJECT_ALREADY_EXISTS` as success
|
||
|
||
for (const auto& result : batch_put_results) {
|
||
ASSERT_TRUE(result.has_value())
|
||
<< "BatchPut operation failed: " << toString(result.error());
|
||
}
|
||
|
||
// Clean up allocated memory
|
||
for (const auto& slices : batched_slices) {
|
||
for (const auto& slice : slices) {
|
||
client_buffer_allocator_->deallocate(slice.ptr, slice.size);
|
||
}
|
||
}
|
||
|
||
// Clean up the key that was successfully put
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_));
|
||
auto remove_result = test_client_->Remove(key);
|
||
// Remove might fail if the key wasn't actually put, which is fine
|
||
ASSERT_TRUE(remove_result);
|
||
}
|
||
|
||
// Test BatchReplicaClear operations through the client
|
||
TEST_F(ClientIntegrationTest, BatchReplicaClearOperations) {
|
||
// Skip test if we couldn't capture client_id
|
||
if (test_client_id_.first == 0 && test_client_id_.second == 0) {
|
||
GTEST_SKIP() << "Could not capture test_client_id, skipping "
|
||
"BatchReplicaClear test";
|
||
}
|
||
|
||
const std::string test_data = "Test data for BatchReplicaClear";
|
||
std::vector<std::string> keys = {"batch_clear_key1", "batch_clear_key2",
|
||
"batch_clear_key3"};
|
||
|
||
// Test 1: Clear a single key (all segments)
|
||
std::string key1 = keys[0];
|
||
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(key1, slices, config);
|
||
ASSERT_TRUE(put_result.has_value())
|
||
<< "Put operation failed: " << toString(put_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, test_data.size());
|
||
|
||
// Wait for lease to expire (PutEnd sets lease_timeout to now, but
|
||
// if IsExist was called, it would grant a new lease)
|
||
// Wait for the full lease TTL to ensure any lease granted by PutEnd or
|
||
// other operations has expired
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_ + 100));
|
||
const auto timeout = std::chrono::seconds(5);
|
||
const auto start_time = std::chrono::steady_clock::now();
|
||
bool cleared = false;
|
||
std::vector<std::string> single_key = {key1};
|
||
|
||
while (std::chrono::steady_clock::now() - start_time < timeout) {
|
||
auto clear_result = test_client_->BatchReplicaClear(
|
||
single_key, test_client_id_,
|
||
""); // Empty segment_name clears all segments
|
||
ASSERT_TRUE(clear_result.has_value())
|
||
<< "BatchReplicaClear failed: " << toString(clear_result.error());
|
||
|
||
if (clear_result.value().size() == 1) {
|
||
cleared = true;
|
||
break;
|
||
}
|
||
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||
}
|
||
|
||
ASSERT_TRUE(cleared) << "Failed to clear key within timeout period";
|
||
|
||
// Verify the key is removed
|
||
auto exist_result2 = test_client_->IsExist(key1);
|
||
ASSERT_TRUE(exist_result2.has_value());
|
||
ASSERT_FALSE(exist_result2.value())
|
||
<< "Key should be removed after BatchReplicaClear";
|
||
|
||
// Test 2: Clear multiple keys
|
||
std::vector<std::string> multiple_keys = {keys[1], keys[2]};
|
||
for (const auto& key : multiple_keys) {
|
||
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())
|
||
<< "Put operation failed for key: " << key
|
||
<< ", error: " << toString(put_result2.error());
|
||
client_buffer_allocator_->deallocate(buffer, test_data.size());
|
||
}
|
||
|
||
// Wait for lease to expire and clear
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_ + 100));
|
||
const auto start_time2 = std::chrono::steady_clock::now();
|
||
bool all_cleared = false;
|
||
|
||
while (std::chrono::steady_clock::now() - start_time2 < timeout) {
|
||
auto clear_result = test_client_->BatchReplicaClear(
|
||
multiple_keys, test_client_id_, ""); // Clear all segments
|
||
ASSERT_TRUE(clear_result.has_value())
|
||
<< "BatchReplicaClear failed: " << toString(clear_result.error());
|
||
|
||
if (clear_result.value().size() == multiple_keys.size()) {
|
||
all_cleared = true;
|
||
break;
|
||
}
|
||
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||
}
|
||
|
||
ASSERT_TRUE(all_cleared)
|
||
<< "Failed to clear all keys within timeout period";
|
||
|
||
// Verify all keys are removed
|
||
for (const auto& key : multiple_keys) {
|
||
auto exist_result3 = test_client_->IsExist(key);
|
||
ASSERT_TRUE(exist_result3.has_value());
|
||
ASSERT_FALSE(exist_result3.value()) << "Key should be removed: " << key;
|
||
}
|
||
|
||
// Test 3: Clear with empty keys list
|
||
std::vector<std::string> empty_keys;
|
||
auto empty_result =
|
||
test_client_->BatchReplicaClear(empty_keys, test_client_id_, "");
|
||
ASSERT_TRUE(empty_result.has_value());
|
||
EXPECT_TRUE(empty_result.value().empty())
|
||
<< "Empty keys should return empty results";
|
||
|
||
// Test 4: Clear with non-existent keys (should be silently skipped)
|
||
std::vector<std::string> non_existent_keys = {"non_existent_key1",
|
||
"non_existent_key2"};
|
||
auto non_existent_result =
|
||
test_client_->BatchReplicaClear(non_existent_keys, test_client_id_, "");
|
||
ASSERT_TRUE(non_existent_result.has_value());
|
||
// Non-existent keys should not be in results (silently skipped)
|
||
EXPECT_TRUE(non_existent_result.value().empty())
|
||
<< "Non-existent keys should return empty results";
|
||
}
|
||
|
||
// Helper: extract transport endpoints from Query() result
|
||
static std::unordered_set<std::string> ExtractReplicaEndpoints(
|
||
const decltype(std::declval<Client>()
|
||
.Query(std::declval<std::string>())
|
||
.value())& q) {
|
||
std::unordered_set<std::string> endpoints;
|
||
for (const auto& r : q.replicas) {
|
||
// Memory replicas carry buffer_descriptor.transport_endpoint_
|
||
endpoints.insert(
|
||
r.get_memory_descriptor().buffer_descriptor.transport_endpoint_);
|
||
}
|
||
return endpoints;
|
||
}
|
||
|
||
// Helper: fill a target segment by placing many objects there until Put fails.
|
||
// Returns keys that were successfully placed on target.
|
||
static std::vector<std::string> FillSegmentUntilFull(
|
||
const std::shared_ptr<Client>& writer_client, SimpleAllocator* writer_alloc,
|
||
const std::string& target_segment_name, size_t value_size, int num_keys) {
|
||
std::vector<std::string> keys;
|
||
keys.reserve(num_keys);
|
||
|
||
std::string payload(value_size, 'X');
|
||
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
cfg.preferred_segment = target_segment_name;
|
||
|
||
for (int i = 0; i < num_keys; ++i) {
|
||
std::string key =
|
||
"fill_" + target_segment_name + "_" + std::to_string(i);
|
||
|
||
void* buf = writer_alloc->allocate(payload.size());
|
||
if (!buf) {
|
||
EXPECT_NE(buf, nullptr);
|
||
return keys;
|
||
}
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
|
||
writer_client->Put(key, slices, cfg);
|
||
writer_alloc->deallocate(buf, payload.size());
|
||
|
||
keys.push_back(std::move(key));
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
// Helper: poll QueryTask until SUCCESS/FAILED or timeout.
|
||
static TaskStatus WaitTaskTerminalStatus(const std::shared_ptr<Client>& client,
|
||
const UUID& task_id,
|
||
std::chrono::milliseconds timeout,
|
||
std::chrono::milliseconds interval) {
|
||
const auto start = std::chrono::steady_clock::now();
|
||
TaskStatus last = TaskStatus::PROCESSING;
|
||
|
||
while (std::chrono::steady_clock::now() - start < timeout) {
|
||
auto q = client->QueryTask(task_id);
|
||
EXPECT_TRUE(q.has_value())
|
||
<< "QueryTask failed: " << toString(q.error());
|
||
last = q.value().status;
|
||
|
||
if (last == TaskStatus::SUCCESS || last == TaskStatus::FAILED)
|
||
return last;
|
||
std::this_thread::sleep_for(interval);
|
||
}
|
||
return last;
|
||
}
|
||
|
||
TEST_F(ClientIntegrationTest, ReplicaCopyAndMoveOperations) {
|
||
// Create two extra clients:
|
||
// - target_small: small segment to force allocation failure
|
||
// - target_big: normal segment for copy/move verification
|
||
const std::string target_small_name = "localhost:17814";
|
||
const std::string target_big_name = "localhost:17815";
|
||
|
||
auto target_small = CreateClient(target_small_name);
|
||
ASSERT_TRUE(target_small != nullptr);
|
||
|
||
auto target_big = CreateClient(target_big_name);
|
||
ASSERT_TRUE(target_big != nullptr);
|
||
|
||
// Mount segments for the extra clients
|
||
constexpr size_t kSegAlign = 16 * 1024 * 1024; // 16MB alignment
|
||
const size_t kSmallSeg = kSegAlign; // 16MB
|
||
const size_t kBigSeg = 8 * kSegAlign; // 128MB
|
||
|
||
void* small_seg_ptr = allocate_buffer_allocator_memory(kSmallSeg);
|
||
ASSERT_NE(small_seg_ptr, nullptr);
|
||
auto m1 = target_small->MountSegment(small_seg_ptr, kSmallSeg);
|
||
ASSERT_TRUE(m1.has_value())
|
||
<< "MountSegment(small) failed: " << toString(m1.error());
|
||
|
||
void* big_seg_ptr = allocate_buffer_allocator_memory(kBigSeg);
|
||
ASSERT_NE(big_seg_ptr, nullptr);
|
||
auto m2 = target_big->MountSegment(big_seg_ptr, kBigSeg);
|
||
ASSERT_TRUE(m2.has_value())
|
||
<< "MountSegment(big) failed: " << toString(m2.error());
|
||
|
||
// --- Phase A: Fill small target (segment full) ---
|
||
const size_t kFillValueSize = 512 * 1024; // 512KB
|
||
auto fill_keys = FillSegmentUntilFull(
|
||
test_client_, client_buffer_allocator_.get(), target_small_name,
|
||
kFillValueSize, /*num_keys=*/50);
|
||
|
||
ASSERT_FALSE(fill_keys.empty())
|
||
<< "Failed to fill anything into target_small; test setup invalid";
|
||
|
||
// --- Phase B: COPY retry should succeed after freeing space ---
|
||
{
|
||
const std::string source_key = "retry_key_success_cpp";
|
||
std::string payload(kFillValueSize, 'A');
|
||
|
||
// Put source object onto the source segment (test_client_ segment).
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
cfg.preferred_segment = "localhost:17813"; // test_client_ name
|
||
|
||
void* buf = client_buffer_allocator_->allocate(payload.size());
|
||
ASSERT_NE(buf, nullptr);
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
auto put_res = test_client_->Put(source_key, slices, cfg);
|
||
client_buffer_allocator_->deallocate(buf, payload.size());
|
||
ASSERT_TRUE(put_res.has_value())
|
||
<< "Put(source) failed: " << toString(put_res.error());
|
||
|
||
// Start COPY task to the full small target.
|
||
auto copy_task =
|
||
test_client_->CreateCopyTask(source_key, {target_small_name});
|
||
ASSERT_TRUE(copy_task.has_value())
|
||
<< "CreateCopyTask failed: " << toString(copy_task.error());
|
||
const auto copy_task_id = copy_task.value();
|
||
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(
|
||
500)); // Give some time for initial attempt
|
||
|
||
// Free space by removing one of the fill keys.
|
||
test_client_->Remove(fill_keys.front());
|
||
|
||
// Poll task: should succeed after space is freed.
|
||
auto status =
|
||
WaitTaskTerminalStatus(test_client_, copy_task_id,
|
||
/*timeout=*/std::chrono::seconds(30),
|
||
/*interval=*/std::chrono::milliseconds(200));
|
||
|
||
ASSERT_EQ(status, TaskStatus::SUCCESS)
|
||
<< "COPY task did not succeed after freeing space";
|
||
|
||
// Verify replica now exists on target_small (by endpoint)
|
||
auto q = test_client_->Query(source_key);
|
||
ASSERT_TRUE(q.has_value()) << "Query failed: " << toString(q.error());
|
||
|
||
auto endpoints = ExtractReplicaEndpoints(q.value());
|
||
EXPECT_TRUE(endpoints.contains(target_small->GetTransportEndpoint()))
|
||
<< "Expected a replica on target_small endpoint";
|
||
}
|
||
|
||
// --- Phase C: COPY retry should fail if we do NOT free space ---
|
||
{
|
||
const std::string source_key = "retry_key_fail_cpp";
|
||
std::string payload(kFillValueSize, 'B');
|
||
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
cfg.preferred_segment = "localhost:17813";
|
||
|
||
void* buf = client_buffer_allocator_->allocate(payload.size());
|
||
ASSERT_NE(buf, nullptr);
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
auto put_res = test_client_->Put(source_key, slices, cfg);
|
||
client_buffer_allocator_->deallocate(buf, payload.size());
|
||
ASSERT_TRUE(put_res.has_value())
|
||
<< "Put(source) failed: " << toString(put_res.error());
|
||
|
||
auto copy_task =
|
||
test_client_->CreateCopyTask(source_key, {target_small_name});
|
||
ASSERT_TRUE(copy_task.has_value())
|
||
<< "CreateCopyTask failed: " << toString(copy_task.error());
|
||
const auto copy_task_id = copy_task.value();
|
||
|
||
// Do NOT free space; wait for retries to exhaust.
|
||
auto status =
|
||
WaitTaskTerminalStatus(test_client_, copy_task_id,
|
||
/*timeout=*/std::chrono::seconds(60),
|
||
/*interval=*/std::chrono::milliseconds(300));
|
||
|
||
ASSERT_EQ(status, TaskStatus::FAILED)
|
||
<< "COPY task unexpectedly succeeded or did not fail in time";
|
||
}
|
||
|
||
// --- Phase D: Basic multi-client COPY and MOVE---
|
||
{
|
||
const int kNumKeys = 10;
|
||
std::vector<std::string> keys;
|
||
keys.reserve(kNumKeys);
|
||
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
cfg.preferred_segment = "localhost:17813";
|
||
|
||
// Put keys on source segment
|
||
for (int i = 0; i < kNumKeys; ++i) {
|
||
std::string key = "cm_key_" + std::to_string(i);
|
||
std::string payload(4 * 1024, static_cast<char>('a' + (i % 26)));
|
||
|
||
void* buf = client_buffer_allocator_->allocate(payload.size());
|
||
ASSERT_NE(buf, nullptr);
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
|
||
auto r = test_client_->Put(key, slices, cfg);
|
||
client_buffer_allocator_->deallocate(buf, payload.size());
|
||
ASSERT_TRUE(r.has_value()) << "Put failed: " << toString(r.error());
|
||
|
||
keys.push_back(std::move(key));
|
||
}
|
||
|
||
// COPY all keys to target_big
|
||
std::vector<UUID> copy_task_ids;
|
||
|
||
copy_task_ids.reserve(keys.size());
|
||
for (const auto& key : keys) {
|
||
auto t = test_client_->CreateCopyTask(key, {target_big_name});
|
||
ASSERT_TRUE(t.has_value())
|
||
<< "CreateCopyTask failed: " << toString(t.error());
|
||
copy_task_ids.push_back(t.value());
|
||
}
|
||
|
||
for (const auto& tid : copy_task_ids) {
|
||
auto status = WaitTaskTerminalStatus(
|
||
test_client_, tid,
|
||
/*timeout=*/std::chrono::seconds(30),
|
||
/*interval=*/std::chrono::milliseconds(200));
|
||
ASSERT_EQ(status, TaskStatus::SUCCESS) << "COPY did not succeed";
|
||
}
|
||
|
||
// MOVE first 5 keys from source -> target_big
|
||
std::vector<UUID> move_task_ids;
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
auto t = test_client_->CreateMoveTask(keys[i], "localhost:17813",
|
||
target_big_name);
|
||
ASSERT_TRUE(t.has_value())
|
||
<< "CreateMoveTask failed: " << toString(t.error());
|
||
move_task_ids.push_back(t.value());
|
||
}
|
||
|
||
for (const auto& tid : move_task_ids) {
|
||
auto status = WaitTaskTerminalStatus(
|
||
test_client_, tid,
|
||
/*timeout=*/std::chrono::seconds(30),
|
||
/*interval=*/std::chrono::milliseconds(200));
|
||
ASSERT_EQ(status, TaskStatus::SUCCESS) << "MOVE did not succeed";
|
||
}
|
||
|
||
// Verify moved keys are on target_big and (ideally) not on source
|
||
// endpoint
|
||
const auto source_ep = test_client_->GetTransportEndpoint();
|
||
const auto target_ep = target_big->GetTransportEndpoint();
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
auto q = test_client_->Query(keys[i]);
|
||
ASSERT_TRUE(q.has_value())
|
||
<< "Query failed: " << toString(q.error());
|
||
auto eps = ExtractReplicaEndpoints(q.value());
|
||
|
||
EXPECT_TRUE(eps.contains(target_ep))
|
||
<< "Moved key missing on target_big";
|
||
EXPECT_FALSE(eps.contains(source_ep))
|
||
<< "Moved key still present on source";
|
||
}
|
||
}
|
||
|
||
// Unmount and free extra segments
|
||
auto u1 = target_small->UnmountSegment(small_seg_ptr, kSmallSeg);
|
||
EXPECT_TRUE(u1.has_value()) << "UnmountSegment(small) failed";
|
||
auto u2 = target_big->UnmountSegment(big_seg_ptr, kBigSeg);
|
||
EXPECT_TRUE(u2.has_value()) << "UnmountSegment(big) failed";
|
||
|
||
std::free(small_seg_ptr);
|
||
std::free(big_seg_ptr);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Eviction notification integration test
|
||
// Uses a separate InProcMaster with root_fs_dir and small quota_bytes so that
|
||
// StorageBackend triggers FIFO eviction after a few Puts. Verifies that the
|
||
// evicted key's DISK replica is removed from master metadata.
|
||
// ---------------------------------------------------------------------------
|
||
class EvictionNotificationTest : public ::testing::Test {
|
||
protected:
|
||
void SetUp() override {
|
||
// Create a temporary directory for storage
|
||
tmp_dir_ = std::filesystem::temp_directory_path() /
|
||
("mc_evict_test_" + std::to_string(::getpid()));
|
||
std::filesystem::create_directories(tmp_dir_);
|
||
|
||
// Start master with root_fs_dir pointing to our temp dir and small
|
||
// quota. The client will discover fsdir = root_fs_dir/cluster_id and
|
||
// initialise a StorageBackend with the given quota.
|
||
auto config = InProcMasterConfigBuilder()
|
||
.set_root_fs_dir(tmp_dir_.string())
|
||
.set_enable_disk_eviction(true)
|
||
.set_quota_bytes(kQuotaBytes)
|
||
.build();
|
||
ASSERT_TRUE(master_.Start(config));
|
||
master_address_ = master_.master_address();
|
||
}
|
||
|
||
void TearDown() override {
|
||
client_.reset();
|
||
master_.Stop();
|
||
std::error_code ec;
|
||
std::filesystem::remove_all(tmp_dir_, ec);
|
||
}
|
||
|
||
void CreateClientAndMount() {
|
||
auto client_opt = Client::Create("localhost:17820", // unique hostname
|
||
"P2PHANDSHAKE", FLAGS_protocol,
|
||
std::nullopt, master_address_);
|
||
ASSERT_TRUE(client_opt.has_value()) << "Failed to create client";
|
||
client_ = client_opt.value();
|
||
|
||
// Mount segment so that PutStart can allocate memory replicas
|
||
constexpr size_t kSegSize = 64 * 1024 * 1024; // 64MB
|
||
seg_ptr_ = allocate_buffer_allocator_memory(kSegSize);
|
||
ASSERT_NE(seg_ptr_, nullptr);
|
||
seg_size_ = kSegSize;
|
||
auto mount = client_->MountSegment(seg_ptr_, seg_size_, FLAGS_protocol);
|
||
ASSERT_TRUE(mount.has_value())
|
||
<< "MountSegment failed: " << toString(mount.error());
|
||
|
||
// Register local memory for client-side buffers
|
||
alloc_ = std::make_unique<SimpleAllocator>(16 * 1024 * 1024);
|
||
auto reg = client_->RegisterLocalMemory(
|
||
alloc_->getBase(), 16 * 1024 * 1024, "cpu:0", false, false);
|
||
ASSERT_TRUE(reg.has_value())
|
||
<< "RegisterLocalMemory failed: " << toString(reg.error());
|
||
}
|
||
|
||
// Helper: check if a key has a DISK replica in master
|
||
bool HasDiskReplica(const std::string& key) {
|
||
auto q = client_->Query(key);
|
||
if (!q.has_value()) return false;
|
||
for (const auto& r : q.value().replicas) {
|
||
if (r.is_disk_replica()) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Helper: wait until a key has a DISK replica (async PutToLocalFile)
|
||
bool WaitForDiskReplica(const std::string& key,
|
||
std::chrono::milliseconds timeout) {
|
||
auto start = std::chrono::steady_clock::now();
|
||
while (std::chrono::steady_clock::now() - start < timeout) {
|
||
if (HasDiskReplica(key)) return true;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Helper: wait until a key no longer has a DISK replica
|
||
bool WaitForNoDiskReplica(const std::string& key,
|
||
std::chrono::milliseconds timeout) {
|
||
auto start = std::chrono::steady_clock::now();
|
||
while (std::chrono::steady_clock::now() - start < timeout) {
|
||
if (!HasDiskReplica(key)) return true;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Quota: 3KB so that 3 × 1KB objects fit, and the 4th triggers eviction
|
||
static constexpr uint64_t kQuotaBytes = 3 * 1024;
|
||
static constexpr size_t kValueSize = 1024;
|
||
|
||
InProcMaster master_;
|
||
std::string master_address_;
|
||
std::filesystem::path tmp_dir_;
|
||
std::shared_ptr<Client> client_;
|
||
std::unique_ptr<SimpleAllocator> alloc_;
|
||
void* seg_ptr_ = nullptr;
|
||
size_t seg_size_ = 0;
|
||
};
|
||
|
||
TEST_F(EvictionNotificationTest, DiskReplicaRemovedAfterEviction) {
|
||
CreateClientAndMount();
|
||
|
||
// Put 3 keys — should all fit within quota.
|
||
// Wait for each DISK replica before putting the next key so that the
|
||
// FIFO write-queue order is deterministic (write_thread_pool_ has >1
|
||
// thread, so concurrent writes can reorder).
|
||
std::vector<std::string> keys;
|
||
for (int i = 0; i < 3; ++i) {
|
||
std::string key = "evict_test_key_" + std::to_string(i);
|
||
std::string payload(kValueSize, 'A' + i);
|
||
|
||
void* buf = alloc_->allocate(payload.size());
|
||
ASSERT_NE(buf, nullptr);
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
auto put = client_->Put(key, slices, cfg);
|
||
ASSERT_TRUE(put.has_value())
|
||
<< "Put(" << key << ") failed: " << toString(put.error());
|
||
alloc_->deallocate(buf, payload.size());
|
||
keys.push_back(key);
|
||
|
||
// Wait for this key's DISK replica before proceeding to the next
|
||
// Put, ensuring deterministic FIFO order in the eviction queue.
|
||
ASSERT_TRUE(WaitForDiskReplica(key, std::chrono::seconds(10)))
|
||
<< "DISK replica did not appear for key: " << key;
|
||
}
|
||
|
||
// Put a 4th key — triggers eviction of the oldest (key 0) due to FIFO
|
||
{
|
||
std::string key = "evict_test_key_3";
|
||
std::string payload(kValueSize, 'D');
|
||
|
||
void* buf = alloc_->allocate(payload.size());
|
||
ASSERT_NE(buf, nullptr);
|
||
std::memcpy(buf, payload.data(), payload.size());
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buf, payload.size()});
|
||
|
||
ReplicateConfig cfg;
|
||
cfg.replica_num = 1;
|
||
auto put = client_->Put(key, slices, cfg);
|
||
ASSERT_TRUE(put.has_value())
|
||
<< "Put(" << key << ") failed: " << toString(put.error());
|
||
alloc_->deallocate(buf, payload.size());
|
||
keys.push_back(key);
|
||
}
|
||
|
||
// Wait for the 4th key's DISK replica to appear
|
||
ASSERT_TRUE(WaitForDiskReplica(keys[3], std::chrono::seconds(10)))
|
||
<< "DISK replica did not appear for eviction-trigger key";
|
||
|
||
// The oldest key (key 0) should have had its DISK replica evicted
|
||
EXPECT_TRUE(WaitForNoDiskReplica(keys[0], std::chrono::seconds(10)))
|
||
<< "Evicted key's DISK replica was not removed from master";
|
||
|
||
// Keys 1..3 should still have DISK replicas
|
||
for (int i = 1; i <= 3; ++i) {
|
||
EXPECT_TRUE(HasDiskReplica(keys[i]))
|
||
<< "Key " << keys[i] << " should still have a DISK replica";
|
||
}
|
||
|
||
// Clean up: unmount segment
|
||
auto unmount = client_->UnmountSegment(seg_ptr_, seg_size_);
|
||
EXPECT_TRUE(unmount.has_value()) << "UnmountSegment failed";
|
||
std::free(seg_ptr_);
|
||
seg_ptr_ = nullptr;
|
||
}
|
||
|
||
// Test Upsert Case A: key does not exist — equivalent to Put
|
||
TEST_F(ClientIntegrationTest, UpsertNewKey) {
|
||
const std::string test_data = "upsert_new_key_data";
|
||
const std::string key = "upsert_case_a_key";
|
||
void* buffer = client_buffer_allocator_->allocate(test_data.size());
|
||
ASSERT_NE(buffer, nullptr);
|
||
|
||
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;
|
||
|
||
// Upsert on a non-existent key should succeed (Case A)
|
||
auto upsert_result = test_client_->Upsert(key, slices, config);
|
||
ASSERT_TRUE(upsert_result.has_value())
|
||
<< "Upsert (Case A) failed: " << toString(upsert_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, test_data.size());
|
||
|
||
// Verify data through Get
|
||
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_TRUE(get_result.has_value())
|
||
<< "Get after Upsert failed: " << toString(get_result.error());
|
||
ASSERT_EQ(memcmp(slices[0].ptr, test_data.data(), test_data.size()), 0);
|
||
client_buffer_allocator_->deallocate(buffer, test_data.size());
|
||
|
||
// Clean up
|
||
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 failed: " << toString(remove_result.error());
|
||
}
|
||
|
||
// Test Upsert Case B: key exists, same size — in-place update
|
||
TEST_F(ClientIntegrationTest, UpsertSameSize) {
|
||
const std::string key = "upsert_case_b_key";
|
||
const size_t data_size = 64;
|
||
|
||
// Initial data: all 'A'
|
||
std::string initial_data(data_size, 'A');
|
||
void* buffer = client_buffer_allocator_->allocate(data_size);
|
||
ASSERT_NE(buffer, nullptr);
|
||
memcpy(buffer, initial_data.data(), data_size);
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buffer, data_size});
|
||
|
||
ReplicateConfig config;
|
||
config.replica_num = 1;
|
||
|
||
// First Put to create the key
|
||
auto put_result = test_client_->Put(key, slices, config);
|
||
ASSERT_TRUE(put_result.has_value())
|
||
<< "Initial Put failed: " << toString(put_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, data_size);
|
||
|
||
// Wait for lease to expire so refcnt drops to 0
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_));
|
||
|
||
// Upsert with same size but different content: all 'B'
|
||
std::string updated_data(data_size, 'B');
|
||
buffer = client_buffer_allocator_->allocate(data_size);
|
||
ASSERT_NE(buffer, nullptr);
|
||
memcpy(buffer, updated_data.data(), data_size);
|
||
slices.clear();
|
||
slices.emplace_back(Slice{buffer, data_size});
|
||
|
||
auto upsert_result = test_client_->Upsert(key, slices, config);
|
||
ASSERT_TRUE(upsert_result.has_value())
|
||
<< "Upsert (Case B) failed: " << toString(upsert_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, data_size);
|
||
|
||
// Verify the data was updated
|
||
buffer = client_buffer_allocator_->allocate(data_size);
|
||
slices.clear();
|
||
slices.emplace_back(Slice{buffer, data_size});
|
||
auto get_result = test_client_->Get(key, slices);
|
||
ASSERT_TRUE(get_result.has_value())
|
||
<< "Get after Upsert failed: " << toString(get_result.error());
|
||
ASSERT_EQ(memcmp(slices[0].ptr, updated_data.data(), data_size), 0)
|
||
<< "Data should be updated to 'B's after in-place upsert";
|
||
client_buffer_allocator_->deallocate(buffer, data_size);
|
||
|
||
// Clean up
|
||
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 failed: " << toString(remove_result.error());
|
||
}
|
||
|
||
// Test Upsert Case C: key exists, different size — delete and reallocate
|
||
TEST_F(ClientIntegrationTest, UpsertDifferentSize) {
|
||
const std::string key = "upsert_case_c_key";
|
||
const size_t initial_size = 64;
|
||
const size_t updated_size = 128;
|
||
|
||
// Initial data: 64 bytes of 'X'
|
||
std::string initial_data(initial_size, 'X');
|
||
void* buffer = client_buffer_allocator_->allocate(initial_size);
|
||
ASSERT_NE(buffer, nullptr);
|
||
memcpy(buffer, initial_data.data(), initial_size);
|
||
std::vector<Slice> slices;
|
||
slices.emplace_back(Slice{buffer, initial_size});
|
||
|
||
ReplicateConfig config;
|
||
config.replica_num = 1;
|
||
|
||
auto put_result = test_client_->Put(key, slices, config);
|
||
ASSERT_TRUE(put_result.has_value())
|
||
<< "Initial Put failed: " << toString(put_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, initial_size);
|
||
|
||
// Wait for lease to expire so refcnt drops to 0
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_));
|
||
|
||
// Upsert with different (larger) size: 128 bytes of 'Y'
|
||
std::string updated_data(updated_size, 'Y');
|
||
buffer = client_buffer_allocator_->allocate(updated_size);
|
||
ASSERT_NE(buffer, nullptr);
|
||
memcpy(buffer, updated_data.data(), updated_size);
|
||
slices.clear();
|
||
slices.emplace_back(Slice{buffer, updated_size});
|
||
|
||
auto upsert_result = test_client_->Upsert(key, slices, config);
|
||
ASSERT_TRUE(upsert_result.has_value())
|
||
<< "Upsert (Case C) failed: " << toString(upsert_result.error());
|
||
client_buffer_allocator_->deallocate(buffer, updated_size);
|
||
|
||
// Verify the data was updated with new size
|
||
buffer = client_buffer_allocator_->allocate(updated_size);
|
||
slices.clear();
|
||
slices.emplace_back(Slice{buffer, updated_size});
|
||
auto get_result = test_client_->Get(key, slices);
|
||
ASSERT_TRUE(get_result.has_value())
|
||
<< "Get after Upsert failed: " << toString(get_result.error());
|
||
ASSERT_EQ(slices[0].size, updated_size);
|
||
ASSERT_EQ(memcmp(slices[0].ptr, updated_data.data(), updated_size), 0)
|
||
<< "Data should be updated to 'Y's with new size";
|
||
client_buffer_allocator_->deallocate(buffer, updated_size);
|
||
|
||
// Clean up
|
||
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 failed: " << toString(remove_result.error());
|
||
}
|
||
|
||
// Test BatchUpsert with mixed cases (A + B + C)
|
||
TEST_F(ClientIntegrationTest, BatchUpsertMixed) {
|
||
const size_t data_size = 64;
|
||
ReplicateConfig config;
|
||
config.replica_num = 1;
|
||
|
||
// Pre-create key_b (for Case B: same size) and key_c (for Case C: diff
|
||
// size)
|
||
std::string key_b = "batch_upsert_case_b";
|
||
std::string key_c = "batch_upsert_case_c";
|
||
|
||
// Put key_b: 64 bytes of 'M'
|
||
{
|
||
std::string data(data_size, 'M');
|
||
void* buf = client_buffer_allocator_->allocate(data_size);
|
||
memcpy(buf, data.data(), data_size);
|
||
std::vector<Slice> sl;
|
||
sl.emplace_back(Slice{buf, data_size});
|
||
auto r = test_client_->Put(key_b, sl, config);
|
||
ASSERT_TRUE(r.has_value())
|
||
<< "Put key_b failed: " << toString(r.error());
|
||
client_buffer_allocator_->deallocate(buf, data_size);
|
||
}
|
||
|
||
// Put key_c: 64 bytes of 'N'
|
||
{
|
||
std::string data(data_size, 'N');
|
||
void* buf = client_buffer_allocator_->allocate(data_size);
|
||
memcpy(buf, data.data(), data_size);
|
||
std::vector<Slice> sl;
|
||
sl.emplace_back(Slice{buf, data_size});
|
||
auto r = test_client_->Put(key_c, sl, config);
|
||
ASSERT_TRUE(r.has_value())
|
||
<< "Put key_c failed: " << toString(r.error());
|
||
client_buffer_allocator_->deallocate(buf, data_size);
|
||
}
|
||
|
||
// Wait for lease to expire
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_));
|
||
|
||
// Now BatchUpsert:
|
||
// key_a (new) → Case A, 64 bytes of 'P'
|
||
// key_b (exists, same) → Case B, 64 bytes of 'Q'
|
||
// key_c (exists, larger) → Case C, 128 bytes of 'R'
|
||
std::string key_a = "batch_upsert_case_a";
|
||
std::vector<std::string> keys = {key_a, key_b, key_c};
|
||
|
||
const size_t size_a = data_size;
|
||
const size_t size_b = data_size;
|
||
const size_t size_c = data_size * 2;
|
||
std::string data_a(size_a, 'P');
|
||
std::string data_b(size_b, 'Q');
|
||
std::string data_c(size_c, 'R');
|
||
|
||
std::vector<std::vector<Slice>> batched_slices;
|
||
std::vector<void*> alloc_ptrs; // track for cleanup
|
||
|
||
auto alloc_and_fill = [&](const std::string& data, size_t sz) {
|
||
void* buf = client_buffer_allocator_->allocate(sz);
|
||
EXPECT_NE(buf, nullptr);
|
||
memcpy(buf, data.data(), sz);
|
||
alloc_ptrs.push_back(buf);
|
||
std::vector<Slice> sl;
|
||
sl.emplace_back(Slice{buf, sz});
|
||
batched_slices.push_back(std::move(sl));
|
||
};
|
||
|
||
alloc_and_fill(data_a, size_a);
|
||
alloc_and_fill(data_b, size_b);
|
||
alloc_and_fill(data_c, size_c);
|
||
|
||
auto batch_results =
|
||
test_client_->BatchUpsert(keys, batched_slices, config);
|
||
ASSERT_EQ(batch_results.size(), 3);
|
||
for (size_t i = 0; i < batch_results.size(); ++i) {
|
||
ASSERT_TRUE(batch_results[i].has_value())
|
||
<< "BatchUpsert failed for key " << keys[i] << ": "
|
||
<< toString(batch_results[i].error());
|
||
}
|
||
|
||
// Free write buffers
|
||
client_buffer_allocator_->deallocate(alloc_ptrs[0], size_a);
|
||
client_buffer_allocator_->deallocate(alloc_ptrs[1], size_b);
|
||
client_buffer_allocator_->deallocate(alloc_ptrs[2], size_c);
|
||
|
||
// Verify each key's data
|
||
auto verify = [&](const std::string& key, const std::string& expected,
|
||
size_t sz) {
|
||
void* buf = client_buffer_allocator_->allocate(sz);
|
||
std::vector<Slice> sl;
|
||
sl.emplace_back(Slice{buf, sz});
|
||
auto get_result = test_client_->Get(key, sl);
|
||
EXPECT_TRUE(get_result.has_value())
|
||
<< "Get failed for " << key << ": " << toString(get_result.error());
|
||
if (get_result.has_value()) {
|
||
EXPECT_EQ(sl[0].size, sz);
|
||
EXPECT_EQ(memcmp(sl[0].ptr, expected.data(), sz), 0)
|
||
<< "Data mismatch for " << key;
|
||
}
|
||
client_buffer_allocator_->deallocate(buf, sz);
|
||
};
|
||
|
||
verify(key_a, data_a, size_a);
|
||
verify(key_b, data_b, size_b);
|
||
verify(key_c, data_c, size_c);
|
||
|
||
// Clean up
|
||
std::this_thread::sleep_for(
|
||
std::chrono::milliseconds(default_kv_lease_ttl_));
|
||
for (const auto& key : keys) {
|
||
auto r = test_client_->Remove(key);
|
||
EXPECT_TRUE(r.has_value())
|
||
<< "Remove failed for " << key << ": " << toString(r.error());
|
||
}
|
||
}
|
||
|
||
} // namespace testing
|
||
|
||
} // namespace mooncake
|
||
|
||
int main(int argc, char** argv) {
|
||
// Initialize Google Test
|
||
::testing::InitGoogleTest(&argc, argv);
|
||
|
||
// Initialize Google's flags library
|
||
gflags::ParseCommandLineFlags(&argc, &argv, false);
|
||
mooncake::init_ylt_log_level();
|
||
// Run all tests
|
||
return RUN_ALL_TESTS();
|
||
}
|