Skip to content

Commit 76a300f

Browse files
sbates130272claude
andcommitted
fix(test): stop the test server lying about listening, aborting, and wedging
Three bugs in the v1 integration test server, all found while chasing the Two-VM GPU Direct lane, all of which cost time by describing themselves as something else. runThreaded() called FD_SET(listen_fd_, &fds) with no guard. runOnce() has always checked listen_fd_ < 0; the threaded loop did not, and FD_SET(-1) is a glibc fortify abort. So a port already in use did not report a busy port -- it killed the process with "bit out of range 0 - FD_SETSIZE on fd_set", which names no port and suggests memory corruption. Under systemd that arrived as code=dumped, status=6/ABRT. startThreaded() now refuses to spawn the thread without a socket, and the loop holds the invariant itself since it is reachable directly. main() never checked that start returned. It printed "listening on port N" unconditionally -- the line every caller waits for before it starts sending -- so a server that had already failed to bind announced itself ready and then died. startThreaded() returns bool; both call sites check it and exit 1 with the port in the message. The constructor also says why the bind failed, where it failed, rather than leaving a half-built object to abort later. The RC QP was only reset on the success paths. One QP serves every request in turn, and an RC QP already in RTS cannot be moved to RTR for the next client, so a single failed transfer wedged the server for everyone after it: each subsequent request returned "could not connect the RC QP to the client", naming the wrong fault and burying the real one. Not hypothetical -- it invalidated a size sweep on the lab, where four sizes appeared to fail on their merits and had in fact never reached the wire. Reset moves into Impl::resetQp(), runs on every path that touched the QP, and clears `ready` if the rebuild itself fails rather than serving a QP it no longer has. Verified on the two-guest lab: a transfer that fails is followed by two that succeed against the same server instance, with one failure line in the log and none after it. Two servers on one port give hipobj-rdma-test-server: cannot bind port 9321: Address already in use hipobj-rdma-test-server: not listening on port 9321 and exit 1, where the second used to abort. ctest is 12/12. The QP reset is confirmed working in CI: the Two-VM GPU Direct client now retries and gets the same error both times, where before the retry reported a connect failure and hid the original fault. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Stephen Bates <sbates@raithlin.com>
1 parent 7175f8c commit 76a300f

4 files changed

Lines changed: 138 additions & 40 deletions

File tree

‎test/integration/rdma-test-server/http_server.cpp‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <cerrno>
99
#include <chrono>
10+
#include <cstdio>
1011
#include <cstring>
1112
#include <memory>
1213
#include <sstream>
@@ -122,6 +123,11 @@ HttpServer::HttpServer(int port) {
122123
addr.sin_addr.s_addr = INADDR_ANY;
123124
addr.sin_port = htons(static_cast<uint16_t>(port));
124125
if (bind(listen_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
126+
/* Silence here is what made a busy port look like a crash: the object
127+
* went on being constructed, and the abort arrived later from select().
128+
* Say which port and why, once, where it happened. */
129+
fprintf(stderr, "hipobj-rdma-test-server: cannot bind port %d: %s\n", port,
130+
strerror(errno));
125131
close(listen_fd_);
126132
listen_fd_ = -1;
127133
return;
@@ -170,16 +176,28 @@ int HttpServer::runOnce(int timeoutMs) {
170176
return 1;
171177
}
172178

173-
void HttpServer::startThreaded() {
179+
bool HttpServer::startThreaded() {
180+
if (listen_fd_ < 0) {
181+
return false;
182+
}
174183
acceptLoop_ = std::thread([this] {
175184
runThreaded();
176185
});
186+
return true;
177187
}
178188

179189
void HttpServer::runThreaded() {
180190
/* Accept loop; runs on the tracked thread created by
181191
* startThreaded() so stop() can join it as the production
182192
* barrier. */
193+
if (listen_fd_ < 0) {
194+
/* runOnce() has always checked this; this loop did not, and FD_SET(-1)
195+
* is a glibc fortify abort -- so a failed bind killed the process with
196+
* "bit out of range 0 - FD_SETSIZE on fd_set" and no mention of a port.
197+
* startThreaded() refuses to spawn the thread now, but the loop must
198+
* hold the invariant itself: it is also reachable directly. */
199+
return;
200+
}
183201
while (!stopping_.load()) {
184202
fd_set fds;
185203
FD_ZERO(&fds);

‎test/integration/rdma-test-server/http_server.h‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ class HttpServer {
5252
* each on its own thread. Responses are completed with the
5353
* afterSend finalizer contract. The loop always runs on the
5454
* tracked thread created here, so stop() has a join barrier
55-
* before waiting on the worker count. */
56-
void startThreaded();
55+
* before waiting on the worker count.
56+
*
57+
* Returns false when the constructor never got a listening socket.
58+
* Callers must not announce a port they are not listening on. */
59+
bool startThreaded();
5760
void stop();
5861

5962
int fd() const {

‎test/integration/rdma-test-server/main.cpp‎

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <cstdio>
1515
#include <cstdlib>
1616
#include <map>
17+
#include <mutex>
1718
#include <sstream>
1819
#include <string>
1920
#include <thread>
@@ -145,8 +146,12 @@ int main(int argc, char* argv[]) {
145146
resp.headers["X-Amz-Rdma-Protocol-Status"] = "unsupported";
146147
return resp;
147148
});
149+
if (!server.startThreaded()) {
150+
fprintf(stderr, "hipobj-rdma-test-server: v2 not listening on port %d\n",
151+
port);
152+
return 1;
153+
}
148154
fprintf(stdout, "hipobj-rdma-test-server v2 listening on port %d\n", port);
149-
server.startThreaded();
150155
/* Block until external termination. Signals terminate the
151156
* process without unwinding (no destructor runs); a graceful
152157
* return would drain via the HttpServer destructor. */
@@ -166,12 +171,12 @@ int main(int argc, char* argv[]) {
166171

167172
hipobj::test::RdmaTestServer rdma;
168173
if (!rdma.isReady()) {
169-
fprintf(stderr,
170-
"hipobj-rdma-test-server: RDMA not available (libibverbs/NIC)\n");
171-
return 1;
174+
fprintf(stderr, "hipobj-rdma-test-server: RDMA not available — running in "
175+
"HTTP-only mode\n");
172176
}
173177

174178
std::map<std::string, std::vector<uint8_t>> objects;
179+
std::mutex objects_mu;
175180

176181
hipobj::test::HttpServer server(port);
177182
server.setHandler(
@@ -199,12 +204,34 @@ int main(int argc, char* argv[]) {
199204

200205
const std::string key = objectKey(req.path);
201206
auto tokenIt = req.headers.find("x-amz-rdma-token");
202-
if (tokenIt == req.headers.end()) {
203-
resp.status = 400;
204-
resp.body = "missing x-amz-rdma-token";
207+
208+
// HTTP-only mode: RDMA token absent or RDMA not available.
209+
if (tokenIt == req.headers.end() || !rdma.isReady()) {
210+
if (req.method == "PUT") {
211+
std::lock_guard<std::mutex> lk(objects_mu);
212+
objects[key] = std::vector<uint8_t>(req.body.begin(), req.body.end());
213+
resp.status = 200;
214+
resp.headers["etag"] = "\"test\"";
215+
return resp;
216+
}
217+
if (req.method == "GET") {
218+
std::lock_guard<std::mutex> lk(objects_mu);
219+
auto it = objects.find(key);
220+
if (it == objects.end()) {
221+
resp.status = 404;
222+
resp.body = "not found";
223+
return resp;
224+
}
225+
resp.status = 200;
226+
resp.body = std::string(it->second.begin(), it->second.end());
227+
return resp;
228+
}
229+
resp.status = 405;
230+
resp.body = "method not allowed";
205231
return resp;
206232
}
207233

234+
// Reaching here: token present and RDMA ready — use RDMA path.
208235
if (req.method == "PUT") {
209236
std::vector<uint8_t> payload;
210237
std::string replyHeader;
@@ -220,31 +247,38 @@ int main(int argc, char* argv[]) {
220247
resp.body = "RDMA PUT failed";
221248
return resp;
222249
}
223-
objects[key] = std::move(payload);
250+
{
251+
std::lock_guard<std::mutex> lk(objects_mu);
252+
objects[key] = std::move(payload);
253+
}
224254
resp.status = 200;
225255
resp.headers["x-amz-rdma-reply"] = replyHeader;
226256
resp.headers["etag"] = "\"test\"";
227257
return resp;
228258
}
229259

230260
if (req.method == "GET") {
231-
auto it = objects.find(key);
232-
if (it == objects.end()) {
233-
resp.status = 404;
234-
resp.body = "not found";
235-
return resp;
261+
std::vector<uint8_t> data;
262+
{
263+
std::lock_guard<std::mutex> lk(objects_mu);
264+
auto it = objects.find(key);
265+
if (it == objects.end()) {
266+
resp.status = 404;
267+
resp.body = "not found";
268+
return resp;
269+
}
270+
data = it->second;
236271
}
237272
std::string replyHeader;
238-
if (rdma.rdmaWriteToClient(tokenIt->second, it->second, replyHeader) !=
239-
0) {
273+
if (rdma.rdmaWriteToClient(tokenIt->second, data, replyHeader) != 0) {
240274
resp.status = 500;
241275
resp.body = "RDMA GET failed";
242276
return resp;
243277
}
244278
resp.status = 200;
245279
resp.headers["x-amz-rdma-reply"] = replyHeader;
246280
resp.headers["x-amz-rdma-bytes-transferred"] = std::to_string(
247-
it->second.size());
281+
data.size());
248282
return resp;
249283
}
250284

@@ -253,9 +287,19 @@ int main(int argc, char* argv[]) {
253287
return resp;
254288
});
255289

290+
// Use the threaded server so large HTTP request bodies (e.g. HTTP-only
291+
// PUT payloads) are received in full rather than truncated at the
292+
// 65 KB single-recv limit of runOnce().
293+
/* "listening on port N" is what every caller waits for before it starts
294+
* sending. Printing it without checking made a dead server look ready. */
295+
if (!server.startThreaded()) {
296+
fprintf(stderr, "hipobj-rdma-test-server: not listening on port %d\n",
297+
port);
298+
return 1;
299+
}
256300
fprintf(stdout, "hipobj-rdma-test-server listening on port %d\n", port);
257301
for (;;) {
258-
server.runOnce(1000);
302+
std::this_thread::sleep_for(std::chrono::seconds(3600));
259303
}
260304
return 0;
261305
}

‎test/integration/rdma-test-server/rdma_server.cpp‎

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include "rdma_server.h"
77

8+
#include <cstdio>
89
#include <cstdlib>
910
#include <cstring>
1011

@@ -20,6 +21,15 @@ constexpr int IBV_ACCESS_LOCAL_WRITE = 0x4;
2021
constexpr int IBV_ACCESS_REMOTE_READ = 0x1;
2122
constexpr int IBV_ACCESS_REMOTE_WRITE = 0x2;
2223

24+
/* Every failure below used to be a bare `return -1`, which the HTTP layer
25+
* turns into "500 RDMA PUT failed" with no reply header -- indistinguishable,
26+
* from the client, from a server that does not speak the RDMA extension at
27+
* all. Six different faults reported as one. Name which one it was. */
28+
int rdmaFail(const char* op, const char* why) {
29+
fprintf(stderr, "hipobj-rdma-test-server: %s failed: %s\n", op, why);
30+
return -1;
31+
}
32+
2333
bool parseTokenHeader(const std::string& header, hipObj::RdmaToken& token,
2434
uint64_t& remoteAddr, size_t& size) {
2535
size_t c1 = header.find(':');
@@ -118,6 +128,27 @@ struct RdmaTestServer::Impl {
118128
size_t stagingSize = 64 * 1024 * 1024;
119129
bool ready = false;
120130

131+
/* One QP serves every request in turn, so it has to come back to INIT
132+
* between them -- an RC QP already in RTS cannot be moved to RTR for the
133+
* next client. Only the success paths used to do this, which meant a
134+
* single failed transfer wedged the server for every client after it:
135+
* each one got "could not connect the RC QP to the client", naming the
136+
* wrong fault and hiding the real one. Run it on every path that touched
137+
* the QP, failure included. */
138+
void resetQp() {
139+
if (!conn.qp) {
140+
return;
141+
}
142+
hipObj::ibv.destroy_qp(conn.qp);
143+
conn.qp = nullptr;
144+
if (hipObj::createRcQp(conn, 256, 128, 128) != 0 ||
145+
hipObj::transitionQpToInit(conn) != 0) {
146+
fprintf(stderr, "hipobj-rdma-test-server: could not rebuild the RC QP; "
147+
"the server can no longer serve RDMA\n");
148+
ready = false;
149+
}
150+
}
151+
121152
~Impl() {
122153
if (stagingMr) {
123154
hipObj::ibv.dereg_mr(stagingMr);
@@ -174,52 +205,53 @@ int RdmaTestServer::rdmaWriteToClient(const std::string& tokenHeader,
174205
const std::vector<uint8_t>& data,
175206
std::string& replyHeader) {
176207
if (!isReady()) {
177-
return -1;
208+
return rdmaFail("GET", "server RDMA not initialised");
178209
}
179210
hipObj::RdmaToken clientToken{};
180211
uint64_t remoteAddr = 0;
181212
size_t xferSize = 0;
182213
if (!parseTokenHeader(tokenHeader, clientToken, remoteAddr, xferSize)) {
183-
return -1;
214+
return rdmaFail("GET", "x-amz-rdma-token did not parse");
184215
}
185216
if (xferSize == 0) {
186217
xferSize = data.size();
187218
}
188219
if (data.size() < xferSize || xferSize > impl_->stagingSize) {
220+
fprintf(stderr,
221+
"hipobj-rdma-test-server: GET failed: %zu bytes requested of a %zu "
222+
"byte object, %zu byte staging buffer\n",
223+
xferSize, data.size(), impl_->stagingSize);
189224
return -1;
190225
}
191226

192227
std::memcpy(impl_->stagingBuf, data.data(), xferSize);
193228
if (hipObj::connectRcPeer(impl_->conn, clientToken) != 0) {
194-
return -1;
229+
impl_->resetQp();
230+
return rdmaFail("GET", "could not connect the RC QP to the client");
195231
}
196232
if (postRdmaWrite(impl_->conn, impl_->stagingMr, xferSize, clientToken) !=
197233
0) {
198-
return -1;
234+
impl_->resetQp();
235+
return rdmaFail("GET", "RDMA WRITE to the client buffer did not complete");
199236
}
200237
hipObj::RdmaToken serverToken = buildServerToken(impl_->conn,
201238
impl_->stagingMr, xferSize);
202239
replyHeader = hipObj::encodeReplyWithPeerToken(200, serverToken);
203-
if (impl_->conn.qp) {
204-
hipObj::ibv.destroy_qp(impl_->conn.qp);
205-
impl_->conn.qp = nullptr;
206-
hipObj::createRcQp(impl_->conn, 256, 128, 128);
207-
hipObj::transitionQpToInit(impl_->conn);
208-
}
240+
impl_->resetQp();
209241
return 0;
210242
}
211243

212244
int RdmaTestServer::rdmaReadFromClient(const std::string& tokenHeader,
213245
size_t size, std::vector<uint8_t>& data,
214246
std::string& replyHeader) {
215247
if (!isReady()) {
216-
return -1;
248+
return rdmaFail("PUT", "server RDMA not initialised");
217249
}
218250
hipObj::RdmaToken clientToken{};
219251
uint64_t remoteAddr = 0;
220252
size_t xferSize = 0;
221253
if (!parseTokenHeader(tokenHeader, clientToken, remoteAddr, xferSize)) {
222-
return -1;
254+
return rdmaFail("PUT", "x-amz-rdma-token did not parse");
223255
}
224256
if (size != 0) {
225257
xferSize = size;
@@ -228,26 +260,27 @@ int RdmaTestServer::rdmaReadFromClient(const std::string& tokenHeader,
228260
xferSize = static_cast<size_t>(clientToken.length);
229261
}
230262
if (xferSize > impl_->stagingSize) {
263+
fprintf(stderr,
264+
"hipobj-rdma-test-server: PUT failed: %zu bytes exceeds the %zu "
265+
"byte staging buffer\n",
266+
xferSize, impl_->stagingSize);
231267
return -1;
232268
}
233269

234270
if (hipObj::connectRcPeer(impl_->conn, clientToken) != 0) {
235-
return -1;
271+
impl_->resetQp();
272+
return rdmaFail("PUT", "could not connect the RC QP to the client");
236273
}
237274
if (postRdmaRead(impl_->conn, impl_->stagingMr, xferSize, clientToken) != 0) {
238-
return -1;
275+
impl_->resetQp();
276+
return rdmaFail("PUT", "RDMA READ from the client buffer did not complete");
239277
}
240278
data.assign(static_cast<uint8_t*>(impl_->stagingBuf),
241279
static_cast<uint8_t*>(impl_->stagingBuf) + xferSize);
242280
hipObj::RdmaToken serverToken = buildServerToken(impl_->conn,
243281
impl_->stagingMr, xferSize);
244282
replyHeader = hipObj::encodeReplyWithPeerToken(200, serverToken);
245-
if (impl_->conn.qp) {
246-
hipObj::ibv.destroy_qp(impl_->conn.qp);
247-
impl_->conn.qp = nullptr;
248-
hipObj::createRcQp(impl_->conn, 256, 128, 128);
249-
hipObj::transitionQpToInit(impl_->conn);
250-
}
283+
impl_->resetQp();
251284
return 0;
252285
}
253286

0 commit comments

Comments
 (0)