rdma: add a log callback ABI to the RC data plane

Wire C-side diagnostics (session reap, READY data phase outcome,
init failures) through a sink callback so the gateway can surface
them next to its own logs instead of losing them in stderr noise.

The sink is a plain C function pointer installed once after init
and valid until destroy: the Go side registers a fixed cgo
trampoline (closures cannot cross the boundary), copies the
message immediately per the lifetime contract, and never runs
under the session map lock. Error-level lines keep the existing
stderr output; --debug enables the level-2 diagnostic stream.
This commit is contained in:
Jihyeon Gim
2026-09-05 19:46:00 +09:00
parent 980078d822
commit d782e622fc
5 changed files with 128 additions and 0 deletions
+57
View File
@@ -15,6 +15,8 @@
#include <thread>
#include <unordered_map>
#include <vector>
#include <cstdarg>
#include <cstdio>
#include "rc_ibv_host.h"
#include "v2_data_phase.h"
@@ -89,6 +91,13 @@ struct rc_server {
hipObj::DeviceHandle *device = nullptr;
SessionTable table;
rc_device_opts opts{};
/* Diagnostic sink: null keeps stderr-only error reporting.
* Reads/writes are plain loads/stores; the sink is installed
* once at init time (before the reaper starts) and only
* cleared by destroy after the reaper joined, so no thread
* races an in-flight sink pointer swap. */
rc_log_fn log_fn = nullptr;
void *log_ctx = nullptr;
std::atomic<uint64_t> epoch_counter{1};
/* resource accounting (global buckets; per-principal map). */
std::mutex acct_mtx;
@@ -117,6 +126,22 @@ struct rc_server {
namespace {
/* Emits a diagnostic line to the installed sink (level 0 keeps
* the stderr error stream intact by also printing there, so
* existing deployments do not lose the only log they had).
* Callers must not hold map_mtx/acct_mtx when calling. */
void rcLog(const rc_server *srv, int level, const char *file, int line,
const char *fmt, ...) {
char buf[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (level <= 0) fprintf(stderr, "%s\n", buf);
rc_log_fn fn = srv->log_fn;
if (fn) fn(srv->log_ctx, level, buf, file, line);
}
RcSession *findSession(rc_server *srv, const std::string &id) {
auto it = srv->sessions_map.find(id);
return it == srv->sessions_map.end() ? nullptr : it->second.get();
@@ -187,6 +212,16 @@ void reapSession(rc_server *srv, RcSession *s) {
s->core.qp = conn.qp; /* null on success, survivor on failure */
s->core.cq = conn.cq;
bool destroyed = q_ok && c_ok;
/* Terminal record for every session teardown path (expiry,
* CANCEL, and destroy); reap_pass may have missed the final
* state, so the last outcome observed at READY time travels
* with the log line. */
rcLog(srv, 2, __FILE__, __LINE__,
"rc: session reaped id=%s op=%s target=%.96s staged=%llu "
"qp_destroyed=%d",
s->core.id.c_str(), s->core.op.c_str(),
s->core.target.c_str(),
(unsigned long long)s->staging_len, (int)destroyed);
if (destroyed) {
/* Same policy as releaseStaging: a failed dereg leaves the
* MR registered against the shared PD, so the buffer stays
@@ -257,6 +292,12 @@ std::string encodeReplyToken(hipObj::DeviceHandle *dh, uint32_t qpn) {
extern "C" {
void rc_server_set_log_sink(rc_server *srv, rc_log_fn fn, void *ctx) {
if (!srv) return;
srv->log_fn = fn;
srv->log_ctx = ctx;
}
int rc_server_init(const rc_device_opts *opts, rc_server **out) {
if (!opts || !out) return RC_E_ARG;
if (!hipObj::ibv.ensureLoaded()) return RC_E_INTERNAL;
@@ -309,12 +350,15 @@ int rc_server_init(const rc_device_opts *opts, rc_server **out) {
}
if (!ctx) {
hipObj::ibv.free_device_list(devs);
fprintf(stderr, "rc: no verbs device matches gid_hint %.32s\n",
opts->gid_hint ? opts->gid_hint : "");
return RC_E_INTERNAL;
}
struct ibv_pd *pd = hipObj::ibv.alloc_pd(ctx);
hipObj::ibv.free_device_list(devs);
if (!pd) {
hipObj::ibv.close_device(ctx);
fprintf(stderr, "rc: alloc_pd failed\n");
return RC_E_INTERNAL;
}
srv->device = new hipObj::DeviceHandle();
@@ -798,6 +842,19 @@ int rc_ready_transfer(rc_server *srv, const rc_ready_req *req,
RcSession *after = findSession(srv, id);
if (!after) return RC_E_NO_SESSION;
/* Keep the wire-level reason (poll status vs post failure vs
* timeout) alongside the outcome the response carries, so a
* VerifyFail is diagnosable without re-running the transfer.
* Logged with the lock dropped: the sink must not block under
* map_mtx. */
int rlog = (r == hipObj::v2::DataPhaseResult::Ok)
? 2
: (r == hipObj::v2::DataPhaseResult::Busy ? 2 : 0);
rcLog(srv, rlog, __FILE__, __LINE__,
"rc: ready data phase session=%s op=%s outcome=%d bytes=%llu",
id.c_str(), after->core.op.c_str(), (int)r,
(unsigned long long)stats.bytes);
switch (r) {
case hipObj::v2::DataPhaseResult::Ok:
after->last_outcome = RC_READY_OK;