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
+58
View File
@@ -27,6 +27,12 @@ package rcserver
#cgo LDFLAGS: -L${SRCDIR}/.. -l:librcserver.a -lstdc++ -ldl -lpthread
#include "rc_server_abi.h"
#include <stdlib.h>
// Shim: a C function pointer that forwards into the exported Go
// sink. Go closures cannot be stored as C callbacks, so the
// level/file/line/msg are marshalled through this fixed trampoline.
extern void rcgo_log_sink(void *ctx, int level, char *msg,
char *file, int line);
*/
import "C"
@@ -34,6 +40,7 @@ import (
"context"
"errors"
"fmt"
"log"
"runtime"
"sync"
"sync/atomic"
@@ -76,6 +83,10 @@ type DeviceOpts struct {
// Concurrency slots (0 picks the C-side default 64/32).
MaxReadySlots uint32
MaxStageSlots uint32
// Debug enables level-2 (debug) RC diagnostics through the
// log sink; false keeps error-only reporting. Mirrors the
// gateway --debug flag.
Debug bool
}
// PrincipalID is the SHA-256 digest identifying the requester.
@@ -174,6 +185,52 @@ func (s *RCSvc) Context() context.Context {
return s.ctx
}
// rcLogSink receives every diagnostic line the C server emits.
// It is stateless and process-global on purpose: the sink must be
// valid from init through destroy, independent of any single
// RCSvc instance. Lines are copied out immediately (the C msg
// buffer is only valid for the duration of the callback) and
// written to the standard error logger. rcLogMu serializes the
// copy because log.Logger is safe for concurrent use but keeps
// a single writer cheap.
var (
rcLogMu sync.Mutex
rcLogLevel atomic.Int32
rcLog = log.New(log.Writer(), "vgwrdma rc: ", 0)
)
//export rcgo_log_sink
func rcgo_log_sink(_ unsafe.Pointer, level C.int, msg *C.char,
file *C.char, line C.int) {
// Level gating mirrors the C contract: 0 (error) always
// arrives here; 2 (debug) only when the service opted in.
if int32(level) > rcLogLevel.Load() {
return
}
m := C.GoString(msg)
f := ""
if file != nil {
f = C.GoString(file)
}
rcLogMu.Lock()
rcLog.Printf("%s (%s:%d) level=%d", m, f, int(line), int(level))
rcLogMu.Unlock()
}
// installLogSink wires the C server's diagnostics into the Go
// sink. debug selects level 2 (debug) versus level 0 (errors
// only); the C side keeps stderr error reporting when the sink
// is absent, so a nil install is a no-op by design.
func installLogSink(srv *C.rc_server, debug bool) {
if debug {
rcLogLevel.Store(2)
} else {
rcLogLevel.Store(0)
}
C.rc_server_set_log_sink(srv,
(*[0]byte)(C.rcgo_log_sink), nil)
}
// Init opens the verbs device and returns a service.
func Init(opts DeviceOpts) (*RCSvc, error) {
copts := C.rc_device_opts{
@@ -201,6 +258,7 @@ func Init(opts DeviceOpts) (*RCSvc, error) {
if rc := C.rc_server_init(&copts, &srv); rc != C.RC_OK {
return nil, fmt.Errorf("rcserver: init failed: status %d", int(rc))
}
installLogSink(srv, opts.Debug)
ctx, cancel := context.WithCancel(context.Background())
return &RCSvc{srv: srv, ctx: ctx, cancel: cancel}, nil
}