Merge pull request #2333 from potatogim/rdma-rc-go-binding

rdma: add the Go binding for the RC session server
This commit is contained in:
Ben McClelland
2026-08-31 13:41:43 -07:00
committed by GitHub
2 changed files with 751 additions and 0 deletions
+564
View File
@@ -0,0 +1,564 @@
// Copyright 2026 Versity Software
// This file is 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 ANY KIND, either express or implied.
// See the License for the specific language governing permissions
// and limitations under the License.
//go:build linux && amd64 && cgo
// Package rcserver binds the RC session server (rdma/librcserver.a)
// that implements the hipobj-rc-v2 two-phase transfer protocol.
//
// The gateway owns routing, authentication, and the object backend;
// the C side owns the session state machine, QP/CQ/MR lifetimes, the
// data phase, and session reaping. Every cgo call is synchronous
// from the request goroutine.
package rcserver
/*
#cgo CFLAGS: -I${SRCDIR}/../../cuwrapper/rc
#cgo LDFLAGS: -L${SRCDIR}/.. -l:librcserver.a -lstdc++ -ldl -lpthread
#include "rc_server_abi.h"
#include <stdlib.h>
*/
import "C"
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"sync/atomic"
"unsafe"
)
// Status codes mirrored from rc_server_abi.h.
const (
StatusOK = C.RC_OK
StatusErrArg = C.RC_E_ARG
StatusErrState = C.RC_E_STATE
StatusErrSession = C.RC_E_SESSION
StatusErrStale = C.RC_E_STALE
StatusErrNoSession = C.RC_E_NO_SESSION
StatusErrDouble = C.RC_E_DOUBLE
StatusErrWire = C.RC_E_WIRE
StatusErrShort = C.RC_E_SHORT
StatusErrTrunc = C.RC_E_TRUNC
StatusErrLimit = C.RC_E_LIMIT
StatusErrInternal = C.RC_E_INTERNAL
)
// DeviceOpts configures device selection and resource limits.
type DeviceOpts struct {
// GidHint selects the device whose GID starts with this dotted
// prefix; empty picks the first verbs device.
GidHint string
Port uint8
GidIdx int
// Global limits.
MaxSessions uint32
MaxStagingBytes uint64
MaxQPs uint32
MaxUserSessions uint32
MaxUserStagingBytes uint64
MaxUserQPs uint32
// Timeouts in milliseconds.
TPrepMs uint64
TExecMs uint64
// Concurrency slots (0 picks the C-side default 64/32).
MaxReadySlots uint32
MaxStageSlots uint32
}
// PrincipalID is the SHA-256 digest identifying the requester.
type PrincipalID [32]byte
// PrepareRequest carries the PREPARE wire parameters.
type PrepareRequest struct {
Principal PrincipalID
Op uint8 // 0 = GET, 1 = PUT
Target string
Offset uint64
Size uint64
ClientPsn uint32
Cookie uint32
ClientToken string // 88-hex, optional
}
// PrepareResponse is the PREPARE reply.
type PrepareResponse struct {
SessionID string
ServerQpn uint32
ServerPsn uint32
StagingAddr uint64
StagingRkey uint32
ReplyToken string
}
// Handle is an opaque consume-once lease identifier.
type Handle struct {
Epoch uint64
Nonce uint64
}
// StagingLease is a borrowed GET staging buffer. The caller must
// return it with FinishStaging exactly once.
type StagingLease struct {
Buf []byte
Capacity int
Handle Handle
}
// PutView is a borrowed PUT data view. The caller must return it
// with FinishPut exactly once.
type PutView struct {
Buf []byte
Len int
Handle Handle
}
// ReadyRequest carries the READY wire parameters.
type ReadyRequest struct {
Principal PrincipalID
SessionID string
Cookie uint32
ClientQpn uint32
ClientMrAddr uint64
ClientMrRkey uint32
}
// ReadyResponse is the READY reply.
type ReadyResponse struct {
BytesTransferred uint64
CookieEcho uint32
// Outcome mirrors the RC_READY_* enum: returned atomically
// with the transfer result so a concurrent READY cannot
// rewrite it between the transfer and the read.
Outcome int
Etag string
VersionID string
}
// SessionInfo describes a session for READY re-authorization.
type SessionInfo struct {
Op uint8 // 0 = GET, 1 = PUT
Target string
}
// RCSvc owns the RC session server. Handlers must call TryEnter
// before any session call and Leave when the request completes
// (defer). Close stops admissions, cancels the service context
// (bound to every handler's backend I/O so in-flight requests
// unblock), waits for every entered call to leave, then tears the
// server down; it is idempotent and safe from any goroutine.
type RCSvc struct {
srv *C.rc_server
closing atomic.Bool
ops atomic.Int64
once sync.Once
ctx context.Context
cancel context.CancelFunc
}
// Context returns the service-lifetime context. Handlers bind
// their backend calls to it so Close unblocks in-flight I/O.
func (s *RCSvc) Context() context.Context {
return s.ctx
}
// Init opens the verbs device and returns a service.
func Init(opts DeviceOpts) (*RCSvc, error) {
copts := C.rc_device_opts{
port: C.uint8_t(opts.Port),
gid_index: C.int(opts.GidIdx),
max_sessions: C.uint32_t(opts.MaxSessions),
max_user_sessions: C.uint32_t(opts.MaxUserSessions),
max_staging_bytes: C.uint64_t(opts.MaxStagingBytes),
max_user_staging_bytes: C.uint64_t(opts.MaxUserStagingBytes),
max_qps: C.uint32_t(opts.MaxQPs),
max_user_qps: C.uint32_t(opts.MaxUserQPs),
t_prep_ms: C.uint64_t(opts.TPrepMs),
t_exec_ms: C.uint64_t(opts.TExecMs),
max_ready_slots: C.uint32_t(opts.MaxReadySlots),
max_stage_slots: C.uint32_t(opts.MaxStageSlots),
}
var gidHint *C.char
if opts.GidHint != "" {
gidHint = C.CString(opts.GidHint)
defer C.free(unsafe.Pointer(gidHint))
}
copts.gid_hint = gidHint
var srv *C.rc_server
if rc := C.rc_server_init(&copts, &srv); rc != C.RC_OK {
return nil, fmt.Errorf("rcserver: init failed: status %d", int(rc))
}
ctx, cancel := context.WithCancel(context.Background())
return &RCSvc{srv: srv, ctx: ctx, cancel: cancel}, nil
}
// TryEnter admits a request into the service. It returns false once
// Close has begun; the caller must not touch the service then.
func (s *RCSvc) TryEnter() bool {
if s.closing.Load() {
return false
}
s.ops.Add(1)
if s.closing.Load() {
s.ops.Add(-1)
return false
}
return true
}
// Leave releases a TryEnter admission.
func (s *RCSvc) Leave() {
s.ops.Add(-1)
}
// Close shuts the service down: it stops admissions, marks every
// session for reaping, waits for all in-flight handlers to
// finish, and destroys the server. Safe to call multiple times.
//
// Convergence: every blocking call an RC handler makes after
// TryEnter is bounded - GET staging, PUT commit, and the
// bucket-ACL lookup run under the service context and unblock on
// the cancel below; the fresh IAM revalidation selects on the
// same context and its lookup goroutines are capped (a stalled
// context-less IAM backend can strand at most a fixed number of
// them; further requests fail fast); and the remaining
// authorization helpers (VerifyAccess, CheckObjectAccess)
// consume the fiber request context, which the gateway shuts
// down before RunVersityGW returns - and Close runs after
// RunVersityGW returns in every current caller - so those calls
// are cancelled by the fiber shutdown that precedes Close.
func (s *RCSvc) Close() {
s.once.Do(func() {
s.closing.Store(true)
// Cancel the service context first: handlers bind their
// backend I/O to it, so in-flight GET staging and PUT
// commits unblock instead of stretching the ops wait
// below into an unbounded spin.
s.cancel()
if s.srv != nil {
// Mark every session reap-pending first so the
// destroy path below can actually collect them;
// sessions with drained refs are reaped here.
C.rc_cancel_all(s.srv)
}
for s.ops.Load() > 0 {
runtime.Gosched()
}
if s.srv != nil {
C.rc_server_destroy(s.srv)
s.srv = nil
}
})
}
func statusErr(rc C.int, op string) error {
if rc == C.RC_OK {
return nil
}
return fmt.Errorf("rcserver: %s: status %d", op, int(rc))
}
// Sentinel errors matched by errors.Is for each ABI status.
var (
ErrArg = errors.New("rcserver: invalid argument")
ErrState = errors.New("rcserver: wrong session state")
ErrSession = errors.New("rcserver: owner mismatch")
ErrStale = errors.New("rcserver: stale handle or session")
ErrNoSession = errors.New("rcserver: no such session")
ErrDouble = errors.New("rcserver: duplicate borrow")
ErrWire = errors.New("rcserver: verbs transfer failure")
ErrShort = errors.New("rcserver: short transfer")
ErrTrunc = errors.New("rcserver: value too long")
ErrLimit = errors.New("rcserver: resource limit")
ErrInternal = errors.New("rcserver: internal error")
)
// statusError maps an ABI status to a descriptive error value.
func statusError(rc C.int) error {
switch rc {
case C.RC_OK:
return nil
case C.RC_E_ARG:
return ErrArg
case C.RC_E_STATE:
return ErrState
case C.RC_E_SESSION:
return ErrSession
case C.RC_E_STALE:
return ErrStale
case C.RC_E_NO_SESSION:
return ErrNoSession
case C.RC_E_DOUBLE:
return ErrDouble
case C.RC_E_WIRE:
return ErrWire
case C.RC_E_SHORT:
return ErrShort
case C.RC_E_TRUNC:
return ErrTrunc
case C.RC_E_LIMIT:
return ErrLimit
default:
return ErrInternal
}
}
func strIn(s string) C.rc_str_in {
if s == "" {
return C.rc_str_in{ptr: nil, len: 0}
}
// The pointer must stay alive across the cgo call; every
// wrapper keeps the string referenced until after the call.
return C.rc_str_in{
ptr: (*C.char)(unsafe.Pointer(unsafe.StringData(s))),
len: C.uint32_t(len(s)),
}
}
// cStr reads a fixed C char array of length n into a Go string.
func cStr(p *C.char, n C.uint32_t) string {
if n == 0 {
return ""
}
return C.GoStringN(p, C.int(n))
}
// Prepare creates a session (PREPARE).
func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:])
var token C.rc_str_in
if req.ClientToken != "" {
token = strIn(req.ClientToken)
}
target := strIn(req.Target)
creq := C.rc_prepare_req{
principal: principal,
op: C.uint8_t(req.Op),
target: target,
offset: C.uint64_t(req.Offset),
size: C.uint64_t(req.Size),
client_psn: C.uint32_t(req.ClientPsn),
cookie: C.uint32_t(req.Cookie),
client_token: token,
}
var cresp C.rc_prepare_resp
runtime.KeepAlive(req.ClientToken)
runtime.KeepAlive(req.Target)
rc := C.rc_prepare(s.srv, &creq, &cresp)
if rc != C.RC_OK {
return nil, statusError(rc)
}
return &PrepareResponse{
SessionID: cStr(&cresp.session_id[0], cresp.session_len),
ServerQpn: uint32(cresp.server_qpn),
ServerPsn: uint32(cresp.server_psn),
StagingAddr: uint64(cresp.staging_addr),
StagingRkey: uint32(cresp.staging_rkey),
ReplyToken: cStr(&cresp.reply_token[0], cresp.reply_len),
}, nil
}
// FinishPrepare commits or aborts a PREPARE.
func (s *RCSvc) FinishPrepare(sessionID string, committed bool) error {
if s.srv == nil {
return errors.New("rcserver: service closed")
}
in := strIn(sessionID)
rc := C.rc_finish_prepare(s.srv, in, C.int(btoi(committed)))
runtime.KeepAlive(sessionID)
return statusError(rc)
}
// BorrowStaging borrows the GET staging buffer.
func (s *RCSvc) BorrowStaging(sessionID string) (*StagingLease, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
in := strIn(sessionID)
var lease C.rc_staging_lease
rc := C.rc_borrow_staging(s.srv, in, &lease)
runtime.KeepAlive(sessionID)
if rc != C.RC_OK {
return nil, statusError(rc)
}
return &StagingLease{
Buf: unsafe.Slice((*byte)(unsafe.Pointer(lease.buf)),
lease.capacity),
Capacity: int(lease.capacity),
Handle: Handle{
Epoch: uint64(lease.handle.session_epoch),
Nonce: uint64(lease.handle.nonce),
},
}, nil
}
// FinishStaging returns a staging lease (GET).
func (s *RCSvc) FinishStaging(lease StagingLease, ok bool,
written int, etag, versionID string) error {
if s.srv == nil {
return errors.New("rcserver: service closed")
}
h := C.rc_handle{
session_epoch: C.uint64_t(lease.Handle.Epoch),
nonce: C.uint64_t(lease.Handle.Nonce),
}
clease := C.rc_staging_lease{handle: h}
inEtag := strIn(etag)
inVer := strIn(versionID)
rc := C.rc_finish_staging(s.srv, clease, C.int(btoi(ok)),
C.size_t(written), inEtag, inVer)
runtime.KeepAlive(etag)
runtime.KeepAlive(versionID)
return statusError(rc)
}
// SessionInfo looks up a session's op and target (READY auth).
func (s *RCSvc) SessionInfo(sessionID string, who PrincipalID) (*SessionInfo, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], who[:])
in := strIn(sessionID)
var out C.rc_session_info_resp
rc := C.rc_session_info(s.srv, in, principal, &out)
runtime.KeepAlive(sessionID)
if rc != C.RC_OK {
return nil, statusError(rc)
}
return &SessionInfo{
Op: uint8(out.op),
Target: cStr(&out.target[0], out.target_len),
}, nil
}
// ReadyTransfer runs the data phase (READY).
func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:])
in := strIn(req.SessionID)
creq := C.rc_ready_req{
principal: principal,
session_id: in,
cookie: C.uint32_t(req.Cookie),
client_qpn: C.uint32_t(req.ClientQpn),
client_mr_addr: C.uint64_t(req.ClientMrAddr),
client_mr_rkey: C.uint32_t(req.ClientMrRkey),
}
var cresp C.rc_ready_resp
rc := C.rc_ready_transfer(s.srv, &creq, &cresp)
runtime.KeepAlive(req.SessionID)
if rc != C.RC_OK {
return nil, statusError(rc)
}
return &ReadyResponse{
BytesTransferred: uint64(cresp.bytes_transferred),
CookieEcho: uint32(cresp.cookie_echo),
Outcome: int(cresp.outcome),
Etag: cStr(&cresp.etag[0], cresp.etag_len),
VersionID: cStr(&cresp.version_id[0], cresp.version_len),
}, nil
}
// Ready outcome detail codes (rc_ready_resp.outcome).
const (
ReadyOK = C.RC_READY_OK
ReadyBusy = C.RC_READY_BUSY
ReadyTimeout = C.RC_READY_TIMEOUT
ReadyVerifyFail = C.RC_READY_VERIFY_FAIL
ReadyWireFail = C.RC_READY_WIRE_FAIL
)
// GetPutData borrows the PUT data view.
func (s *RCSvc) GetPutData(sessionID string) (*PutView, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
in := strIn(sessionID)
var view C.rc_put_view
rc := C.rc_get_put_data(s.srv, in, &view)
runtime.KeepAlive(sessionID)
if rc != C.RC_OK {
return nil, statusError(rc)
}
return &PutView{
Buf: unsafe.Slice((*byte)(unsafe.Pointer(view.buf)), view.len),
Len: int(view.len),
Handle: Handle{
Epoch: uint64(view.handle.session_epoch),
Nonce: uint64(view.handle.nonce),
},
}, nil
}
// FinishPut returns a PUT data view with the commit outcome.
func (s *RCSvc) FinishPut(view PutView, committed bool,
etag, versionID string) error {
if s.srv == nil {
return errors.New("rcserver: service closed")
}
h := C.rc_handle{
session_epoch: C.uint64_t(view.Handle.Epoch),
nonce: C.uint64_t(view.Handle.Nonce),
}
cview := C.rc_put_view{handle: h}
inEtag := strIn(etag)
inVer := strIn(versionID)
rc := C.rc_finish_put(s.srv, cview, C.int(btoi(committed)),
inEtag, inVer)
runtime.KeepAlive(etag)
runtime.KeepAlive(versionID)
return statusError(rc)
}
// FinishFinal marks the FINAL response committed.
func (s *RCSvc) FinishFinal(sessionID string) error {
if s.srv == nil {
return errors.New("rcserver: service closed")
}
in := strIn(sessionID)
rc := C.rc_finish_final(s.srv, in)
runtime.KeepAlive(sessionID)
return statusError(rc)
}
// Cancel cancels a session (owner-checked).
func (s *RCSvc) Cancel(sessionID string, who PrincipalID) error {
if s.srv == nil {
return errors.New("rcserver: service closed")
}
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], who[:])
in := strIn(sessionID)
rc := C.rc_cancel(s.srv, in, principal)
runtime.KeepAlive(sessionID)
return statusError(rc)
}
func btoi(b bool) int {
if b {
return 1
}
return 0
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2026 Versity Software
// This file is 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 ANY KIND, either express or implied.
// See the License for the specific language governing permissions
// and limitations under the License.
//go:build !(linux && amd64 && cgo)
// Package rcserver binds the RC session server implementing the
// hipobj-rc-v2 two-phase transfer protocol. This file is a stub for
// platforms without RDMA support.
package rcserver
import "errors"
var errNotSupported = errors.New("rcserver: not supported on this platform")
// DeviceOpts configures device selection and resource limits.
type DeviceOpts struct {
GidHint string
Port uint8
GidIdx int
MaxSessions uint32
MaxStagingBytes uint64
MaxQPs uint32
MaxUserSessions uint32
MaxUserStagingBytes uint64
MaxUserQPs uint32
TPrepMs uint64
TExecMs uint64
MaxReadySlots uint32
MaxStageSlots uint32
}
// PrincipalID is the SHA-256 digest identifying the requester.
type PrincipalID [32]byte
// PrepareRequest carries the PREPARE wire parameters.
type PrepareRequest struct {
Principal PrincipalID
Op uint8
Target string
Offset uint64
Size uint64
ClientPsn uint32
Cookie uint32
ClientToken string
}
// PrepareResponse is the PREPARE reply.
type PrepareResponse struct {
SessionID string
ServerQpn uint32
ServerPsn uint32
StagingAddr uint64
StagingRkey uint32
ReplyToken string
}
// Handle is an opaque consume-once lease identifier.
type Handle struct {
Epoch uint64
Nonce uint64
}
// StagingLease is a borrowed GET staging buffer.
type StagingLease struct {
Buf []byte
Capacity int
Handle Handle
}
// PutView is a borrowed PUT data view.
type PutView struct {
Buf []byte
Len int
Handle Handle
}
// ReadyRequest carries the READY wire parameters.
type ReadyRequest struct {
Principal PrincipalID
SessionID string
Cookie uint32
ClientQpn uint32
ClientMrAddr uint64
ClientMrRkey uint32
}
// ReadyResponse is the READY reply.
type ReadyResponse struct {
BytesTransferred uint64
CookieEcho uint32
// Outcome mirrors the RC_READY_* enum.
Outcome int
Etag string
VersionID string
}
// SessionInfo describes a session for READY re-authorization.
type SessionInfo struct {
Op uint8
Target string
}
// RCSvc owns the RC session server (stub).
type RCSvc struct{}
// Init is a stub that always fails.
func Init(opts DeviceOpts) (*RCSvc, error) { return nil, errNotSupported }
// TryEnter is a stub.
func (s *RCSvc) TryEnter() bool { return false }
// Leave is a stub.
func (s *RCSvc) Leave() {}
// Close is a stub.
func (s *RCSvc) Close() {}
// Prepare is a stub.
func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) {
return nil, errNotSupported
}
// FinishPrepare is a stub.
func (s *RCSvc) FinishPrepare(sessionID string, committed bool) error {
return errNotSupported
}
// BorrowStaging is a stub.
func (s *RCSvc) BorrowStaging(sessionID string) (*StagingLease, error) {
return nil, errNotSupported
}
// FinishStaging is a stub.
func (s *RCSvc) FinishStaging(lease StagingLease, ok bool,
written int, etag, versionID string) error {
return errNotSupported
}
// SessionInfo is a stub.
func (s *RCSvc) SessionInfo(sessionID string, who PrincipalID) (*SessionInfo, error) {
return nil, errNotSupported
}
// ReadyTransfer is a stub.
func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) {
return nil, errNotSupported
}
// GetPutData is a stub.
func (s *RCSvc) GetPutData(sessionID string) (*PutView, error) {
return nil, errNotSupported
}
// FinishPut is a stub.
func (s *RCSvc) FinishPut(view PutView, committed bool,
etag, versionID string) error {
return errNotSupported
}
// FinishFinal is a stub.
func (s *RCSvc) FinishFinal(sessionID string) error {
return errNotSupported
}
// Cancel is a stub.
func (s *RCSvc) Cancel(sessionID string, who PrincipalID) error {
return errNotSupported
}
// Ready outcome detail codes (see the linux build).
const (
ReadyOK = 0
ReadyBusy = 1
ReadyTimeout = 2
ReadyVerifyFail = 3
ReadyWireFail = 4
)