485 lines
18 KiB
C++
485 lines
18 KiB
C++
// Copyright 2026 KVCache.AI
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/*
|
|
* RDMA Endpoint Re-establishment Test
|
|
*
|
|
* Purpose:
|
|
* This test verifies that TE correctly handles endpoint re-establish during
|
|
* simulated initiator restarts, and that classic RDMA can recover from a first
|
|
* RTR/EINVAL by reprobeing the next auto-selected local GID.
|
|
*
|
|
* How to run:
|
|
* sudo env MC_METADATA_SERVER=P2PHANDSHAKE \
|
|
* MC_TARGET_SERVER_NAME=127.0.0.1:12345 \
|
|
* MC_INITIATOR_SERVER_NAME=127.0.0.1:12346 \
|
|
* MC_TARGET_DEVICE_NAME=erdma_0 MC_INITIATOR_DEVICE_NAME=erdma_1 \
|
|
* ./build/mooncake-transfer-engine/tests/rdma_endpoint_reestablish_test
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include <gflags/gflags.h>
|
|
#include <glog/logging.h>
|
|
#include <gtest/gtest.h>
|
|
#include <infiniband/verbs.h>
|
|
#include <numa.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "common.h"
|
|
#include "transfer_engine.h"
|
|
#include "transport/transport.h"
|
|
|
|
using namespace mooncake;
|
|
|
|
namespace {
|
|
|
|
constexpr size_t kRAMBufSize = 256ull << 24;
|
|
constexpr size_t kDataLength = 16ull << 24;
|
|
|
|
bool usesP2PHandshake(const std::string& metadata_server) {
|
|
return metadata_server == P2PHANDSHAKE;
|
|
}
|
|
|
|
std::vector<std::string> getAvailableRdmaDevices() {
|
|
int num_devices = 0;
|
|
ibv_device** device_list = ibv_get_device_list(&num_devices);
|
|
std::vector<std::string> devices;
|
|
if (device_list == nullptr) {
|
|
return devices;
|
|
}
|
|
devices.reserve(num_devices);
|
|
for (int i = 0; i < num_devices; ++i) {
|
|
devices.emplace_back(ibv_get_device_name(device_list[i]));
|
|
}
|
|
ibv_free_device_list(device_list);
|
|
return devices;
|
|
}
|
|
|
|
struct RtrFaultInjectionState {
|
|
std::mutex mu;
|
|
bool synthetic_gid_swap_enabled = false;
|
|
std::string synthetic_gid_device;
|
|
bool fail_first_rtr_einval = false;
|
|
std::string fail_rtr_device;
|
|
int injected_failures = 0;
|
|
std::unordered_map<std::string, std::vector<int>> rtr_sgid_history;
|
|
std::unordered_map<std::string, std::vector<std::string>> rtr_gid_history;
|
|
} g_rtr_fault_injection_state;
|
|
|
|
std::string formatGidBytes(const uint8_t* raw) {
|
|
std::ostringstream oss;
|
|
oss << std::hex << std::setfill('0');
|
|
for (size_t i = 0; i < 16; ++i) {
|
|
if (i != 0) {
|
|
oss << ":";
|
|
}
|
|
oss << std::setw(2) << static_cast<int>(raw[i]);
|
|
}
|
|
return oss.str();
|
|
}
|
|
|
|
void resetRtrFaultInjectionState() {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
g_rtr_fault_injection_state.synthetic_gid_swap_enabled = false;
|
|
g_rtr_fault_injection_state.synthetic_gid_device.clear();
|
|
g_rtr_fault_injection_state.fail_first_rtr_einval = false;
|
|
g_rtr_fault_injection_state.fail_rtr_device.clear();
|
|
g_rtr_fault_injection_state.injected_failures = 0;
|
|
g_rtr_fault_injection_state.rtr_sgid_history.clear();
|
|
g_rtr_fault_injection_state.rtr_gid_history.clear();
|
|
}
|
|
|
|
void configureRtrFaultInjection(const std::string& device_name) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
g_rtr_fault_injection_state.synthetic_gid_swap_enabled = true;
|
|
g_rtr_fault_injection_state.synthetic_gid_device = device_name;
|
|
g_rtr_fault_injection_state.fail_first_rtr_einval = true;
|
|
g_rtr_fault_injection_state.fail_rtr_device = device_name;
|
|
g_rtr_fault_injection_state.injected_failures = 0;
|
|
g_rtr_fault_injection_state.rtr_sgid_history.clear();
|
|
g_rtr_fault_injection_state.rtr_gid_history.clear();
|
|
}
|
|
|
|
std::vector<int> getRtrSgidHistory(const std::string& device_name) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
auto iter = g_rtr_fault_injection_state.rtr_sgid_history.find(device_name);
|
|
if (iter == g_rtr_fault_injection_state.rtr_sgid_history.end()) {
|
|
return {};
|
|
}
|
|
return iter->second;
|
|
}
|
|
|
|
int getInjectedFailureCount() {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
return g_rtr_fault_injection_state.injected_failures;
|
|
}
|
|
|
|
std::vector<std::string> getRtrGidHistory(const std::string& device_name) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
auto iter = g_rtr_fault_injection_state.rtr_gid_history.find(device_name);
|
|
if (iter == g_rtr_fault_injection_state.rtr_gid_history.end()) {
|
|
return {};
|
|
}
|
|
return iter->second;
|
|
}
|
|
|
|
void recordRtrAttempt(const std::string& device_name, int sgid_index,
|
|
const std::string& gid) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
g_rtr_fault_injection_state.rtr_sgid_history[device_name].push_back(
|
|
sgid_index);
|
|
g_rtr_fault_injection_state.rtr_gid_history[device_name].push_back(gid);
|
|
}
|
|
|
|
int maybeSwapSyntheticGidIndex(const std::string& device_name, int gid_index) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
if (!g_rtr_fault_injection_state.synthetic_gid_swap_enabled ||
|
|
g_rtr_fault_injection_state.synthetic_gid_device != device_name) {
|
|
return gid_index;
|
|
}
|
|
if (gid_index == 0) return 1;
|
|
if (gid_index == 1) return 0;
|
|
return gid_index;
|
|
}
|
|
|
|
bool shouldInjectRtrEinval(const std::string& device_name, int sgid_index) {
|
|
std::lock_guard<std::mutex> guard(g_rtr_fault_injection_state.mu);
|
|
if (!g_rtr_fault_injection_state.fail_first_rtr_einval ||
|
|
g_rtr_fault_injection_state.fail_rtr_device != device_name ||
|
|
sgid_index != 0) {
|
|
return false;
|
|
}
|
|
g_rtr_fault_injection_state.fail_first_rtr_einval = false;
|
|
g_rtr_fault_injection_state.synthetic_gid_swap_enabled = false;
|
|
++g_rtr_fault_injection_state.injected_failures;
|
|
return true;
|
|
}
|
|
|
|
std::string formatDeviceNames(const std::string& device_names) {
|
|
std::stringstream ss(device_names);
|
|
std::string item;
|
|
std::vector<std::string> tokens;
|
|
while (getline(ss, item, ',')) {
|
|
tokens.push_back(item);
|
|
}
|
|
|
|
std::string formatted;
|
|
for (size_t i = 0; i < tokens.size(); ++i) {
|
|
formatted += "\"" + tokens[i] + "\"";
|
|
if (i < tokens.size() - 1) {
|
|
formatted += ",";
|
|
}
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
std::string makeNicPriorityMatrix(const std::string& device_name) {
|
|
auto formatted_devices = formatDeviceNames(device_name);
|
|
return "{\"cpu:0\": [[" + formatted_devices +
|
|
"],[]], "
|
|
" \"cpu:1\": [[" +
|
|
formatted_devices + "],[]]}";
|
|
}
|
|
|
|
void waitForTransfer(TransferEngine* engine, BatchID batch_id,
|
|
const std::string& op_name) {
|
|
bool completed = false;
|
|
TransferStatus status;
|
|
while (!completed) {
|
|
Status s = engine->getTransferStatus(batch_id, 0, status);
|
|
EXPECT_EQ(s, Status::OK());
|
|
if (status.s == TransferStatusEnum::COMPLETED) {
|
|
completed = true;
|
|
} else if (status.s == TransferStatusEnum::FAILED) {
|
|
FAIL() << op_name << " FAILED";
|
|
}
|
|
}
|
|
Status s = engine->freeBatchID(batch_id);
|
|
EXPECT_EQ(s, Status::OK());
|
|
}
|
|
|
|
struct TEContext {
|
|
std::unique_ptr<TransferEngine> engine_{};
|
|
uint8_t* local_addr_{};
|
|
bool segment_opened_{false};
|
|
SegmentHandle segment_handle_{};
|
|
uint64_t remote_base_{};
|
|
|
|
TEContext(const std::string& local_server_name,
|
|
const std::string& metadata_server, const std::string& segment_id,
|
|
const std::string& device_name) {
|
|
engine_ = std::make_unique<TransferEngine>(false);
|
|
auto hostname_port = parseHostNameWithPort(local_server_name);
|
|
engine_->init(metadata_server, local_server_name, hostname_port.first,
|
|
hostname_port.second);
|
|
|
|
auto nic_priority_matrix = makeNicPriorityMatrix(device_name);
|
|
void* args[2] = {const_cast<char*>(nic_priority_matrix.c_str()),
|
|
nullptr};
|
|
Transport* xport = engine_->installTransport("rdma", args);
|
|
LOG_ASSERT(xport);
|
|
|
|
local_addr_ = static_cast<uint8_t*>(numa_alloc_onnode(kRAMBufSize, 0));
|
|
memset(local_addr_, 0, kDataLength);
|
|
|
|
int rc =
|
|
engine_->registerLocalMemory(local_addr_, kRAMBufSize, "cpu:0");
|
|
LOG_ASSERT(!rc);
|
|
|
|
if (!segment_id.empty()) {
|
|
segment_opened_ = true;
|
|
LOG(INFO) << "Opening segment " << segment_id << "...";
|
|
segment_handle_ = engine_->openSegment(segment_id);
|
|
auto segment_desc =
|
|
engine_->getMetadata()->getSegmentDescByID(segment_handle_);
|
|
remote_base_ = (uint64_t)segment_desc->buffers[0].addr;
|
|
}
|
|
}
|
|
|
|
~TEContext() {
|
|
engine_->unregisterLocalMemory(local_addr_);
|
|
numa_free(local_addr_, kRAMBufSize);
|
|
if (segment_opened_) engine_->closeSegment(segment_handle_);
|
|
}
|
|
|
|
std::string localSegmentName() const {
|
|
return engine_->getLocalIpAndPort();
|
|
}
|
|
};
|
|
|
|
class RDMAEndpointReestablishTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
resetRtrFaultInjectionState();
|
|
google::InitGoogleLogging("RDMAEndpointReestablishTest");
|
|
FLAGS_logtostderr = true;
|
|
|
|
const char* env = std::getenv("MC_METADATA_SERVER");
|
|
metadata_server = env ? env : P2PHANDSHAKE;
|
|
LOG(INFO) << "metadata_server: " << metadata_server;
|
|
|
|
env = std::getenv("MC_TARGET_SERVER_NAME");
|
|
target_server_name = env ? env : "127.0.0.1:12345";
|
|
LOG(INFO) << "target_server_name: " << target_server_name;
|
|
|
|
env = std::getenv("MC_INITIATOR_SERVER_NAME");
|
|
initiator_server_name = env ? env : "127.0.0.1:12346";
|
|
LOG(INFO) << "initiator_server_name: " << initiator_server_name;
|
|
|
|
auto devices = getAvailableRdmaDevices();
|
|
if (devices.size() < 2) {
|
|
GTEST_SKIP() << "Need at least two RDMA devices, found "
|
|
<< devices.size();
|
|
}
|
|
|
|
env = std::getenv("MC_TARGET_DEVICE_NAME");
|
|
target_device_name = env ? env : devices[0];
|
|
LOG(INFO) << "target_device_name: " << target_device_name;
|
|
|
|
env = std::getenv("MC_INITIATOR_DEVICE_NAME");
|
|
initiator_device_name = env ? env : devices[1];
|
|
LOG(INFO) << "initiator_device_name: " << initiator_device_name;
|
|
}
|
|
|
|
void TearDown() override {
|
|
google::ShutdownGoogleLogging();
|
|
resetRtrFaultInjectionState();
|
|
}
|
|
|
|
void runEndpointReestablishScenario(const std::string& target_device,
|
|
const std::string& initiator_device) {
|
|
LOG(INFO) << "========== Setting up Target ==========";
|
|
TEContext target_ctx(target_server_name, metadata_server, "",
|
|
target_device);
|
|
const std::string target_segment_name =
|
|
usesP2PHandshake(metadata_server) ? target_ctx.localSegmentName()
|
|
: target_server_name;
|
|
LOG(INFO) << "Resolved target segment name: " << target_segment_name;
|
|
LOG(INFO)
|
|
<< "Target is up. Waiting for RDMA connections and operations...";
|
|
|
|
LOG(INFO) << "========== Phase 1: Start, Connect & Write ==========";
|
|
{
|
|
TEContext init_ctx(initiator_server_name, metadata_server,
|
|
target_segment_name, initiator_device);
|
|
for (size_t i = 0; i < kDataLength; ++i) {
|
|
init_ctx.local_addr_[i] = static_cast<uint8_t>(i % 256);
|
|
}
|
|
|
|
LOG(INFO) << "Writing " << kDataLength << " bytes to Target...";
|
|
auto batch_id = init_ctx.engine_->allocateBatchID(1);
|
|
TransferRequest entry;
|
|
entry.opcode = TransferRequest::WRITE;
|
|
entry.length = kDataLength;
|
|
entry.source = init_ctx.local_addr_;
|
|
entry.target_id = init_ctx.segment_handle_;
|
|
entry.target_offset = init_ctx.remote_base_;
|
|
|
|
Status s = init_ctx.engine_->submitTransfer(batch_id, {entry});
|
|
ASSERT_EQ(s, Status::OK());
|
|
waitForTransfer(init_ctx.engine_.get(), batch_id, "WRITE");
|
|
LOG(INFO) << "Phase 1: Write Completed. Tearing down connection...";
|
|
}
|
|
|
|
LOG(INFO) << "Simulating Initiator Crash/Restart... Waiting 2 seconds.";
|
|
sleep(2);
|
|
|
|
LOG(INFO)
|
|
<< "========== Phase 2: Restart, Re-establish Endpoint & Read "
|
|
"==========";
|
|
{
|
|
TEContext init_ctx(initiator_server_name, metadata_server,
|
|
target_segment_name, initiator_device);
|
|
LOG(INFO) << "Reading data back over new Endpoint...";
|
|
auto batch_id = init_ctx.engine_->allocateBatchID(1);
|
|
TransferRequest entry;
|
|
entry.opcode = TransferRequest::READ;
|
|
entry.length = kDataLength;
|
|
entry.source = init_ctx.local_addr_;
|
|
entry.target_id = init_ctx.segment_handle_;
|
|
entry.target_offset = init_ctx.remote_base_;
|
|
|
|
Status s = init_ctx.engine_->submitTransfer(batch_id, {entry});
|
|
ASSERT_EQ(s, Status::OK());
|
|
waitForTransfer(init_ctx.engine_.get(), batch_id, "READ");
|
|
|
|
bool ok = true;
|
|
for (size_t i = 0; i < kDataLength; ++i) {
|
|
if (init_ctx.local_addr_[i] != static_cast<uint8_t>(i % 256)) {
|
|
ok = false;
|
|
LOG(ERROR) << "Data mismatch at offset " << i
|
|
<< ", expected " << (i % 256) << ", got "
|
|
<< (int)init_ctx.local_addr_[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
ASSERT_TRUE(ok) << "Endpoint Reconstruction Verification Failed!";
|
|
LOG(INFO) << ">>> ENDPOINT RECONSTRUCTION VERIFICATION: "
|
|
"\033[32mSUCCESS\033";
|
|
}
|
|
}
|
|
|
|
std::string metadata_server;
|
|
std::string target_server_name;
|
|
std::string initiator_server_name;
|
|
std::string target_device_name;
|
|
std::string initiator_device_name;
|
|
};
|
|
|
|
TEST_F(RDMAEndpointReestablishTest, EndpointReestablish) {
|
|
runEndpointReestablishScenario(target_device_name, initiator_device_name);
|
|
}
|
|
|
|
TEST_F(RDMAEndpointReestablishTest, EndpointReestablishReverseDevices) {
|
|
runEndpointReestablishScenario(initiator_device_name, target_device_name);
|
|
}
|
|
|
|
TEST_F(RDMAEndpointReestablishTest, ActiveHandshakeRetriesAfterAutoGidReprobe) {
|
|
configureRtrFaultInjection(initiator_device_name);
|
|
runEndpointReestablishScenario(target_device_name, initiator_device_name);
|
|
|
|
EXPECT_EQ(getInjectedFailureCount(), 1);
|
|
auto sgid_history = getRtrSgidHistory(initiator_device_name);
|
|
auto gid_history = getRtrGidHistory(initiator_device_name);
|
|
ASSERT_EQ(sgid_history.size(), gid_history.size());
|
|
ASSERT_GE(sgid_history.size(), 2u);
|
|
EXPECT_EQ(sgid_history.front(), 0);
|
|
EXPECT_FALSE(gid_history.front().empty());
|
|
EXPECT_TRUE(std::any_of(
|
|
gid_history.begin() + 1, gid_history.end(),
|
|
[&](const std::string& gid) { return gid != gid_history.front(); }));
|
|
}
|
|
|
|
TEST_F(RDMAEndpointReestablishTest,
|
|
PassiveHandshakeRetriesAfterAutoGidReprobe) {
|
|
configureRtrFaultInjection(target_device_name);
|
|
runEndpointReestablishScenario(target_device_name, initiator_device_name);
|
|
|
|
EXPECT_EQ(getInjectedFailureCount(), 1);
|
|
auto sgid_history = getRtrSgidHistory(target_device_name);
|
|
auto gid_history = getRtrGidHistory(target_device_name);
|
|
ASSERT_EQ(sgid_history.size(), gid_history.size());
|
|
ASSERT_GE(sgid_history.size(), 2u);
|
|
EXPECT_EQ(sgid_history.front(), 0);
|
|
EXPECT_FALSE(gid_history.front().empty());
|
|
EXPECT_TRUE(std::any_of(
|
|
gid_history.begin() + 1, gid_history.end(),
|
|
[&](const std::string& gid) { return gid != gid_history.front(); }));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" int __real__ibv_query_gid_ex(ibv_context* context, uint8_t port_num,
|
|
int gid_index,
|
|
struct ibv_gid_entry* entry,
|
|
uint32_t flags, size_t entry_size);
|
|
|
|
extern "C" int __wrap__ibv_query_gid_ex(ibv_context* context, uint8_t port_num,
|
|
int gid_index,
|
|
struct ibv_gid_entry* entry,
|
|
uint32_t flags, size_t entry_size) {
|
|
const std::string device_name = ibv_get_device_name(context->device);
|
|
int wrapped_gid_index = maybeSwapSyntheticGidIndex(device_name, gid_index);
|
|
return __real__ibv_query_gid_ex(context, port_num, wrapped_gid_index, entry,
|
|
flags, entry_size);
|
|
}
|
|
|
|
extern "C" int __real_ibv_query_gid(ibv_context* context, uint8_t port_num,
|
|
int gid_index, union ibv_gid* gid);
|
|
|
|
extern "C" int __wrap_ibv_query_gid(ibv_context* context, uint8_t port_num,
|
|
int gid_index, union ibv_gid* gid) {
|
|
const std::string device_name = ibv_get_device_name(context->device);
|
|
int wrapped_gid_index = maybeSwapSyntheticGidIndex(device_name, gid_index);
|
|
return __real_ibv_query_gid(context, port_num, wrapped_gid_index, gid);
|
|
}
|
|
|
|
extern "C" int __real_ibv_modify_qp(ibv_qp* qp, ibv_qp_attr* attr,
|
|
int attr_mask);
|
|
|
|
extern "C" int __wrap_ibv_modify_qp(ibv_qp* qp, ibv_qp_attr* attr,
|
|
int attr_mask) {
|
|
if (qp != nullptr && attr != nullptr && attr->qp_state == IBV_QPS_RTR &&
|
|
(attr_mask & IBV_QP_AV)) {
|
|
const std::string device_name =
|
|
ibv_get_device_name(qp->context->device);
|
|
int sgid_index = attr->ah_attr.grh.sgid_index;
|
|
union ibv_gid actual_gid = {};
|
|
std::string gid_string;
|
|
if (__real_ibv_query_gid(qp->context, attr->ah_attr.port_num,
|
|
sgid_index, &actual_gid) == 0) {
|
|
gid_string = formatGidBytes(actual_gid.raw);
|
|
}
|
|
recordRtrAttempt(device_name, sgid_index, gid_string);
|
|
if (shouldInjectRtrEinval(device_name, sgid_index)) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
}
|
|
return __real_ibv_modify_qp(qp, attr, attr_mask);
|
|
}
|