rogue
Loading...
Searching...
No Matches
Server.cpp
Go to the documentation of this file.
1
17#include "rogue/Directives.h"
18
20
21#include <arpa/inet.h>
22#include <errno.h>
23#include <fcntl.h>
24#include <infiniband/verbs.h>
25#include <inttypes.h>
26#include <stdint.h>
27#include <string.h>
28#include <sys/select.h>
29#include <unistd.h>
30
31#include <chrono>
32#include <cstdlib>
33#include <iomanip>
34#include <memory>
35#include <random>
36#include <sstream>
37#include <string>
38#include <thread>
39#include <vector>
40
41#include "rogue/GeneralError.h"
42#include "rogue/GilRelease.h"
43#include "rogue/Logging.h"
48
51
52#ifndef NO_PYTHON
53 #include <boost/python.hpp>
54namespace bp = boost::python;
55#endif
56
57// SSI Start-of-Frame bit set on every received frame
58static const uint8_t SsiSof = 0x02;
59
60// ---------------------------------------------------------------------------
61// Factory
62// ---------------------------------------------------------------------------
63rpr::ServerPtr rpr::Server::create(const std::string& deviceName,
64 uint8_t ibPort,
65 uint8_t gidIndex,
66 uint32_t maxPayload,
67 uint32_t rxQueueDepth) {
68 return std::make_shared<rpr::Server>(
69 deviceName, ibPort, gidIndex, maxPayload, rxQueueDepth);
70}
71
72// ---------------------------------------------------------------------------
73// Constructor
74// ibverbs setup through QP INIT. Receive thread starts in
75// completeConnection() once the FPGA QPN is known.
76// ---------------------------------------------------------------------------
77rpr::Server::Server(const std::string& deviceName,
78 uint8_t ibPort,
79 uint8_t gidIndex,
80 uint32_t maxPayload,
81 uint32_t rxQueueDepth)
82 : rpr::Core(deviceName, ibPort, gidIndex, maxPayload),
83 ris::Master(),
84 ris::Slave(),
85 cq_(nullptr), qp_(nullptr), mr_(nullptr), comp_channel_(nullptr),
86 slab_(nullptr),
87 slabSize_(0),
88 numBufs_(rxQueueDepth),
89 bufSize_(0),
90 hostQpn_(0),
91 hostRqPsn_(0),
92 hostSqPsn_(0),
93 mrAddr_(0),
94 mrRkey_(0),
95 thread_(nullptr),
96 threadEn_(false),
97 frameCount_(0),
98 byteCount_(0) {
99
100 log_ = rogue::Logging::create("rocev2.Server");
101 memset(hostGid_, 0, 16);
102 memset(fpgaGid_, 0, 16);
103 wakeFd_[0] = wakeFd_[1] = -1;
104
105 // The destructor does NOT run on a partially-constructed object, so any
106 // throw between here and the end of the body would leak slab_ / mr_ /
107 // cq_ / qp_. Wrap the body in try/catch and call cleanupResources() on
108 // the way out before rethrowing — same effect as a stack of unique_ptr
109 // wrappers but with a single cleanup path.
110 try {
111 // -------------------------------------------------------------------
112 // 1. Allocate slab
113 // RC QPs do not prepend a GRH so each slot is exactly maxPayload_.
114 // posix_memalign accepts any size (aligned_alloc requires the size
115 // to be a multiple of the alignment per C11; with the default
116 // 9000 * 256 slab that constraint does not hold).
117 // -------------------------------------------------------------------
118 bufSize_ = maxPayload_;
119
120 uint64_t slabSize64 = static_cast<uint64_t>(numBufs_) * bufSize_;
121 if (slabSize64 > UINT32_MAX)
122 throw(rogue::GeneralError::create("rocev2::Server::Server",
123 "RX slab too large: %u * %u = %" PRIu64 " exceeds 4 GiB",
124 numBufs_, bufSize_, slabSize64));
125 slabSize_ = static_cast<uint32_t>(slabSize64);
126
127 void* slabPtr = nullptr;
128 if (posix_memalign(&slabPtr, 4096, slabSize_) != 0 || !slabPtr)
129 throw(rogue::GeneralError::create("rocev2::Server::Server",
130 "Failed to allocate RX slab (%u bytes)",
131 slabSize_));
132 slab_ = static_cast<uint8_t*>(slabPtr);
133 memset(slab_, 0, slabSize_);
134
135 // -------------------------------------------------------------------
136 // 2. Register slab as a single MR
137 // -------------------------------------------------------------------
138 mr_ = ibv_reg_mr(pd_, slab_, slabSize_,
139 IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
140 if (!mr_)
141 throw(rogue::GeneralError::create("rocev2::Server::Server",
142 "ibv_reg_mr failed"));
143
144 mrAddr_ = reinterpret_cast<uint64_t>(slab_);
145 mrRkey_ = mr_->rkey;
146
147 log_->info("MR: addr=0x%016" PRIx64 " rkey=0x%08x size=%u",
148 mrAddr_, mrRkey_, slabSize_);
149
150 // -------------------------------------------------------------------
151 // 3a. Completion channel + self-pipe (event-driven CQ, no busy-poll)
152 // The CQ below is bound to comp_channel_ so each completion makes
153 // the channel fd readable; runThread() blocks in select() on that
154 // fd plus wakeFd_ (a self-pipe stop() writes to) for prompt,
155 // sleepless shutdown. Both fds are non-blocking so the drain loops
156 // terminate cleanly (ibv_get_cq_event returns EAGAIN when empty).
157 // -------------------------------------------------------------------
158 comp_channel_ = ibv_create_comp_channel(ctx_);
159 if (!comp_channel_)
160 throw(rogue::GeneralError::create("rocev2::Server::Server",
161 "ibv_create_comp_channel failed"));
162 {
163 int flags = fcntl(comp_channel_->fd, F_GETFL);
164 if (flags < 0 ||
165 fcntl(comp_channel_->fd, F_SETFL, flags | O_NONBLOCK) < 0)
166 throw(rogue::GeneralError::create("rocev2::Server::Server",
167 "failed to set comp channel fd non-blocking"));
168 }
169
170 if (pipe(wakeFd_) != 0)
171 throw(rogue::GeneralError::create("rocev2::Server::Server",
172 "wake self-pipe creation failed"));
173 for (int i = 0; i < 2; ++i) {
174 int flags = fcntl(wakeFd_[i], F_GETFL);
175 if (flags >= 0) fcntl(wakeFd_[i], F_SETFL, flags | O_NONBLOCK);
176 }
177
178 // -------------------------------------------------------------------
179 // 3. Create Completion Queue (bound to comp_channel_ for event-driven
180 // notification; cq_context = this)
181 // -------------------------------------------------------------------
182 cq_ = ibv_create_cq(ctx_,
183 static_cast<int>(numBufs_),
184 this, comp_channel_, 0);
185 if (!cq_)
186 throw(rogue::GeneralError::create("rocev2::Server::Server",
187 "ibv_create_cq failed"));
188
189 // -------------------------------------------------------------------
190 // 4. Create RC Queue Pair
191 // -------------------------------------------------------------------
192 struct ibv_qp_init_attr qpAttr;
193 memset(&qpAttr, 0, sizeof(qpAttr));
194 qpAttr.qp_type = IBV_QPT_RC;
195 qpAttr.sq_sig_all = 0;
196 qpAttr.send_cq = cq_;
197 qpAttr.recv_cq = cq_;
198 qpAttr.cap.max_recv_wr = numBufs_;
199 qpAttr.cap.max_send_wr = 1;
200 qpAttr.cap.max_recv_sge = 1;
201 qpAttr.cap.max_send_sge = 1;
202
203 qp_ = ibv_create_qp(pd_, &qpAttr);
204 if (!qp_)
205 throw(rogue::GeneralError::create("rocev2::Server::Server",
206 "ibv_create_qp (RC) failed"));
207
208 hostQpn_ = qp_->qp_num;
209
210 // -------------------------------------------------------------------
211 // 5. QP: RESET → INIT
212 // -------------------------------------------------------------------
213 {
214 struct ibv_qp_attr attr;
215 memset(&attr, 0, sizeof(attr));
216 attr.qp_state = IBV_QPS_INIT;
217 attr.pkey_index = 0;
218 attr.port_num = ibPort_;
219 attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE |
220 IBV_ACCESS_REMOTE_READ |
221 IBV_ACCESS_LOCAL_WRITE;
222
223 if (ibv_modify_qp(qp_, &attr,
224 IBV_QP_STATE |
225 IBV_QP_PKEY_INDEX |
226 IBV_QP_PORT |
227 IBV_QP_ACCESS_FLAGS))
228 throw(rogue::GeneralError::create("rocev2::Server::Server",
229 "QP RESET→INIT failed"));
230 }
231
232 // -------------------------------------------------------------------
233 // 6. Read host GID
234 //
235 // The rdma_rxe (Soft-RoCE) kernel driver does NOT validate
236 // gidIndex against the populated range of its GID table —
237 // ibv_query_gid returns rc=0 for any in-range index per the
238 // reported gid_tbl_len (1024 on RXE) and silently writes an
239 // all-zero GID for unpopulated slots. A zero GID passes the
240 // rc==0 check here but cripples the later QP→RTR transition
241 // (dest GID cannot resolve to a peer), producing a cryptic
242 // downstream failure whose trail back to the bad gidIndex is
243 // obscured. Validate the returned GID is non-zero so users
244 // get a clear, actionable error pinned to the gidIndex they
245 // actually passed — valid RoCE GIDs are never all-zero
246 // (IPv4-mapped carries the 0xFFFF marker, IB-native uses an
247 // fe80::/10 or assigned subnet prefix).
248 // -------------------------------------------------------------------
249 union ibv_gid gid;
250 if (ibv_query_gid(ctx_, ibPort_, gidIndex_, &gid))
251 throw(rogue::GeneralError::create("rocev2::Server::Server",
252 "ibv_query_gid failed"));
253 bool zeroGid = true;
254 for (int i = 0; i < 16; ++i) {
255 if (gid.raw[i] != 0) {
256 zeroGid = false;
257 break;
258 }
259 }
260 if (zeroGid)
261 throw(rogue::GeneralError::create("rocev2::Server::Server",
262 "GID query returned all-zero for "
263 "gidIndex=%u on device '%s' "
264 "(likely out-of-range; rdma_rxe "
265 "does not validate gidIndex and "
266 "returns an empty GID for unused "
267 "slots)",
268 gidIndex_,
269 deviceName_.c_str()));
270 memcpy(hostGid_, gid.raw, 16);
271
272 // -------------------------------------------------------------------
273 // 7. Random starting PSNs (seeded per construction to avoid
274 // deterministic sequences across process restarts; RC QP PSN is
275 // expected to be randomized to reduce stale/replay confusion).
276 // -------------------------------------------------------------------
277 std::mt19937 psnRng(std::random_device {}());
278 hostRqPsn_ = psnRng() & 0xFFFFFF;
279 hostSqPsn_ = psnRng() & 0xFFFFFF;
280
281 log_->info("RC QP ready: qpn=0x%06x rqPsn=0x%06x sqPsn=0x%06x",
282 hostQpn_, hostRqPsn_, hostSqPsn_);
283 } catch (...) {
284 cleanupResources();
285 throw;
286 }
287}
288
289// ---------------------------------------------------------------------------
290// cleanupResources — release every ibverbs / heap resource owned by Server,
291// in reverse order of allocation. Idempotent (safe to call from both the
292// failed-construction path and stop()).
293// ---------------------------------------------------------------------------
294void rpr::Server::cleanupResources() {
295 if (qp_) {
296 ibv_destroy_qp(qp_);
297 qp_ = nullptr;
298 }
299 if (cq_) {
300 ibv_destroy_cq(cq_);
301 cq_ = nullptr;
302 }
303 if (comp_channel_) {
304 ibv_destroy_comp_channel(comp_channel_);
305 comp_channel_ = nullptr;
306 }
307 if (mr_) {
308 ibv_dereg_mr(mr_);
309 mr_ = nullptr;
310 }
311 if (slab_) {
312 free(slab_);
313 slab_ = nullptr;
314 }
315 for (int i = 0; i < 2; ++i) {
316 if (wakeFd_[i] >= 0) {
317 close(wakeFd_[i]);
318 wakeFd_[i] = -1;
319 }
320 }
321}
322
323// ---------------------------------------------------------------------------
324// setFpgaGid
325// ---------------------------------------------------------------------------
326void rpr::Server::setFpgaGid(const std::string& gidBytes) {
327 if (gidBytes.size() != 16)
328 throw(rogue::GeneralError::create("rocev2::Server::setFpgaGid",
329 "GID must be 16 bytes, got %zu",
330 gidBytes.size()));
331 memcpy(fpgaGid_, gidBytes.c_str(), 16);
332 log_->info("FPGA GID stored");
333}
334
335// ---------------------------------------------------------------------------
336// completeConnection — finish handshake and start receive thread
337// ---------------------------------------------------------------------------
338void rpr::Server::completeConnection(uint32_t fpgaQpn, uint32_t fpgaRqPsn,
339 uint32_t pmtu, uint32_t minRnrTimer) {
340 // Single-use: a second call would reassign thread_ and orphan the
341 // original std::thread. Real misuse would also be caught by
342 // ibv_modify_qp rejecting INIT→RTR when the QP is already in RTS,
343 // but guarding here gives a clearer error and prevents the raw
344 // pointer reassignment pattern entirely.
345 if (thread_ != nullptr)
347 "rocev2::Server::completeConnection",
348 "completeConnection already invoked; destroy and recreate "
349 "Server to re-establish the RC connection"));
350
351 // pmtu is blindly static_cast<ibv_mtu>() below; validate the range up
352 // front so a direct C++ caller (bypassing the Python wrapper which
353 // already validates) can't smuggle an undefined enum value into
354 // ibv_modify_qp. Mirrors RoCEv2Server.__init__'s 1..5 check.
355 if (pmtu < 1 || pmtu > 5)
357 "rocev2::Server::completeConnection",
358 "pmtu must be in the range 1..5 "
359 "(1=256 2=512 3=1024 4=2048 5=4096); got %u", pmtu));
360
361 log_->info("completeConnection: fpgaQpn=0x%06x fpgaRqPsn=0x%06x minRnrTimer=%u",
362 fpgaQpn, fpgaRqPsn, minRnrTimer);
363
364 // Pre-post all receive WRs BEFORE moving to RTR so no incoming
365 // RDMA WRITE-with-Immediate finds an empty RQ (which would cause RNR).
366 for (uint32_t i = 0; i < numBufs_; ++i) postRecvWr(i);
367 log_->info("Pre-posted %u recv WRs", numBufs_);
368
369 // QP: INIT → RTR
370 {
371 union ibv_gid dgid;
372 memcpy(dgid.raw, fpgaGid_, 16);
373
374 struct ibv_qp_attr attr;
375 memset(&attr, 0, sizeof(attr));
376 attr.qp_state = IBV_QPS_RTR;
377 attr.path_mtu = static_cast<ibv_mtu>(pmtu);
378 attr.dest_qp_num = fpgaQpn;
379 attr.rq_psn = fpgaRqPsn;
380 attr.max_dest_rd_atomic = 16;
381 attr.min_rnr_timer = minRnrTimer;
382 attr.ah_attr.is_global = 1;
383 attr.ah_attr.grh.dgid = dgid;
384 attr.ah_attr.grh.sgid_index = gidIndex_;
385 attr.ah_attr.grh.hop_limit = 64;
386 attr.ah_attr.port_num = ibPort_;
387 attr.ah_attr.sl = 0;
388
389 if (ibv_modify_qp(qp_, &attr,
390 IBV_QP_STATE |
391 IBV_QP_AV |
392 IBV_QP_PATH_MTU |
393 IBV_QP_DEST_QPN |
394 IBV_QP_RQ_PSN |
395 IBV_QP_MAX_DEST_RD_ATOMIC |
396 IBV_QP_MIN_RNR_TIMER))
397 throw(rogue::GeneralError::create("rocev2::Server::completeConnection",
398 "QP INIT→RTR failed"));
399 }
400
401 log_->info("QP → RTR (minRnrTimer=%u)", minRnrTimer);
402
403 // QP: RTR → RTS
404 {
405 struct ibv_qp_attr attr;
406 memset(&attr, 0, sizeof(attr));
407 attr.qp_state = IBV_QPS_RTS;
408 attr.sq_psn = hostSqPsn_;
409 attr.timeout = 14;
410 attr.retry_cnt = 3;
411 attr.rnr_retry = 7; // infinite (host SQ never transmits; cosmetic consistency)
412 attr.max_rd_atomic = 16;
413
414 if (ibv_modify_qp(qp_, &attr,
415 IBV_QP_STATE |
416 IBV_QP_SQ_PSN |
417 IBV_QP_TIMEOUT |
418 IBV_QP_RETRY_CNT |
419 IBV_QP_RNR_RETRY |
420 IBV_QP_MAX_QP_RD_ATOMIC))
421 throw(rogue::GeneralError::create("rocev2::Server::completeConnection",
422 "QP RTR→RTS failed"));
423 }
424
425 log_->info("QP → RTS — ready to receive RDMA SENDs");
426
427 // Start receive thread
428 std::shared_ptr<int> scopePtr = std::make_shared<int>(0);
429 threadEn_.store(true);
430 thread_ = new std::thread(&rpr::Server::runThread, this,
431 std::weak_ptr<int>(scopePtr));
432
433#ifndef __MACH__
434 pthread_setname_np(thread_->native_handle(), "RoCEv2Server");
435#endif
436}
437
438// ---------------------------------------------------------------------------
439// getGid
440// ---------------------------------------------------------------------------
441std::string rpr::Server::getGid() const {
442 std::ostringstream oss;
443 for (int i = 0; i < 16; i += 2) {
444 if (i) oss << ':';
445 oss << std::hex << std::setfill('0')
446 << std::setw(2) << static_cast<int>(hostGid_[i])
447 << std::setw(2) << static_cast<int>(hostGid_[i+1]);
448 }
449 return oss.str();
450}
451
452// ---------------------------------------------------------------------------
453// postRecvWr — post a receive WR for slot `slot`
454// wr_id == slot index so no lookup is needed on completion
455// ---------------------------------------------------------------------------
456void rpr::Server::postRecvWr(uint32_t slot) {
457 // Defensive: slot indexes into slab_; a corrupted wr_id (from the CQ)
458 // or meta (from retBuffer) must not produce an out-of-bounds slab
459 // pointer. Normal control flow keeps slot < numBufs_ because we set
460 // wr_id = slot at post time and encode slot in meta at createBuffer
461 // time, but the check is cheap and catches any future regression.
462 if (slot >= numBufs_)
463 throw(rogue::GeneralError::create("rocev2::Server::postRecvWr",
464 "slot=%u out of range (numBufs=%u)",
465 slot, numBufs_));
466
467 uint8_t* bufStart = slab_ + (static_cast<uint64_t>(slot) * bufSize_);
468
469 struct ibv_sge sge;
470 memset(&sge, 0, sizeof(sge));
471 sge.addr = reinterpret_cast<uint64_t>(bufStart);
472 sge.length = bufSize_;
473 sge.lkey = mr_->lkey;
474
475 struct ibv_recv_wr wr;
476 memset(&wr, 0, sizeof(wr));
477 wr.wr_id = static_cast<uint64_t>(slot);
478 wr.sg_list = &sge;
479 wr.num_sge = 1;
480 wr.next = nullptr;
481
482 struct ibv_recv_wr* bad = nullptr;
483 if (ibv_post_recv(qp_, &wr, &bad))
484 throw(rogue::GeneralError::create("rocev2::Server::postRecvWr",
485 "ibv_post_recv failed (slot=%u)", slot));
486}
487
488// ---------------------------------------------------------------------------
489// retBuffer — zero-copy hook
490//
491// Called by Buffer::~Buffer() when the last FramePtr holding this slot is
492// released by downstream. We re-post the slot to the QP so the FPGA can
493// write into it again.
494//
495// meta lower 24 bits = slot index (set in createBuffer() call in runThread)
496// ---------------------------------------------------------------------------
497void rpr::Server::retBuffer(uint8_t* data, uint32_t meta, uint32_t rawSize) {
498 uint32_t slot = meta & 0x00FFFFFF;
499
500 log_->debug("retBuffer: re-posting slot=%u", slot);
501
502 if (threadEn_.load() && qp_) {
503 try {
504 postRecvWr(slot);
505 } catch (...) {
506 // Swallow errors during shutdown
507 }
508 }
509
510 decCounter(rawSize);
511}
512
513// ---------------------------------------------------------------------------
514// processCompletion — handle one receive completion (zero-copy).
515// Decodes the immediate, wraps the slab slot as a rogue Buffer, and forwards it
516// downstream; retBuffer() re-posts the slot's credit when downstream releases
517// the buffer. On IBV_WC_WR_FLUSH_ERR (QP in ERROR) it clears threadEn_ so the
518// caller's drain/poll loop exits.
519// ---------------------------------------------------------------------------
520void rpr::Server::processCompletion(struct ibv_wc& wc) {
521 uint32_t slot = static_cast<uint32_t>(wc.wr_id);
522
523 // Defensive: every posted WR sets wr_id = slot with slot < numBufs_; a
524 // corrupted wr_id must not be dereferenced as a slab offset below.
525 if (slot >= numBufs_) {
526 log_->warning("CQ returned out-of-range wr_id=%" PRIu64
527 " (numBufs=%u); discarding",
528 static_cast<uint64_t>(wc.wr_id), numBufs_);
529 return;
530 }
531
532 if (wc.status != IBV_WC_SUCCESS) {
533 log_->warning("CQ error: %s (slot=%u)", ibv_wc_status_str(wc.status), slot);
534
535 if (wc.status == IBV_WC_WR_FLUSH_ERR) {
536 // QP transitioned to ERROR; remaining posted WRs flush with this
537 // status. Do NOT re-post — signal the poll loop to stop.
538 log_->error("QP in ERROR state (WR_FLUSH_ERR); exiting CQ poll thread");
539 threadEn_.store(false);
540 return;
541 }
542
543 postRecvWr(slot);
544 return;
545 }
546
547 // RDMA-SEND-with-immediate completes as IBV_WC_RECV with the IBV_WC_WITH_IMM
548 // flag set (RDMA-WRITE-with-immediate would have been IBV_WC_RECV_RDMA_WITH_IMM).
549 if (wc.opcode != IBV_WC_RECV || !(wc.wc_flags & IBV_WC_WITH_IMM)) {
550 log_->warning("Unexpected opcode %d / wc_flags 0x%x (slot=%u), re-posting",
551 wc.opcode, wc.wc_flags, slot);
552 postRecvWr(slot);
553 return;
554 }
555
556 // Decode immediate value: bits [7:0] = channel id; bits [31:8] = the
557 // free-running ring position the FPGA stamped (addrCount, informational).
558 // With RDMA-SEND (two-sided) the payload landed in the CONSUMED recv-WR's
559 // SGE buffer (= slot). RoCE carries imm_data in network byte order.
560 uint32_t imm = ntohl(wc.imm_data);
561 uint8_t channel = static_cast<uint8_t>(imm & 0xFF);
562 uint32_t dataSlot = (imm >> 8) & 0x00FFFFFF;
563 uint32_t payloadLen = wc.byte_len;
564
565 if (payloadLen == 0 || payloadLen > bufSize_) {
566 log_->warning("Bad payload len=%u (slot=%u), re-posting", payloadLen, slot);
567 postRecvWr(slot);
568 return;
569 }
570
571 // Zero-copy: the SEND landed the payload at slab_ + slot*bufSize_; wrap it
572 // as a rogue Buffer. The meta is the same slot so retBuffer() re-posts THAT
573 // credit when downstream releases the buffer.
574 uint8_t* slotPtr = slab_ + (static_cast<uint64_t>(slot) * bufSize_);
575
576 ris::BufferPtr buff = createBuffer(slotPtr,
577 slot & 0x00FFFFFF,
578 payloadLen,
579 bufSize_);
580 buff->setPayload(payloadLen);
581
582 ris::FramePtr frame = ris::Frame::create();
583 frame->appendBuffer(buff);
584 frame->setChannel(channel);
585 frame->setFirstUser(SsiSof);
586 frame->setLastUser(0);
587
588 log_->debug("RX wrId=%u dataSlot=%u channel=%u len=%u",
589 slot, dataSlot, channel, payloadLen);
590
591 sendFrame(frame);
592
593 frameCount_.fetch_add(1, std::memory_order_relaxed);
594 byteCount_.fetch_add(payloadLen, std::memory_order_relaxed);
595}
596
597// ---------------------------------------------------------------------------
598// runThread — event-driven CQ completion loop (zero-copy, no busy-poll)
599//
600// Blocks in select() on the completion-channel fd (raised when the CQ delivers
601// an event) and the self-pipe wakeFd_ (raised by stop()). On a CQ event it
602// drains the channel, RE-ARMS before draining the CQ — so a completion that
603// lands during the drain still raises a fresh event rather than being missed —
604// then processes every ready completion. There is NO sleep(): the kernel
605// parks the thread until a completion or shutdown actually occurs, so the
606// receive rate is not capped by a fixed poll-then-sleep cadence.
607// ---------------------------------------------------------------------------
608void rpr::Server::runThread(std::weak_ptr<int> lockPtr) {
609 while (!lockPtr.expired()) continue;
610
611 log_->logThreadId();
612 log_->info("RoCEv2 receive thread started (event-driven)");
613
614 rogue::GilRelease noGil; // Release GIL for the entire thread;
615 // sendFrame() re-acquires via ScopedGil when
616 // calling Python slaves.
617
618 constexpr int kPollBatch = 16;
619 struct ibv_wc wcArr[kPollBatch];
620
621 const int cqFd = comp_channel_->fd;
622 const int wakeFd = wakeFd_[0];
623 const int maxFd = (cqFd > wakeFd ? cqFd : wakeFd) + 1;
624
625 // The loop body is wrapped in try/catch so that an ibverbs failure
626 // (postRecvWr throws rogue::GeneralError on ibv_post_recv failure) shuts the
627 // thread down cleanly instead of escaping the thread entry point and
628 // triggering std::terminate.
629 try {
630 // Active-poll while completions are flowing — no sleep and no per-batch
631 // interrupt/event latency (ibv_poll_cq reads the CQ from memory). The
632 // RDMA-SEND flow control is closed-loop: the FPGA self-throttles to our
633 // recv-WR re-post rate, so any per-batch wait (a sleep OR a completion-
634 // event/interrupt that is subject to NIC coalescing) lengthens the
635 // credit-return latency, the FPGA delivers in bursts, the CQ empties
636 // between batches, and throughput collapses to a bursty half-rate
637 // equilibrium. Polling keeps the re-post latency low so the loop stays
638 // in the full-rate regime.
639 //
640 // Only when the CQ has stayed empty for a bounded spin budget (the
641 // source is genuinely idle, not a brief inter-batch gap) do we ARM the
642 // completion channel and block in select() — parking the thread with no
643 // CPU spin and no sleep until the next completion or a shutdown wake.
644 constexpr uint32_t kSpinBudget = 100000; // empty polls (~sub-us each) before parking
645 uint32_t idleSpins = 0;
646
647 while (threadEn_.load()) {
648 int n = ibv_poll_cq(cq_, kPollBatch, wcArr);
649 if (n < 0) {
650 log_->error("ibv_poll_cq error (%d); exiting CQ poll thread", n);
651 break;
652 }
653 if (n > 0) {
654 for (int k = 0; k < n; ++k) processCompletion(wcArr[k]);
655 idleSpins = 0;
656 continue;
657 }
658
659 // CQ empty: spin a bounded number of polls so a brief inter-batch
660 // gap does not pay the event-wake latency.
661 if (++idleSpins < kSpinBudget) continue;
662 idleSpins = 0;
663
664 // Sustained idle: arm the channel, then re-check once (a completion
665 // may have landed between the last poll and the arm) before parking.
666 if (ibv_req_notify_cq(cq_, 0)) {
667 log_->error("ibv_req_notify_cq failed; exiting CQ poll thread");
668 break;
669 }
670 n = ibv_poll_cq(cq_, kPollBatch, wcArr);
671 if (n < 0) {
672 log_->error("ibv_poll_cq error (%d); exiting CQ poll thread", n);
673 break;
674 }
675 if (n > 0) {
676 for (int k = 0; k < n; ++k) processCompletion(wcArr[k]);
677 continue; // armed; the pending event is consumed at the next park
678 }
679
680 // Truly idle and armed: park until a completion event or a shutdown
681 // wake. No timeout — the self-pipe guarantees prompt, sleepless exit.
682 fd_set rfds;
683 FD_ZERO(&rfds);
684 FD_SET(cqFd, &rfds);
685 FD_SET(wakeFd, &rfds);
686 int s = select(maxFd, &rfds, nullptr, nullptr, nullptr);
687 if (s < 0) {
688 if (errno == EINTR) continue;
689 log_->error("select() failed (errno=%d); exiting CQ poll thread", errno);
690 break;
691 }
692 if (FD_ISSET(wakeFd, &rfds)) {
693 uint8_t drainBuf[64];
694 while (read(wakeFd, drainBuf, sizeof(drainBuf)) > 0) {}
695 break;
696 }
697 if (FD_ISSET(cqFd, &rfds)) {
698 // Consume every queued CQ event (fd is non-blocking) and ack
699 // them as a batch so the channel fd clears, then resume polling.
700 struct ibv_cq* evCq;
701 void* evCtx;
702 int events = 0;
703 while (ibv_get_cq_event(comp_channel_, &evCq, &evCtx) == 0) ++events;
704 if (events) ibv_ack_cq_events(cq_, events);
705 }
706 }
707 } catch (const rogue::GeneralError& e) {
708 log_->error("RoCEv2 receive thread exiting on ibverbs error: %s", e.what());
709 } catch (const std::exception& e) {
710 log_->error("RoCEv2 receive thread exiting on exception: %s", e.what());
711 } catch (...) {
712 log_->error("RoCEv2 receive thread exiting on unknown exception");
713 }
714
715 threadEn_.store(false);
716 log_->info("RoCEv2 receive thread stopped");
717}
718
719// ---------------------------------------------------------------------------
720// acceptFrame — TX not supported. The parameter is required by the
721// stream::Slave contract but intentionally unused; name omitted to silence
722// -Wunused-parameter without a `(void)frame;` cast in the body.
723// ---------------------------------------------------------------------------
724void rpr::Server::acceptFrame(ris::FramePtr /*frame*/) {
725 log_->warning("RoCEv2 Server::acceptFrame: TX not supported, dropping");
726}
727
728// ---------------------------------------------------------------------------
729// stop / destructor
730// ---------------------------------------------------------------------------
731void rpr::Server::stop() {
732 // Signal the thread to exit if it is still running. Always join /
733 // delete thread_ when it is non-null: runThread() may have already
734 // cleared threadEn_ itself (e.g. on IBV_WC_WR_FLUSH_ERR or a caught
735 // exception), which would otherwise leave thread_ joinable and cause
736 // ~std::thread to terminate the process.
737 threadEn_.store(false);
738
739 // Break the receive thread out of its blocking select() immediately by
740 // making wakeFd_[0] readable — no timeout/poll wait needed for shutdown.
741 if (wakeFd_[1] >= 0) {
742 const uint8_t one = 1;
743 ssize_t wr = write(wakeFd_[1], &one, 1);
744 (void)wr; // best-effort; the thread also re-checks threadEn_
745 }
746
747 if (thread_) {
748 if (thread_->joinable()) thread_->join();
749 delete thread_;
750 thread_ = nullptr;
751 }
752 cleanupResources();
753}
754
755rpr::Server::~Server() { this->stop(); }
756
757// ---------------------------------------------------------------------------
758// Python bindings
759// ---------------------------------------------------------------------------
760void rpr::Server::setup_python() {
761#ifndef NO_PYTHON
762 bp::class_<rpr::Server,
764 bp::bases<rpr::Core, ris::Master, ris::Slave>,
765 boost::noncopyable>(
766 "Server",
767 bp::init<std::string, uint8_t, uint8_t, uint32_t, uint32_t>(
768 (bp::arg("deviceName"),
769 bp::arg("ibPort") = 1,
770 bp::arg("gidIndex") = 0,
771 bp::arg("maxPayload") = rpr::DefaultMaxPayload,
772 bp::arg("rxQueueDepth") = rpr::DefaultRxQueueDepth)))
773 .def("create", &rpr::Server::create)
774 .staticmethod("create")
775 .def("setFpgaGid", &rpr::Server::setFpgaGid)
776 .def("completeConnection", &rpr::Server::completeConnection,
777 (bp::arg("fpgaQpn"),
778 bp::arg("fpgaRqPsn"),
779 bp::arg("pmtu") = 5,
780 bp::arg("minRnrTimer") = 1))
781 .def("getQpn", &rpr::Server::getQpn)
782 .def("getGid", &rpr::Server::getGid)
783 .def("getRqPsn", &rpr::Server::getRqPsn)
784 .def("getSqPsn", &rpr::Server::getSqPsn)
785 .def("getMrAddr", &rpr::Server::getMrAddr)
786 .def("getMrRkey", &rpr::Server::getMrRkey)
787 .def("getFrameCount", &rpr::Server::getFrameCount)
788 .def("getByteCount", &rpr::Server::getByteCount)
789 .def("stop", &rpr::Server::stop);
790
791 bp::implicitly_convertible<rpr::ServerPtr, rpr::CorePtr>();
792 bp::implicitly_convertible<rpr::ServerPtr, ris::MasterPtr>();
793 bp::implicitly_convertible<rpr::ServerPtr, ris::SlavePtr>();
794#endif
795}
Generic Rogue exception type.
char const * what() const
Returns exception text for standard exception handling.
static GeneralError create(std::string src, const char *fmt,...)
Creates a formatted error instance.
RAII helper that releases the Python GIL for a scope.
Definition GilRelease.h:36
static std::shared_ptr< rogue::Logging > create(const std::string &name, bool quiet=false)
Creates a logger instance.
Definition Logging.cpp:95
struct ibv_pd * pd_
Definition Core.h:47
struct ibv_context * ctx_
Definition Core.h:46
uint32_t maxPayload() const
Definition Core.h:65
std::shared_ptr< rogue::interfaces::stream::Buffer > BufferPtr
Shared pointer alias for Buffer.
Definition Buffer.h:270
std::shared_ptr< rogue::interfaces::stream::Frame > FramePtr
Shared pointer alias for Frame.
Definition Frame.h:549
std::shared_ptr< rogue::protocols::rocev2::Server > ServerPtr
Definition Server.h:161
static const uint32_t DefaultMaxPayload
Definition Core.h:40
static const uint32_t DefaultRxQueueDepth
Definition Core.h:35
static const uint8_t SsiSof
Definition Server.cpp:58