Files
zap/transport.go
Hanzo AI f42cdb6618 feat(zap): v2 generic API, bufpool, transport allocator + UMA/RDMA cgo
Three orthogonal additions, one wire format.

v2 package — generic ad-hoc schema dispatch (~/work/lux/zap/v2):
- README + IMPOSSIBILITY + BENCH_RESULTS document the v1↔v2 split.
- builder/field/iter/list/pool/registry/schema/view + codegen package
  emit v1-equivalent fast paths from declarative .zap sources.
- _compile_fail_test/ + examples/ cover negative and positive cases.
- v2_test.go exercises the generic dispatch path.
- doc.go in v1 marks v1 deprecated for *new* schema authoring; existing
  v1 buffers stay parseable by both v1 and v2.

bufpool — pooled read slabs (bufpool.go + bufpool_test.go):
- Message gains optional *bufRef; Release() returns the slab to its
  pool when sourced from the pooled read path, GC-managed otherwise.
- ParseHeader returns (data, rootOffset) without allocating *Message,
  used by zapv2 to avoid two allocations per parse.
- bench_read_test.go + node_quic_stream_test.go cover both paths.

Transport allocator + UMA/RDMA cgo (transport/*):
- Transport.Caps()/Buffer interface: capability + slab-typed buffer
  contract. Bytes() vs DevicePtr() decoupled so UMA and GPUDirect
  can share the same call surface.
- DPDK PMD + GPUDirect RDMA + CUDA UMA real cgo backends gated behind
  build tags; stub paths remain default on darwin / non-Linux.
- transport_test.go covers Caps reporting + Buffer lifecycle on the
  default transport.

LLM.md + go.mod follow the additions.
2026-06-04 17:06:59 -07:00

154 lines
5.9 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap
import (
"context"
"errors"
)
// Transport selects which network transport a Node uses.
//
// The default zero value, TransportTCP, preserves the historical
// behavior of NewNode (TCP + optional TLS via NodeConfig.TLS) so every
// existing caller keeps working untouched.
//
// TransportQUIC selects the QUIC transport defined in the quic
// subpackage. The quic subpackage must be imported anonymously by the
// process for TransportQUIC to be available; otherwise NewNode returns
// ErrTransportUnavailable.
type Transport int
const (
// TransportTCP is ZAP's original transport: framed Cap'n Proto
// over TCP, with optional TLS 1.3 supplied via NodeConfig.TLS.
// This is the default for backward compatibility.
TransportTCP Transport = iota
// TransportQUIC selects the QUIC transport from the quic
// subpackage. Requires:
//
// import _ "github.com/luxfi/zap/quic"
//
// in some package linked into the binary, so the QUIC factory
// registers itself at init time.
TransportQUIC
)
// ErrTransportUnavailable is returned by NewNode when the requested
// Transport has no registered factory. For TransportQUIC, this means
// the quic subpackage was not imported.
var ErrTransportUnavailable = errors.New("zap: transport unavailable (did you import _ \"github.com/luxfi/zap/quic\"?)")
// TransportFactory is the extension point the quic subpackage uses
// to register itself with this package at init time, avoiding an
// import cycle.
//
// A TransportFactory wraps the existing Node so all the higher-level
// APIs (Handle, Send, Call, Broadcast) work identically regardless of
// transport. The factory is responsible for:
//
// - Binding a listener on n.port (cfg.Port).
// - Yielding accepted connections via the supplied dispatch hook.
// - Implementing outbound dial via the supplied dispatch hook.
//
// Today only QUIC uses this hook; TCP is wired directly in node.go
// because the original Node embeds the TCP listener fields. This is
// pragmatic — once QUIC ships we can refactor TCP onto the same
// factory shape without a single user-visible change.
type TransportFactory interface {
// Listen starts a listener on the address derived from cfg.
// The listener calls onConn for each accepted, post-handshake
// connection. Listen must not block; it returns the bound
// address (so tests using port 0 can learn the kernel port) and
// a Close func.
Listen(ctx context.Context, cfg NodeConfig, onConn func(peerID string, conn TransportConn)) (string, func() error, error)
// Dial opens a connection to addr. The returned TransportConn
// is symmetric with the conns yielded by Listen.
Dial(ctx context.Context, cfg NodeConfig, addr string) (peerID string, conn TransportConn, err error)
}
// TransportConn is the transport-level abstraction over a single
// peer connection. It mirrors the existing TCP *Conn semantics
// (Send/Recv/Close) and is used by the transport-aware Node code
// path in node.go.
type TransportConn interface {
// Send writes one ZAP frame to the peer.
Send(frame []byte) error
// Recv blocks until the next ZAP frame arrives, or returns
// io.EOF when the peer cleanly closes.
Recv() ([]byte, error)
// Close performs a graceful close.
Close() error
// RemoteAddr returns the peer's network address (best effort —
// for QUIC this is the address at handshake time, not necessarily
// the latest after migration).
RemoteAddr() string
}
// TransportStreamer is an optional capability extension to
// TransportConn for transports that natively multiplex independent
// bidirectional streams (notably QUIC). When a TransportConn also
// implements TransportStreamer, Node.Call routes each request onto a
// fresh per-Call stream instead of serializing on the shared control
// stream — this lets concurrent Calls progress in parallel up to the
// peer-advertised stream limit (1024 for ZAP's QUIC config).
//
// TCP transport does NOT implement TransportStreamer; Node.Call
// transparently falls back to control-stream serialization on TCP.
type TransportStreamer interface {
// OpenCallStream opens a fresh bidirectional stream for a single
// request/response exchange. The caller writes exactly one frame
// (request) then reads exactly one frame (response) then closes.
OpenCallStream(ctx context.Context) (TransportStream, error)
// AcceptCallStream blocks until the peer opens a per-Call stream.
// Used by the server-side dispatch loop to fan out to handlers
// without blocking on a single serialized read.
AcceptCallStream(ctx context.Context) (TransportStream, error)
}
// TransportStream is one per-Call stream. Stream lifecycle:
//
// WriteFrame(req) // single request
// ReadFrame() // single response
// Close() // release stream ID back to the QUIC pool
//
// Both directions are length-prefixed ZAP frames identical to the
// control-stream wire format — so byte-for-byte interop with the
// TCP transport and with control-stream Call paths is preserved.
type TransportStream interface {
WriteFrame(frame []byte) error
ReadFrame() ([]byte, error)
Close() error
}
// transportRegistry holds the registered factories, keyed by
// Transport. Read-only after init.
var transportRegistry = map[Transport]TransportFactory{}
// RegisterTransport plugs a TransportFactory into the registry. The
// quic subpackage calls this in its init function.
//
// Registration is idempotent — re-registering the same Transport
// overwrites — but in practice each Transport has exactly one
// factory linked into the binary.
func RegisterTransport(t Transport, f TransportFactory) {
transportRegistry[t] = f
}
// lookupTransport returns the registered factory for t or
// ErrTransportUnavailable.
func lookupTransport(t Transport) (TransportFactory, error) {
f, ok := transportRegistry[t]
if !ok {
return nil, ErrTransportUnavailable
}
return f, nil
}