forked from mooncake-track/Mooncake
1373 lines
55 KiB
C++
1373 lines
55 KiB
C++
#include <gflags/gflags.h>
|
|
#include <glog/logging.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <csignal>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include <vector>
|
|
|
|
#include <ylt/coro_http/coro_http_client.hpp>
|
|
#include <ylt/struct_json/json_reader.h>
|
|
#include <ylt/struct_json/json_writer.h>
|
|
#include <ylt/reflection/user_reflect_macro.hpp>
|
|
|
|
#include "client_service.h"
|
|
#include "master_client.h"
|
|
#include "task_manager.h"
|
|
#include "types.h"
|
|
#include "utils.h"
|
|
#include "test_server_helpers.h"
|
|
#include "default_config.h"
|
|
#include "allocator.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 {
|
|
|
|
namespace {
|
|
|
|
struct HttpCreateDrainJobResponse {
|
|
bool success{false};
|
|
std::string job_id;
|
|
std::string status;
|
|
int32_t error_code{0};
|
|
std::string error_message;
|
|
};
|
|
YLT_REFL(HttpCreateDrainJobResponse, success, job_id, status, error_code,
|
|
error_message);
|
|
|
|
struct HttpQueryDrainJobResponse {
|
|
bool success{false};
|
|
std::string job_id;
|
|
int32_t type{0};
|
|
std::string type_name;
|
|
int32_t status{0};
|
|
std::string status_name;
|
|
int64_t created_at_ms_epoch{0};
|
|
int64_t last_updated_at_ms_epoch{0};
|
|
std::vector<std::string> segments;
|
|
uint64_t succeeded_units{0};
|
|
uint64_t failed_units{0};
|
|
uint64_t blocked_units{0};
|
|
uint64_t active_units{0};
|
|
uint64_t migrated_bytes{0};
|
|
std::string message;
|
|
int32_t error_code{0};
|
|
std::string error_message;
|
|
};
|
|
YLT_REFL(HttpQueryDrainJobResponse, success, job_id, type, type_name, status,
|
|
status_name, created_at_ms_epoch, last_updated_at_ms_epoch, segments,
|
|
succeeded_units, failed_units, blocked_units, active_units,
|
|
migrated_bytes, message, error_code, error_message);
|
|
|
|
struct HttpSegmentStatusResponse {
|
|
bool success{false};
|
|
std::string segment;
|
|
int32_t status{0};
|
|
std::string status_name;
|
|
int32_t error_code{0};
|
|
std::string error_message;
|
|
};
|
|
YLT_REFL(HttpSegmentStatusResponse, success, segment, status, status_name,
|
|
error_code, error_message);
|
|
|
|
tl::expected<std::string, int> HttpPostJson(const std::string& url,
|
|
const std::string& body) {
|
|
coro_http::coro_http_client client;
|
|
auto response = client.post(url, body, coro_http::req_content_type::json);
|
|
if (response.status != 200) {
|
|
return tl::unexpected(response.status);
|
|
}
|
|
return std::string(response.resp_body);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class TaskExecutorIntegrationTest : public ::testing::Test {
|
|
protected:
|
|
static void SetUpTestSuite() {
|
|
google::InitGoogleLogging("TaskExecutorIntegrationTest");
|
|
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;
|
|
|
|
uint64_t default_kv_lease_ttl = DEFAULT_DEFAULT_KV_LEASE_TTL;
|
|
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;
|
|
}
|
|
|
|
// Start an in-process non-HA master with HTTP metadata server
|
|
InProcMasterConfig config;
|
|
config.http_metadata_port = getFreeTcpPort();
|
|
config.default_kv_lease_ttl = default_kv_lease_ttl;
|
|
|
|
ASSERT_TRUE(master_.Start(config));
|
|
master_address_ = master_.master_address();
|
|
metadata_url_ = master_.metadata_url();
|
|
|
|
LOG(INFO) << "Started in-proc master at " << master_address_
|
|
<< ", metadata_url=" << metadata_url_;
|
|
}
|
|
|
|
static void TearDownTestSuite() {
|
|
CleanupClients();
|
|
master_.Stop();
|
|
google::ShutdownGoogleLogging();
|
|
}
|
|
|
|
void SetUp() override {
|
|
// Create client 1
|
|
auto client1_opt = Client::Create(
|
|
"127.0.0.1:18001",
|
|
metadata_url_.empty() ? "P2PHANDSHAKE" : metadata_url_,
|
|
FLAGS_protocol,
|
|
FLAGS_device_name.empty() ? std::nullopt
|
|
: std::make_optional(FLAGS_device_name),
|
|
master_address_);
|
|
ASSERT_TRUE(client1_opt.has_value());
|
|
client1_ = client1_opt.value();
|
|
|
|
// Create client 2
|
|
auto client2_opt = Client::Create(
|
|
"127.0.0.1:18002",
|
|
metadata_url_.empty() ? "P2PHANDSHAKE" : metadata_url_,
|
|
FLAGS_protocol,
|
|
FLAGS_device_name.empty() ? std::nullopt
|
|
: std::make_optional(FLAGS_device_name),
|
|
master_address_);
|
|
ASSERT_TRUE(client2_opt.has_value());
|
|
client2_ = client2_opt.value();
|
|
|
|
// Create master client for API calls
|
|
UUID master_client_id = generate_uuid();
|
|
master_client_ = std::make_unique<MasterClient>(master_client_id);
|
|
ASSERT_EQ(master_client_->Connect(master_address_), ErrorCode::OK);
|
|
|
|
// Mount segments for both clients
|
|
size_t segment_size = 256 * 1024 * 1024; // 256 MB per segment
|
|
|
|
// Client 1 segment
|
|
client1_segment_ptr_ = allocate_buffer_allocator_memory(segment_size);
|
|
ASSERT_NE(client1_segment_ptr_, nullptr);
|
|
auto mount1_result =
|
|
client1_->MountSegment(client1_segment_ptr_, segment_size);
|
|
ASSERT_TRUE(mount1_result.has_value())
|
|
<< "Failed to mount segment for client1: "
|
|
<< toString(mount1_result.error());
|
|
|
|
// Client 2 segment
|
|
client2_segment_ptr_ = allocate_buffer_allocator_memory(segment_size);
|
|
ASSERT_NE(client2_segment_ptr_, nullptr);
|
|
auto mount2_result =
|
|
client2_->MountSegment(client2_segment_ptr_, segment_size);
|
|
ASSERT_TRUE(mount2_result.has_value())
|
|
<< "Failed to mount segment for client2: "
|
|
<< toString(mount2_result.error());
|
|
|
|
// Wait for segments to be registered and clients to ping master
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
|
|
|
// Client IDs will be extracted from task assignments when tasks are
|
|
// created For now, initialize with placeholder values
|
|
client1_id_ = generate_uuid();
|
|
client2_id_ = generate_uuid();
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (client1_ && client1_segment_ptr_) {
|
|
free(client1_segment_ptr_);
|
|
client1_segment_ptr_ = nullptr;
|
|
}
|
|
if (client2_ && client2_segment_ptr_) {
|
|
free(client2_segment_ptr_);
|
|
client2_segment_ptr_ = nullptr;
|
|
}
|
|
}
|
|
|
|
static void CleanupClients() {
|
|
// Clients will be cleaned up by shared_ptr
|
|
}
|
|
|
|
// Helper to extract client_id from task assignment
|
|
// When a task is created, we can query it to see which client it's assigned
|
|
// to
|
|
UUID GetClientIdFromTask(const UUID& task_id) {
|
|
auto query_result = master_client_->QueryTask(task_id);
|
|
if (query_result.has_value()) {
|
|
return query_result.value().assigned_client;
|
|
}
|
|
return generate_uuid(); // Fallback
|
|
}
|
|
|
|
// Wait for task to complete by polling task status
|
|
bool WaitForTaskCompletion(
|
|
const UUID& task_id,
|
|
std::chrono::seconds timeout = std::chrono::seconds(30)) {
|
|
auto start = std::chrono::steady_clock::now();
|
|
while (std::chrono::steady_clock::now() - start < timeout) {
|
|
auto query_result = master_client_->QueryTask(task_id);
|
|
if (query_result.has_value()) {
|
|
const auto& task_response = query_result.value();
|
|
if (task_response.status == TaskStatus::SUCCESS ||
|
|
task_response.status == TaskStatus::FAILED) {
|
|
return task_response.status == TaskStatus::SUCCESS;
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
tl::expected<HttpCreateDrainJobResponse, int> CreateDrainJobViaHttp(
|
|
const CreateDrainJobRequest& request) {
|
|
std::string body;
|
|
struct_json::to_json(request, body);
|
|
auto response = HttpPostJson(
|
|
master_.http_metrics_base() + "/api/v1/drain_jobs", body);
|
|
if (!response.has_value()) {
|
|
return tl::unexpected(response.error());
|
|
}
|
|
|
|
HttpCreateDrainJobResponse parsed;
|
|
struct_json::from_json(parsed, response.value());
|
|
return parsed;
|
|
}
|
|
|
|
tl::expected<HttpQueryDrainJobResponse, int> QueryDrainJobViaHttp(
|
|
const std::string& job_id) {
|
|
auto response = httpGet(master_.http_metrics_base() +
|
|
"/api/v1/drain_jobs/query?job_id=" + job_id);
|
|
if (!response.has_value()) {
|
|
return tl::unexpected(response.error());
|
|
}
|
|
|
|
HttpQueryDrainJobResponse parsed;
|
|
struct_json::from_json(parsed, response.value());
|
|
return parsed;
|
|
}
|
|
|
|
tl::expected<HttpSegmentStatusResponse, int> QuerySegmentStatusViaHttp(
|
|
const std::string& segment_name) {
|
|
auto response =
|
|
httpGet(master_.http_metrics_base() +
|
|
"/api/v1/segments/status?segment=" + segment_name);
|
|
if (!response.has_value()) {
|
|
return tl::unexpected(response.error());
|
|
}
|
|
|
|
HttpSegmentStatusResponse parsed;
|
|
struct_json::from_json(parsed, response.value());
|
|
return parsed;
|
|
}
|
|
|
|
bool WaitForJobCompletionViaHttp(
|
|
const std::string& job_id, HttpQueryDrainJobResponse* final_job,
|
|
std::chrono::seconds timeout = std::chrono::seconds(30)) {
|
|
auto start = std::chrono::steady_clock::now();
|
|
while (std::chrono::steady_clock::now() - start < timeout) {
|
|
auto query_result = QueryDrainJobViaHttp(job_id);
|
|
if (query_result.has_value()) {
|
|
if (query_result->status_name == "SUCCEEDED" ||
|
|
query_result->status_name == "FAILED" ||
|
|
query_result->status_name == "CANCELED") {
|
|
if (final_job != nullptr) {
|
|
*final_job = query_result.value();
|
|
}
|
|
return query_result->status_name == "SUCCEEDED";
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Get segment name from a query result
|
|
// Note: segment name is typically the client's hostname (e.g.,
|
|
// "127.0.0.1:18001")
|
|
std::string GetSegmentNameFromQuery(const QueryResult& query_result) {
|
|
if (query_result.replicas.empty()) {
|
|
return "";
|
|
}
|
|
|
|
const auto& replica = query_result.replicas[0];
|
|
if (replica.is_memory_replica()) {
|
|
// Try to get segment name from the replica descriptor
|
|
// For memory replicas, the segment name is stored in the buffer
|
|
// descriptor but transport_endpoint_ might not match segment name
|
|
// We need to extract it from the replica's segment information
|
|
const auto& mem_desc = replica.get_memory_descriptor();
|
|
// The segment name is typically embedded in the buffer descriptor
|
|
// For now, use transport_endpoint_ as a fallback, but we should
|
|
// verify it matches the actual segment name used during mount
|
|
return mem_desc.buffer_descriptor.transport_endpoint_;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
protected:
|
|
static InProcMaster master_;
|
|
static std::string master_address_;
|
|
static std::string metadata_url_;
|
|
|
|
std::shared_ptr<Client> client1_;
|
|
std::shared_ptr<Client> client2_;
|
|
std::unique_ptr<MasterClient> master_client_;
|
|
|
|
UUID client1_id_;
|
|
UUID client2_id_;
|
|
|
|
void* client1_segment_ptr_ = nullptr;
|
|
void* client2_segment_ptr_ = nullptr;
|
|
};
|
|
|
|
InProcMaster TaskExecutorIntegrationTest::master_;
|
|
std::string TaskExecutorIntegrationTest::master_address_;
|
|
std::string TaskExecutorIntegrationTest::metadata_url_;
|
|
|
|
// Test complete replica copy flow
|
|
TEST_F(TaskExecutorIntegrationTest, ReplicaCopyCompleteFlow) {
|
|
// Step 1: Put data on client1
|
|
std::string test_key =
|
|
"test_copy_key_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string test_data =
|
|
"This is test data for replica copy operation. "
|
|
"It should be copied from client1 to client2 segment.";
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(test_data.data(), test_data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1; // Start with 1 replica on client1
|
|
|
|
auto put_result = client1_->Put(test_key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value())
|
|
<< "Failed to put data: " << toString(put_result.error());
|
|
|
|
// Wait a bit for put to complete
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
|
|
// Step 2: Verify that client1's data was put successfully and get actual
|
|
// segment name
|
|
auto client1_query_result = client1_->Query(test_key);
|
|
ASSERT_TRUE(client1_query_result.has_value())
|
|
<< "Failed to query key for verification";
|
|
ASSERT_FALSE(client1_query_result.value().replicas.empty())
|
|
<< "No replicas found after Put";
|
|
|
|
// Extract actual segment name from replica
|
|
std::string source_segment;
|
|
const auto& replica = client1_query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica()) << "Expected memory replica";
|
|
source_segment =
|
|
replica.get_memory_descriptor().buffer_descriptor.transport_endpoint_;
|
|
ASSERT_FALSE(source_segment.empty())
|
|
<< "Failed to extract source segment name";
|
|
|
|
// Step 3: Determine target segment dynamically (must be different from
|
|
// source) Choose the other client's segment as target to ensure source !=
|
|
// target
|
|
std::string target_segment;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segment = "127.0.0.1:18002";
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segment = "127.0.0.1:18001";
|
|
} else {
|
|
// If source is neither, default to client2's segment
|
|
target_segment = "127.0.0.1:18002";
|
|
}
|
|
|
|
// Verify source and target are different - this test only covers
|
|
// inconsistent cases
|
|
ASSERT_NE(source_segment, target_segment)
|
|
<< "Source and target segments must be different for this test. "
|
|
<< "Source: " << source_segment << ", Target: " << target_segment;
|
|
|
|
// Step 4: Create copy task via master
|
|
std::vector<std::string> targets = {target_segment};
|
|
auto copy_result = master_client_->CreateCopyTask(test_key, targets);
|
|
ASSERT_TRUE(copy_result.has_value())
|
|
<< "Failed to create copy task: " << toString(copy_result.error());
|
|
|
|
UUID task_id = copy_result.value();
|
|
|
|
// Step 5: Get the actual client_id from the task assignment
|
|
client1_id_ = GetClientIdFromTask(task_id);
|
|
|
|
// Step 6: Wait for task to be fetched and executed by the assigned client
|
|
bool task_completed =
|
|
WaitForTaskCompletion(task_id, std::chrono::seconds(30));
|
|
ASSERT_TRUE(task_completed) << "Task did not complete within timeout";
|
|
|
|
// Step 7: Verify copy was successful by querying from client2
|
|
auto client2_query_result = client2_->Query(test_key);
|
|
ASSERT_TRUE(client2_query_result.has_value())
|
|
<< "Failed to query copied key";
|
|
ASSERT_FALSE(client2_query_result.value().replicas.empty())
|
|
<< "No replicas found for copied key";
|
|
|
|
// Step 8: Verify data integrity by getting data from client2
|
|
std::vector<uint8_t> read_buffer(test_data.size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = client2_->Get(test_key, read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get data from client2: " << toString(get_result.error());
|
|
|
|
std::string read_data(reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data.size());
|
|
ASSERT_EQ(read_data, test_data) << "Data mismatch after copy";
|
|
}
|
|
|
|
// Test complete replica move flow
|
|
TEST_F(TaskExecutorIntegrationTest, ReplicaMoveCompleteFlow) {
|
|
// Step 1: Put data on client1
|
|
std::string test_key =
|
|
"test_move_key_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string test_data =
|
|
"This is test data for replica move operation. "
|
|
"It should be moved from client1 to client2 segment.";
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(test_data.data(), test_data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1; // Start with 1 replica on client1
|
|
|
|
auto put_result = client1_->Put(test_key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value())
|
|
<< "Failed to put data: " << toString(put_result.error());
|
|
|
|
// Wait a bit for put to complete
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
|
|
// Step 2: Verify that client1's data was put successfully and get actual
|
|
// segment name
|
|
auto query_result = client1_->Query(test_key);
|
|
ASSERT_TRUE(query_result.has_value())
|
|
<< "Failed to query key for verification";
|
|
ASSERT_FALSE(query_result.value().replicas.empty())
|
|
<< "No replicas found after Put";
|
|
|
|
// Extract actual segment name from replica
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica()) << "Expected memory replica";
|
|
source_segment =
|
|
replica.get_memory_descriptor().buffer_descriptor.transport_endpoint_;
|
|
ASSERT_FALSE(source_segment.empty())
|
|
<< "Failed to extract source segment name";
|
|
|
|
// Step 3: Determine target segment dynamically (must be different from
|
|
// source) Choose the other client's segment as target to ensure source !=
|
|
// target
|
|
std::string target_segment;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segment = "127.0.0.1:18002";
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segment = "127.0.0.1:18001";
|
|
} else {
|
|
// If source is neither, default to client2's segment
|
|
target_segment = "127.0.0.1:18002";
|
|
}
|
|
|
|
// Verify source and target are different - this test only covers
|
|
// inconsistent cases
|
|
ASSERT_NE(source_segment, target_segment)
|
|
<< "Source and target segments must be different for this test. "
|
|
<< "Source: " << source_segment << ", Target: " << target_segment;
|
|
|
|
// Step 4: Create move task via master
|
|
auto move_result = master_client_->CreateMoveTask(test_key, source_segment,
|
|
target_segment);
|
|
ASSERT_TRUE(move_result.has_value())
|
|
<< "Failed to create move task: " << toString(move_result.error());
|
|
|
|
UUID task_id = move_result.value();
|
|
|
|
// Step 5: Get the actual client_id from the task assignment
|
|
client1_id_ = GetClientIdFromTask(task_id);
|
|
|
|
// Step 6: Wait for task to be fetched and executed by the assigned client
|
|
bool task_completed =
|
|
WaitForTaskCompletion(task_id, std::chrono::seconds(30));
|
|
ASSERT_TRUE(task_completed) << "Task did not complete within timeout";
|
|
|
|
// Step 7: Verify move was successful
|
|
// The replica should now be on client2, not client1
|
|
auto query_result_client2 = client2_->Query(test_key);
|
|
ASSERT_TRUE(query_result_client2.has_value())
|
|
<< "Failed to query moved key from client2";
|
|
ASSERT_FALSE(query_result_client2.value().replicas.empty())
|
|
<< "No replicas found on client2 after move";
|
|
|
|
// Step 8: Verify data integrity
|
|
std::vector<uint8_t> read_buffer(test_data.size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = client2_->Get(test_key, read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get data from client2: " << toString(get_result.error());
|
|
|
|
std::string read_data(reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data.size());
|
|
ASSERT_EQ(read_data, test_data) << "Data mismatch after move";
|
|
}
|
|
|
|
// Test copy to multiple target segments
|
|
TEST_F(TaskExecutorIntegrationTest, DrainJobCompleteFlow) {
|
|
const std::string source_segment = "127.0.0.1:18001";
|
|
const std::string target_segment = "127.0.0.1:18002";
|
|
const auto key_prefix =
|
|
"test_drain_job_key_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
|
|
const auto make_payload = [](const std::string& prefix, size_t payload_size,
|
|
char fill) {
|
|
std::string payload = prefix;
|
|
if (payload.size() < payload_size) {
|
|
payload.resize(payload_size, fill);
|
|
}
|
|
return payload;
|
|
};
|
|
|
|
std::vector<std::pair<std::string, std::string>> preload_items;
|
|
std::vector<std::pair<std::string, std::string>> redirected_items;
|
|
for (int i = 0; i < 12; ++i) {
|
|
preload_items.emplace_back(
|
|
key_prefix + "_preload_" + std::to_string(i),
|
|
make_payload("preload-" + std::to_string(i) + "-", 128 * 1024,
|
|
static_cast<char>('a' + (i % 26))));
|
|
}
|
|
for (int i = 0; i < 4; ++i) {
|
|
redirected_items.emplace_back(
|
|
key_prefix + "_redirect_" + std::to_string(i),
|
|
make_payload("redirect-" + std::to_string(i) + "-", 32 * 1024,
|
|
static_cast<char>('A' + (i % 26))));
|
|
}
|
|
|
|
const auto put_to_preferred_segment =
|
|
[&](const std::string& key, const std::string& value,
|
|
const std::string& preferred_segment) {
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(const_cast<char*>(value.data()), value.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
config.preferred_segment = preferred_segment;
|
|
|
|
auto put_result = client1_->Put(key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value())
|
|
<< "Failed to put key=" << key << ": "
|
|
<< toString(put_result.error());
|
|
};
|
|
|
|
const auto query_segments =
|
|
[&](const std::string& key) -> std::vector<std::string> {
|
|
auto query_result = client2_->Query(key);
|
|
if (!query_result.has_value()) {
|
|
ADD_FAILURE() << "Failed to query key=" << key << ": "
|
|
<< toString(query_result.error());
|
|
return {};
|
|
}
|
|
std::vector<std::string> segments;
|
|
for (const auto& replica : query_result->replicas) {
|
|
if (!replica.is_memory_replica()) {
|
|
ADD_FAILURE() << "Expected memory replica for key=" << key;
|
|
return {};
|
|
}
|
|
segments.push_back(replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_);
|
|
}
|
|
return segments;
|
|
};
|
|
|
|
const auto wait_for_segments = [&](const std::string& key,
|
|
const std::string& required_segment,
|
|
const std::string& forbidden_segment) {
|
|
auto deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
|
std::vector<std::string> last_segments;
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
last_segments = query_segments(key);
|
|
bool has_required = false;
|
|
bool has_forbidden = false;
|
|
for (const auto& segment : last_segments) {
|
|
if (segment == required_segment) {
|
|
has_required = true;
|
|
}
|
|
if (!forbidden_segment.empty() &&
|
|
segment == forbidden_segment) {
|
|
has_forbidden = true;
|
|
}
|
|
}
|
|
if (has_required && !has_forbidden) {
|
|
return;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
FAIL() << "Replica placement mismatch for key=" << key
|
|
<< ", required_segment=" << required_segment
|
|
<< ", forbidden_segment=" << forbidden_segment << ", last_seen="
|
|
<< (last_segments.empty() ? std::string("<empty>")
|
|
: last_segments.front());
|
|
};
|
|
|
|
const auto assert_key_data = [&](const std::string& key,
|
|
const std::string& expected_value) {
|
|
std::vector<uint8_t> read_buffer(expected_value.size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = client2_->Get(key, read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get key=" << key << ": "
|
|
<< toString(get_result.error());
|
|
|
|
std::string read_data(reinterpret_cast<const char*>(read_buffer.data()),
|
|
expected_value.size());
|
|
ASSERT_EQ(read_data, expected_value) << "Data mismatch for key=" << key;
|
|
};
|
|
|
|
size_t expected_migrated_bytes = 0;
|
|
for (const auto& [key, value] : preload_items) {
|
|
put_to_preferred_segment(key, value, source_segment);
|
|
wait_for_segments(key, source_segment, "");
|
|
expected_migrated_bytes += value.size();
|
|
}
|
|
|
|
CreateDrainJobRequest request;
|
|
request.segments = {source_segment};
|
|
request.target_segments = {target_segment};
|
|
request.max_concurrency = 1;
|
|
|
|
auto create_job_result = CreateDrainJobViaHttp(request);
|
|
ASSERT_TRUE(create_job_result.has_value())
|
|
<< "Failed to create drain job over HTTP, status="
|
|
<< create_job_result.error();
|
|
ASSERT_TRUE(create_job_result->success)
|
|
<< "Drain job create returned error: "
|
|
<< create_job_result->error_message;
|
|
EXPECT_EQ(create_job_result->status, "CREATED");
|
|
|
|
auto draining_status = QuerySegmentStatusViaHttp(source_segment);
|
|
ASSERT_TRUE(draining_status.has_value())
|
|
<< "Failed to query segment status over HTTP, status="
|
|
<< draining_status.error();
|
|
EXPECT_TRUE(draining_status->success);
|
|
EXPECT_EQ(draining_status->status_name, "DRAINING");
|
|
|
|
for (const auto& [key, value] : redirected_items) {
|
|
put_to_preferred_segment(key, value, source_segment);
|
|
wait_for_segments(key, target_segment, source_segment);
|
|
}
|
|
|
|
HttpQueryDrainJobResponse final_job;
|
|
ASSERT_TRUE(
|
|
WaitForJobCompletionViaHttp(create_job_result->job_id, &final_job))
|
|
<< "Drain job did not complete within timeout";
|
|
|
|
EXPECT_EQ(final_job.status_name, "SUCCEEDED");
|
|
EXPECT_EQ(final_job.active_units, 0u);
|
|
EXPECT_EQ(final_job.failed_units, 0u);
|
|
EXPECT_GE(final_job.succeeded_units, preload_items.size());
|
|
EXPECT_GE(final_job.migrated_bytes, expected_migrated_bytes);
|
|
|
|
auto drained_status = QuerySegmentStatusViaHttp(source_segment);
|
|
ASSERT_TRUE(drained_status.has_value())
|
|
<< "Failed to query drained segment status over HTTP, status="
|
|
<< drained_status.error();
|
|
EXPECT_TRUE(drained_status->success);
|
|
EXPECT_EQ(drained_status->status_name, "DRAINED");
|
|
|
|
for (const auto& [key, value] : preload_items) {
|
|
wait_for_segments(key, target_segment, source_segment);
|
|
assert_key_data(key, value);
|
|
}
|
|
for (const auto& [key, value] : redirected_items) {
|
|
wait_for_segments(key, target_segment, source_segment);
|
|
assert_key_data(key, value);
|
|
}
|
|
|
|
client1_.reset();
|
|
if (client1_segment_ptr_ != nullptr) {
|
|
free(client1_segment_ptr_);
|
|
client1_segment_ptr_ = nullptr;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
|
|
for (const auto& [key, value] : preload_items) {
|
|
wait_for_segments(key, target_segment, source_segment);
|
|
assert_key_data(key, value);
|
|
}
|
|
for (const auto& [key, value] : redirected_items) {
|
|
wait_for_segments(key, target_segment, source_segment);
|
|
assert_key_data(key, value);
|
|
}
|
|
}
|
|
|
|
TEST_F(TaskExecutorIntegrationTest, ReplicaCopyToMultipleTargets) {
|
|
// Step 1: Put data on client1
|
|
std::string test_key =
|
|
"test_multi_target_copy_key_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string test_data =
|
|
"This is test data for multiple target copy operation.";
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(test_data.data(), test_data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
auto put_result = client1_->Put(test_key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value())
|
|
<< "Failed to put data: " << toString(put_result.error());
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
|
|
// Step 2: Get actual source segment
|
|
auto query_result = client1_->Query(test_key);
|
|
ASSERT_TRUE(query_result.has_value())
|
|
<< "Failed to query key for verification";
|
|
ASSERT_FALSE(query_result.value().replicas.empty())
|
|
<< "No replicas found after Put";
|
|
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica()) << "Expected memory replica";
|
|
source_segment =
|
|
replica.get_memory_descriptor().buffer_descriptor.transport_endpoint_;
|
|
ASSERT_FALSE(source_segment.empty())
|
|
<< "Failed to extract source segment name";
|
|
|
|
// Step 3: Determine target segments (both client1 and client2, excluding
|
|
// source)
|
|
std::vector<std::string> target_segments;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segments = {"127.0.0.1:18002"}; // Copy to client2 only
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segments = {"127.0.0.1:18001"}; // Copy to client1 only
|
|
} else {
|
|
// If source is neither, use both segments
|
|
target_segments = {"127.0.0.1:18001", "127.0.0.1:18002"};
|
|
}
|
|
|
|
// Ensure source is not in targets
|
|
for (const auto& target : target_segments) {
|
|
ASSERT_NE(source_segment, target)
|
|
<< "Source segment must not be in target segments. "
|
|
<< "Source: " << source_segment << ", Target: " << target;
|
|
}
|
|
|
|
// Step 4: Create copy task with multiple targets
|
|
auto copy_result =
|
|
master_client_->CreateCopyTask(test_key, target_segments);
|
|
ASSERT_TRUE(copy_result.has_value())
|
|
<< "Failed to create copy task: " << toString(copy_result.error());
|
|
|
|
UUID task_id = copy_result.value();
|
|
|
|
// Step 5: Wait for task to complete
|
|
bool task_completed =
|
|
WaitForTaskCompletion(task_id, std::chrono::seconds(30));
|
|
ASSERT_TRUE(task_completed) << "Task did not complete within timeout";
|
|
|
|
// Step 6: Verify copy was successful on all target segments
|
|
for (const auto& target_segment : target_segments) {
|
|
std::shared_ptr<Client> target_client;
|
|
if (target_segment == "127.0.0.1:18001") {
|
|
target_client = client1_;
|
|
} else {
|
|
target_client = client2_;
|
|
}
|
|
|
|
auto target_query_result = target_client->Query(test_key);
|
|
ASSERT_TRUE(target_query_result.has_value())
|
|
<< "Failed to query copied key from target segment: "
|
|
<< target_segment;
|
|
ASSERT_FALSE(target_query_result.value().replicas.empty())
|
|
<< "No replicas found on target segment: " << target_segment;
|
|
|
|
// Verify data integrity
|
|
std::vector<uint8_t> read_buffer(test_data.size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = target_client->Get(test_key, read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get data from target segment: " << target_segment;
|
|
|
|
std::string read_data(reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data.size());
|
|
ASSERT_EQ(read_data, test_data)
|
|
<< "Data mismatch on target segment: " << target_segment;
|
|
}
|
|
}
|
|
|
|
// Test multiple copy tasks
|
|
TEST_F(TaskExecutorIntegrationTest, MultipleCopyTasks) {
|
|
const int num_keys = 3;
|
|
std::vector<std::string> keys;
|
|
std::vector<std::string> test_data_list;
|
|
std::vector<UUID> task_ids;
|
|
|
|
// Step 1: Put multiple keys on client1
|
|
for (int i = 0; i < num_keys; ++i) {
|
|
std::string key =
|
|
"test_multi_copy_key_" + std::to_string(i) + "_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string data = "Test data for key " + std::to_string(i);
|
|
keys.push_back(key);
|
|
test_data_list.push_back(data);
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(data.data(), data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
auto put_result = client1_->Put(key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value()) << "Failed to put key " << i;
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
|
|
// Step 2: For each key, determine its source segment and create copy task
|
|
// with appropriate target Each key may be on a different segment, so we
|
|
// need to check each one individually
|
|
for (const auto& key : keys) {
|
|
// Query to get actual source segment for this key
|
|
auto query_result = client1_->Query(key);
|
|
ASSERT_TRUE(query_result.has_value()) << "Failed to query key: " << key;
|
|
ASSERT_FALSE(query_result.value().replicas.empty())
|
|
<< "No replicas found for key: " << key;
|
|
|
|
// Extract actual source segment name from replica
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica())
|
|
<< "Expected memory replica for key: " << key;
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
ASSERT_FALSE(source_segment.empty())
|
|
<< "Failed to extract source segment name for key: " << key;
|
|
|
|
// Determine target segment dynamically (must be different from source)
|
|
// Choose the other client's segment as target to ensure source !=
|
|
// target
|
|
std::string target_segment;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segment = "127.0.0.1:18002";
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segment = "127.0.0.1:18001";
|
|
} else {
|
|
// If source is neither, default to client2's segment
|
|
target_segment = "127.0.0.1:18002";
|
|
}
|
|
|
|
// Verify source and target are different - this test only covers
|
|
// inconsistent cases
|
|
ASSERT_NE(source_segment, target_segment)
|
|
<< "Source and target segments must be different for key: " << key
|
|
<< ". "
|
|
<< "Source: " << source_segment << ", Target: " << target_segment;
|
|
|
|
// Step 3: Create copy task for this key
|
|
// For the last key, use multiple target segments to test multi-target
|
|
// copy
|
|
std::vector<std::string> targets;
|
|
size_t key_index =
|
|
task_ids.size(); // Current index before adding this task
|
|
if (key_index == keys.size() - 1 && keys.size() >= 2) {
|
|
// Last key: use multiple targets if we have at least 2 clients
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
targets = {
|
|
"127.0.0.1:18002"}; // Only one other segment available
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
targets = {
|
|
"127.0.0.1:18001"}; // Only one other segment available
|
|
} else {
|
|
targets = {"127.0.0.1:18001", "127.0.0.1:18002"};
|
|
}
|
|
} else {
|
|
// Other keys: use single target
|
|
targets = {target_segment};
|
|
}
|
|
|
|
auto copy_result = master_client_->CreateCopyTask(key, targets);
|
|
ASSERT_TRUE(copy_result.has_value())
|
|
<< "Failed to create copy task for " << key;
|
|
task_ids.push_back(copy_result.value());
|
|
}
|
|
|
|
// Step 4: Wait for all tasks to complete
|
|
for (size_t i = 0; i < task_ids.size(); ++i) {
|
|
bool completed =
|
|
WaitForTaskCompletion(task_ids[i], std::chrono::seconds(30));
|
|
if (!completed) {
|
|
// Query task to get error message
|
|
auto query_task = master_client_->QueryTask(task_ids[i]);
|
|
if (query_task.has_value()) {
|
|
LOG(ERROR) << "Task " << i
|
|
<< " failed: " << query_task.value().message;
|
|
}
|
|
}
|
|
ASSERT_TRUE(completed)
|
|
<< "Task " << i << " (key: " << keys[i] << ") did not complete";
|
|
}
|
|
|
|
// Step 5: Verify all keys are accessible on their target segments
|
|
// For each key, we need to determine which client should have the replica
|
|
for (size_t i = 0; i < keys.size(); ++i) {
|
|
// Determine source segment for this key
|
|
auto query_result = client1_->Query(keys[i]);
|
|
ASSERT_TRUE(query_result.has_value()) << "Failed to query key " << i;
|
|
ASSERT_FALSE(query_result.value().replicas.empty())
|
|
<< "No replicas found for key " << i;
|
|
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica())
|
|
<< "Expected memory replica for key " << i;
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
|
|
// For the last key with multiple targets, verify on all target segments
|
|
bool is_multi_target = (i == keys.size() - 1 && keys.size() >= 2);
|
|
|
|
if (is_multi_target) {
|
|
// Last key: verify on all target segments
|
|
std::vector<std::string> target_segments;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segments = {"127.0.0.1:18002"};
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segments = {"127.0.0.1:18001"};
|
|
} else {
|
|
target_segments = {"127.0.0.1:18001", "127.0.0.1:18002"};
|
|
}
|
|
|
|
// Verify on each target segment
|
|
for (const auto& target_seg : target_segments) {
|
|
std::shared_ptr<Client> target_client =
|
|
(target_seg == "127.0.0.1:18001") ? client1_ : client2_;
|
|
|
|
auto target_query = target_client->Query(keys[i]);
|
|
ASSERT_TRUE(target_query.has_value())
|
|
<< "Failed to query key " << i
|
|
<< " from target segment: " << target_seg;
|
|
ASSERT_FALSE(target_query.value().replicas.empty())
|
|
<< "No replicas found for key " << i
|
|
<< " on segment: " << target_seg;
|
|
|
|
// Verify data integrity
|
|
std::vector<uint8_t> read_buffer(test_data_list[i].size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(),
|
|
read_buffer.size());
|
|
|
|
auto get_result = target_client->Get(keys[i], read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get key " << i
|
|
<< " from target segment: " << target_seg;
|
|
|
|
std::string read_data(
|
|
reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data_list[i].size());
|
|
ASSERT_EQ(read_data, test_data_list[i])
|
|
<< "Data mismatch for key " << i
|
|
<< " on segment: " << target_seg;
|
|
}
|
|
} else {
|
|
// Other keys: verify on single target segment
|
|
std::shared_ptr<Client> target_client;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_client = client2_; // Copy to client2
|
|
} else {
|
|
target_client = client1_; // Copy to client1
|
|
}
|
|
|
|
// Verify the copied replica is accessible
|
|
auto query_result_target = target_client->Query(keys[i]);
|
|
ASSERT_TRUE(query_result_target.has_value())
|
|
<< "Failed to query copied key " << i << " from target client";
|
|
ASSERT_FALSE(query_result_target.value().replicas.empty())
|
|
<< "No replicas found for copied key " << i;
|
|
|
|
// Verify data integrity
|
|
std::vector<uint8_t> read_buffer(test_data_list[i].size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = target_client->Get(keys[i], read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get key " << i << " from target client";
|
|
|
|
std::string read_data(
|
|
reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data_list[i].size());
|
|
ASSERT_EQ(read_data, test_data_list[i])
|
|
<< "Data mismatch for key " << i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test multiple move tasks
|
|
TEST_F(TaskExecutorIntegrationTest, MultipleMoveTasks) {
|
|
const int num_keys = 3;
|
|
std::vector<std::string> keys;
|
|
std::vector<std::string> test_data_list;
|
|
std::vector<UUID> task_ids;
|
|
|
|
// Step 1: Put multiple keys on client1
|
|
for (int i = 0; i < num_keys; ++i) {
|
|
std::string key =
|
|
"test_multi_move_key_" + std::to_string(i) + "_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string data = "Test data for move key " + std::to_string(i);
|
|
keys.push_back(key);
|
|
test_data_list.push_back(data);
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(data.data(), data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
auto put_result = client1_->Put(key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value()) << "Failed to put key " << i;
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
|
|
// Step 2: For each key, determine source segment and create move task
|
|
for (const auto& key : keys) {
|
|
// Query to get actual source segment for this key
|
|
auto query_result = client1_->Query(key);
|
|
ASSERT_TRUE(query_result.has_value()) << "Failed to query key: " << key;
|
|
ASSERT_FALSE(query_result.value().replicas.empty())
|
|
<< "No replicas found for key: " << key;
|
|
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica())
|
|
<< "Expected memory replica for key: " << key;
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
ASSERT_FALSE(source_segment.empty())
|
|
<< "Failed to extract source segment name for key: " << key;
|
|
|
|
// Determine target segment (must be different from source)
|
|
std::string target_segment;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segment = "127.0.0.1:18002";
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segment = "127.0.0.1:18001";
|
|
} else {
|
|
target_segment = "127.0.0.1:18002";
|
|
}
|
|
|
|
ASSERT_NE(source_segment, target_segment)
|
|
<< "Source and target segments must be different for key: " << key
|
|
<< ". "
|
|
<< "Source: " << source_segment << ", Target: " << target_segment;
|
|
|
|
// Step 3: Create move task
|
|
auto move_result =
|
|
master_client_->CreateMoveTask(key, source_segment, target_segment);
|
|
ASSERT_TRUE(move_result.has_value())
|
|
<< "Failed to create move task for " << key;
|
|
task_ids.push_back(move_result.value());
|
|
}
|
|
|
|
// Step 4: Wait for all tasks to complete
|
|
for (size_t i = 0; i < task_ids.size(); ++i) {
|
|
bool completed =
|
|
WaitForTaskCompletion(task_ids[i], std::chrono::seconds(30));
|
|
if (!completed) {
|
|
auto query_task = master_client_->QueryTask(task_ids[i]);
|
|
if (query_task.has_value()) {
|
|
LOG(ERROR) << "Task " << i
|
|
<< " failed: " << query_task.value().message;
|
|
}
|
|
}
|
|
ASSERT_TRUE(completed)
|
|
<< "Task " << i << " (key: " << keys[i] << ") did not complete";
|
|
}
|
|
|
|
// Step 5: Verify all keys are moved to target segments
|
|
for (size_t i = 0; i < keys.size(); ++i) {
|
|
// Determine target client for this key
|
|
auto query_result = client1_->Query(keys[i]);
|
|
ASSERT_TRUE(query_result.has_value()) << "Failed to query key " << i;
|
|
|
|
std::string source_segment;
|
|
if (!query_result.value().replicas.empty()) {
|
|
const auto& replica = query_result.value().replicas[0];
|
|
if (replica.is_memory_replica()) {
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Client> target_client;
|
|
if (source_segment == "127.0.0.1:18001" || source_segment.empty()) {
|
|
// Check client2 (may have moved from client1 or was originally on
|
|
// client1)
|
|
target_client = client2_;
|
|
} else {
|
|
target_client = client1_;
|
|
}
|
|
|
|
// Verify the moved replica is accessible on target client
|
|
auto target_query_result = target_client->Query(keys[i]);
|
|
ASSERT_TRUE(target_query_result.has_value())
|
|
<< "Failed to query moved key " << i << " from target client";
|
|
ASSERT_FALSE(target_query_result.value().replicas.empty())
|
|
<< "No replicas found for moved key " << i;
|
|
|
|
// Verify data integrity
|
|
std::vector<uint8_t> read_buffer(test_data_list[i].size());
|
|
std::vector<Slice> read_slices;
|
|
read_slices.emplace_back(read_buffer.data(), read_buffer.size());
|
|
|
|
auto get_result = target_client->Get(keys[i], read_slices);
|
|
ASSERT_TRUE(get_result.has_value())
|
|
<< "Failed to get moved key " << i << " from target client";
|
|
|
|
std::string read_data(reinterpret_cast<const char*>(read_buffer.data()),
|
|
test_data_list[i].size());
|
|
ASSERT_EQ(read_data, test_data_list[i])
|
|
<< "Data mismatch for moved key " << i;
|
|
}
|
|
}
|
|
|
|
// Test concurrent copy and move operations
|
|
TEST_F(TaskExecutorIntegrationTest, ConcurrentCopyAndMoveOperations) {
|
|
const int num_copy_keys = 2;
|
|
const int num_move_keys = 2;
|
|
std::vector<std::string> copy_keys, move_keys;
|
|
std::vector<std::string> copy_data_list, move_data_list;
|
|
std::vector<UUID> copy_task_ids, move_task_ids;
|
|
|
|
// Step 1: Put keys for copy operations
|
|
for (int i = 0; i < num_copy_keys; ++i) {
|
|
std::string key =
|
|
"test_concurrent_copy_key_" + std::to_string(i) + "_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string data = "Copy data " + std::to_string(i);
|
|
copy_keys.push_back(key);
|
|
copy_data_list.push_back(data);
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(data.data(), data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
auto put_result = client1_->Put(key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value()) << "Failed to put copy key " << i;
|
|
}
|
|
|
|
// Step 2: Put keys for move operations
|
|
for (int i = 0; i < num_move_keys; ++i) {
|
|
std::string key =
|
|
"test_concurrent_move_key_" + std::to_string(i) + "_" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
std::string data = "Move data " + std::to_string(i);
|
|
move_keys.push_back(key);
|
|
move_data_list.push_back(data);
|
|
|
|
std::vector<Slice> slices;
|
|
slices.emplace_back(data.data(), data.size());
|
|
ReplicateConfig config;
|
|
config.replica_num = 1;
|
|
|
|
auto put_result = client1_->Put(key, slices, config);
|
|
ASSERT_TRUE(put_result.has_value()) << "Failed to put move key " << i;
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
|
|
// Step 3: Create copy tasks concurrently
|
|
// Use multiple targets for the first copy task to test multi-target copy
|
|
for (size_t idx = 0; idx < copy_keys.size(); ++idx) {
|
|
const auto& key = copy_keys[idx];
|
|
auto query_result = client1_->Query(key);
|
|
ASSERT_TRUE(query_result.has_value())
|
|
<< "Failed to query copy key: " << key;
|
|
ASSERT_FALSE(query_result.value().replicas.empty());
|
|
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica());
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
|
|
std::vector<std::string> targets;
|
|
if (idx == 0 && copy_keys.size() >= 1) {
|
|
// First copy task: use multiple targets
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
targets = {
|
|
"127.0.0.1:18002"}; // Only one other segment available
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
targets = {
|
|
"127.0.0.1:18001"}; // Only one other segment available
|
|
} else {
|
|
targets = {"127.0.0.1:18001", "127.0.0.1:18002"};
|
|
}
|
|
} else {
|
|
// Other copy tasks: use single target
|
|
std::string target_segment = (source_segment == "127.0.0.1:18001")
|
|
? "127.0.0.1:18002"
|
|
: "127.0.0.1:18001";
|
|
targets = {target_segment};
|
|
}
|
|
|
|
auto copy_result = master_client_->CreateCopyTask(key, targets);
|
|
ASSERT_TRUE(copy_result.has_value())
|
|
<< "Failed to create copy task for " << key;
|
|
copy_task_ids.push_back(copy_result.value());
|
|
}
|
|
|
|
// Step 4: Create move tasks concurrently
|
|
for (const auto& key : move_keys) {
|
|
auto query_result = client1_->Query(key);
|
|
ASSERT_TRUE(query_result.has_value())
|
|
<< "Failed to query move key: " << key;
|
|
ASSERT_FALSE(query_result.value().replicas.empty());
|
|
|
|
std::string source_segment;
|
|
const auto& replica = query_result.value().replicas[0];
|
|
ASSERT_TRUE(replica.is_memory_replica());
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
|
|
std::string target_segment = (source_segment == "127.0.0.1:18001")
|
|
? "127.0.0.1:18002"
|
|
: "127.0.0.1:18001";
|
|
|
|
auto move_result =
|
|
master_client_->CreateMoveTask(key, source_segment, target_segment);
|
|
ASSERT_TRUE(move_result.has_value())
|
|
<< "Failed to create move task for " << key;
|
|
move_task_ids.push_back(move_result.value());
|
|
}
|
|
|
|
// Step 5: Wait for all tasks to complete
|
|
for (size_t i = 0; i < copy_task_ids.size(); ++i) {
|
|
bool completed =
|
|
WaitForTaskCompletion(copy_task_ids[i], std::chrono::seconds(30));
|
|
ASSERT_TRUE(completed) << "Copy task " << i << " (key: " << copy_keys[i]
|
|
<< ") did not complete";
|
|
}
|
|
|
|
for (size_t i = 0; i < move_task_ids.size(); ++i) {
|
|
bool completed =
|
|
WaitForTaskCompletion(move_task_ids[i], std::chrono::seconds(30));
|
|
ASSERT_TRUE(completed) << "Move task " << i << " (key: " << move_keys[i]
|
|
<< ") did not complete";
|
|
}
|
|
|
|
// Step 6: Verify copy results
|
|
for (size_t i = 0; i < copy_keys.size(); ++i) {
|
|
auto query_result = client1_->Query(copy_keys[i]);
|
|
ASSERT_TRUE(query_result.has_value());
|
|
|
|
std::string source_segment;
|
|
if (!query_result.value().replicas.empty()) {
|
|
const auto& replica = query_result.value().replicas[0];
|
|
if (replica.is_memory_replica()) {
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
}
|
|
}
|
|
|
|
// First copy task uses multiple targets
|
|
bool is_multi_target = (i == 0 && copy_keys.size() >= 1);
|
|
|
|
if (is_multi_target) {
|
|
// Verify on all target segments
|
|
std::vector<std::string> target_segments;
|
|
if (source_segment == "127.0.0.1:18001") {
|
|
target_segments = {"127.0.0.1:18002"};
|
|
} else if (source_segment == "127.0.0.1:18002") {
|
|
target_segments = {"127.0.0.1:18001"};
|
|
} else {
|
|
target_segments = {"127.0.0.1:18001", "127.0.0.1:18002"};
|
|
}
|
|
|
|
for (const auto& target_seg : target_segments) {
|
|
std::shared_ptr<Client> target_client =
|
|
(target_seg == "127.0.0.1:18001") ? client1_ : client2_;
|
|
|
|
auto target_query = target_client->Query(copy_keys[i]);
|
|
ASSERT_TRUE(target_query.has_value())
|
|
<< "Copy key " << i
|
|
<< " not found on target segment: " << target_seg;
|
|
ASSERT_FALSE(target_query.value().replicas.empty())
|
|
<< "No replicas for copy key " << i
|
|
<< " on segment: " << target_seg;
|
|
}
|
|
} else {
|
|
// Verify on single target segment
|
|
std::shared_ptr<Client> target_client =
|
|
(source_segment == "127.0.0.1:18001") ? client2_ : client1_;
|
|
|
|
auto target_query = target_client->Query(copy_keys[i]);
|
|
ASSERT_TRUE(target_query.has_value())
|
|
<< "Copy key " << i << " not found on target";
|
|
ASSERT_FALSE(target_query.value().replicas.empty())
|
|
<< "No replicas for copy key " << i;
|
|
}
|
|
}
|
|
|
|
// Step 7: Verify move results
|
|
for (size_t i = 0; i < move_keys.size(); ++i) {
|
|
auto query_result = client1_->Query(move_keys[i]);
|
|
|
|
std::string source_segment;
|
|
if (query_result.has_value() &&
|
|
!query_result.value().replicas.empty()) {
|
|
const auto& replica = query_result.value().replicas[0];
|
|
if (replica.is_memory_replica()) {
|
|
source_segment = replica.get_memory_descriptor()
|
|
.buffer_descriptor.transport_endpoint_;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Client> target_client =
|
|
(source_segment == "127.0.0.1:18001" || source_segment.empty())
|
|
? client2_
|
|
: client1_;
|
|
|
|
auto target_query = target_client->Query(move_keys[i]);
|
|
ASSERT_TRUE(target_query.has_value())
|
|
<< "Move key " << i << " not found on target";
|
|
ASSERT_FALSE(target_query.value().replicas.empty())
|
|
<< "No replicas for move key " << i;
|
|
}
|
|
}
|
|
|
|
} // namespace testing
|
|
} // namespace mooncake
|