Files
seaweedfs/weed/storage/blockvol/nvme/controller.go
T
Ping QiuandClaude Opus 4.6 3557ae283f feat: Phase 10 CP10-3 -- NVMe/TCP Tier 1 optimizations, WAL admission control, benchmark platform
CP10-3 Tier 1 optimizations (T1-T4):
- TCP_NODELAY + 256KB socket buffers on NVMe/TCP connections
- Response batching: all C2H data chunks + CapsuleResp in single flush
- Tiered buffer pool (4KB/64KB/256KB sync.Pool) for write payloads
- Configurable MaxH2CDataLength wiring through controller/IC/chunking

BUG-CP103-1: NVMe write retry with jittered backoff for transient WAL pressure
- writeWithRetry() with bounded backoff [50/200/800ms]
- throttleOnWALPressure() pre-write delay above 90% WAL usage
- WALPressureProvider interface + NVMeAdapter.WALPressure()

BUG-CP103-2: Volume-level WAL admission control
- WALAdmission with counting semaphore (max concurrent writers)
- Soft watermark (0.7): small delay to desynchronize herd
- Hard watermark (0.9): block until flusher drains
- Single-deadline budget shared across watermark wait + semaphore
- Close-aware during both watermark and semaphore waits
- Wired into BlockVol.WriteLBA() and Trim()

Benchmark platform enhancements:
- NVMe benchmark actions and scenarios (A/B, CW sweep, IOQ sweep)
- Database benchmark actions (SQLite, pgbench)
- K8s operator QA reconciler tests
- New testrunner scenarios for HA, fault injection, CSI lifecycle

Test counts: 213 NVMe + 625 engine + operator + testrunner tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:44:01 -07:00

522 lines
13 KiB
Go

package nvme
import (
"fmt"
"io"
"log"
"net"
"sync"
"sync/atomic"
"time"
)
// controllerState tracks the lifecycle of an NVMe controller session.
type controllerState int
const (
stateConnected controllerState = iota // TCP connected, no IC yet
stateICComplete // IC exchange done
stateAdminReady // Admin queue connected
stateCtrlReady // CC.EN=1, CSTS.RDY=1
stateIOActive // IO queues active
stateClosed // Shut down
)
// Request represents an in-flight NVMe command being processed.
type Request struct {
capsule CapsuleCommand
payload []byte // inline data from host (Write commands)
resp CapsuleResponse
c2hData []byte // data to send to host (Read commands)
status StatusWord
}
// Controller handles one NVMe/TCP connection (one queue per connection).
type Controller struct {
mu sync.Mutex
// Session identity
conn net.Conn
in *Reader
out *Writer
state controllerState
closed atomic.Bool
// Queue state (one queue per TCP connection)
queueID uint16
queueSize uint16
sqhd uint16 // Submission Queue Head pointer
flowCtlOff bool // CATTR bit2: SQ flow control disabled
// Controller identity
cntlID uint16
subNQN string
// Controller registers
regCAP uint64 // Controller Capabilities
regCC uint32 // Controller Configuration (set by host via PropertySet)
regCSTS uint32 // Controller Status (RDY bit)
regVS uint32 // Version
// KeepAlive
katoMs uint32
katoTimer *time.Timer
katoMu sync.Mutex
// Async completion (IO queues)
waiting chan *Request // pre-allocated request pool
completions chan *Request // completed requests to send
// Backend
subsystem *Subsystem
server *Server
// Features
maxIOQueues uint16
grantedQueues uint16
isAdmin bool // true if this controller owns admin queue (QID=0)
maxDataLen uint32 // C2H/H2C data chunk size (from Config)
// Command interleaving: capsules received during R2T H2CData collection.
// Drained by Serve() before reading the next PDU from the wire.
pendingCapsules []*Request
// Lifecycle
wg sync.WaitGroup
closeOnce sync.Once
}
// newController creates a controller for the given connection.
func newController(conn net.Conn, server *Server) *Controller {
maxData := server.cfg.MaxH2CDataLength
if maxData == 0 {
maxData = maxH2CDataLen // fallback to 32KB default
}
c := &Controller{
conn: conn,
in: NewReader(conn),
out: NewWriterSize(conn, int(maxData)+maxHeaderSize),
state: stateConnected,
server: server,
regVS: nvmeVersion14,
// CAP register: MQES=63 (bits 15:0), CQR=1 (bit 16), TO=30 (bits 31:24, *500ms=15s), CSS bit37=1 (NVM command set)
regCAP: uint64(63) | (1 << 16) | (uint64(30) << 24) | (1 << 37),
maxIOQueues: server.cfg.MaxIOQueues,
maxDataLen: maxData,
}
return c
}
// Serve is the main event loop for this controller connection.
func (c *Controller) Serve() error {
defer c.shutdown()
// IC handshake timeout
if err := c.conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
return err
}
for {
if c.closed.Load() {
return nil
}
// Drain capsules that arrived during a prior R2T data collection.
for len(c.pendingCapsules) > 0 {
req := c.pendingCapsules[0]
c.pendingCapsules = c.pendingCapsules[1:]
if err := c.dispatchPending(req); err != nil {
return fmt.Errorf("pending capsule: %w", err)
}
}
hdr, err := c.in.Dequeue()
if err != nil {
if err == io.EOF || c.closed.Load() {
return nil
}
return fmt.Errorf("read header: %w", err)
}
switch hdr.Type {
case pduICReq:
if err := c.handleIC(); err != nil {
return fmt.Errorf("IC handshake: %w", err)
}
// Clear read deadline after successful IC
if err := c.conn.SetReadDeadline(time.Time{}); err != nil {
return err
}
case pduCapsuleCmd:
if err := c.handleCapsule(); err != nil {
return fmt.Errorf("capsule: %w", err)
}
case pduH2CData:
// H2CData PDUs are only expected after R2T, handled inline
// by recvH2CData. If we see one here, it's unexpected.
return fmt.Errorf("unexpected H2CData PDU outside R2T flow")
case pduH2CTermReq:
return nil // host terminated
default:
return fmt.Errorf("unexpected PDU type: 0x%x", hdr.Type)
}
}
}
// handleIC processes the IC handshake.
func (c *Controller) handleIC() error {
var req ICRequest
if err := c.in.Receive(&req); err != nil {
return err
}
resp := ICResponse{
PDUFormatVersion: 0,
MaxH2CDataLength: c.maxDataLen,
}
if err := c.out.SendHeaderOnly(pduICResp, &resp, icBodySize); err != nil {
return err
}
c.state = stateICComplete
return nil
}
// handleCapsule dispatches a CapsuleCmd PDU.
func (c *Controller) handleCapsule() error {
// Reject capsule commands before IC handshake is complete.
if c.state < stateICComplete {
return fmt.Errorf("capsule command before IC handshake")
}
var capsule CapsuleCommand
if err := c.in.Receive(&capsule); err != nil {
return err
}
// Read optional inline data
var payload []byte
if dataLen := c.in.Length(); dataLen > 0 {
payload = getBuffer(int(dataLen))
if err := c.in.ReceiveData(payload); err != nil {
putBuffer(payload)
return err
}
}
// Advance SQHD
c.sqhd++
if c.sqhd >= c.queueSize && c.queueSize > 0 {
c.sqhd = 0
}
req := &Request{
capsule: capsule,
payload: payload,
}
req.resp.CID = capsule.CID
req.resp.QueueID = c.queueID
// SQHD is set in sendResponse/sendC2HDataAndResponse using the
// latest c.flowCtlOff value, so Connect responses correctly get
// SQHD=0xFFFF when the host requests flowCtlOff via CATTR.
req.resp.Status = uint16(StatusSuccess)
if c.queueID == 0 {
return c.dispatchAdmin(req)
}
return c.dispatchIO(req)
}
// dispatchPending processes a capsule that was buffered during R2T data
// collection. The capsule and payload are already fully read — only
// SQHD advance and command dispatch remain.
func (c *Controller) dispatchPending(req *Request) error {
c.sqhd++
if c.sqhd >= c.queueSize && c.queueSize > 0 {
c.sqhd = 0
}
if c.queueID == 0 {
return c.dispatchAdmin(req)
}
return c.dispatchIO(req)
}
// dispatchAdmin handles admin queue commands synchronously.
func (c *Controller) dispatchAdmin(req *Request) error {
defer func() {
if req.payload != nil {
putBuffer(req.payload)
req.payload = nil
}
}()
capsule := &req.capsule
if capsule.OpCode == adminFabric {
return c.handleFabricCommand(req)
}
switch capsule.OpCode {
case adminIdentify:
return c.handleIdentify(req)
case adminSetFeatures:
return c.handleSetFeatures(req)
case adminGetFeatures:
return c.handleGetFeatures(req)
case adminGetLogPage:
return c.handleGetLogPage(req)
case adminKeepAlive:
return c.handleKeepAlive(req)
case adminAsyncEvent:
// Stub: just succeed (don't deliver events in CP10-1)
return c.sendResponse(req)
default:
req.resp.Status = uint16(StatusInvalidOpcode)
return c.sendResponse(req)
}
}
// dispatchIO handles IO queue commands.
func (c *Controller) dispatchIO(req *Request) error {
defer func() {
if req.payload != nil {
putBuffer(req.payload)
req.payload = nil
}
}()
capsule := &req.capsule
switch capsule.OpCode {
case ioRead:
return c.handleRead(req)
case ioWrite:
return c.handleWrite(req)
case ioFlush:
return c.handleFlush(req)
case ioWriteZeros:
return c.handleWriteZeros(req)
default:
req.resp.Status = uint16(StatusInvalidOpcode)
return c.sendResponse(req)
}
}
// sendC2HDataAndResponse sends C2HData PDUs followed by a CapsuleResp.
// All chunks and the final response are batched in the bufio buffer,
// then flushed to the wire in a single FlushBuf() call.
func (c *Controller) sendC2HDataAndResponse(req *Request) error {
if len(req.c2hData) > 0 {
data := req.c2hData
offset := uint32(0)
chunkSize := c.maxDataLen
for offset < uint32(len(data)) {
end := offset + chunkSize
if end > uint32(len(data)) {
end = uint32(len(data))
}
chunk := data[offset:end]
hdr := C2HDataHeader{
CCCID: req.capsule.CID,
DATAO: offset,
DATAL: uint32(len(chunk)),
}
flags := uint8(0)
if end >= uint32(len(data)) {
flags = c2hFlagLast
}
if err := c.out.writeHeaderAndData(pduC2HData, flags, &hdr, c2hDataHdrSize, chunk); err != nil {
return err
}
offset = end
}
}
// Write CapsuleResp to bufio buffer
if c.flowCtlOff {
req.resp.SQHD = 0xFFFF
} else {
req.resp.SQHD = c.sqhd
}
c.resetKATO()
if err := c.out.writeHeaderAndData(pduCapsuleResp, 0, &req.resp, capsuleRespSize, nil); err != nil {
return err
}
// Single flush: all C2H chunks + CapsuleResp in one syscall
return c.out.FlushBuf()
}
// sendResponse sends a CapsuleResp PDU.
// SQHD is set here (not in handleCapsule) so that flowCtlOff changes
// made during command dispatch (e.g. Fabric Connect) take effect
// on the same response.
func (c *Controller) sendResponse(req *Request) error {
if c.flowCtlOff {
req.resp.SQHD = 0xFFFF
} else {
req.resp.SQHD = c.sqhd
}
c.resetKATO()
return c.out.SendHeaderOnly(pduCapsuleResp, &req.resp, capsuleRespSize)
}
// ---------- R2T / H2C Data ----------
// sendR2T sends a Ready-to-Transfer PDU requesting data from the host.
func (c *Controller) sendR2T(cid uint16, tag uint16, offset, length uint32) error {
r2t := R2THeader{
CCCID: cid,
TAG: tag,
DATAO: offset,
DATAL: length,
}
return c.out.SendHeaderOnly(pduR2T, &r2t, r2tHdrSize)
}
// recvH2CData reads H2CData PDU(s) from the wire and returns the accumulated data.
// Reads exactly `totalBytes` of data, potentially across multiple H2C PDUs.
//
// At QD>1 the host may interleave CapsuleCmd PDUs on the same connection
// before the H2CData for a prior R2T arrives. Such capsules are fully read
// and buffered in c.pendingCapsules for dispatch after the current command
// completes (NVMe/TCP spec §3.5 — command pipelining).
func (c *Controller) recvH2CData(totalBytes uint32) ([]byte, error) {
buf := getBuffer(int(totalBytes))
received := uint32(0)
for received < totalBytes {
hdr, err := c.in.Dequeue()
if err != nil {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: read header: %w", err)
}
// Interleaved CapsuleCmd: buffer it for later dispatch.
if hdr.Type == pduCapsuleCmd {
if err := c.bufferInterleaved(); err != nil {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: buffer interleaved capsule: %w", err)
}
continue
}
if hdr.Type != pduH2CData {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: expected H2CData (0x6), got 0x%x", hdr.Type)
}
var h2c H2CDataHeader
if err := c.in.Receive(&h2c); err != nil {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: receive header: %w", err)
}
dataLen := c.in.Length()
if dataLen == 0 {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: H2CData PDU has no payload")
}
if h2c.DATAO+dataLen > totalBytes {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: data exceeds expected size (%d+%d > %d)",
h2c.DATAO, dataLen, totalBytes)
}
if err := c.in.ReceiveData(buf[h2c.DATAO : h2c.DATAO+dataLen]); err != nil {
putBuffer(buf)
return nil, fmt.Errorf("recvH2CData: receive data: %w", err)
}
received += dataLen
}
return buf, nil
}
// bufferInterleaved reads a complete CapsuleCmd (header + optional inline
// data) that arrived during R2T data collection and appends it to
// c.pendingCapsules. Called from recvH2CData when hdr.Type == pduCapsuleCmd.
func (c *Controller) bufferInterleaved() error {
var capsule CapsuleCommand
if err := c.in.Receive(&capsule); err != nil {
return err
}
var payload []byte
if dataLen := c.in.Length(); dataLen > 0 {
payload = getBuffer(int(dataLen))
if err := c.in.ReceiveData(payload); err != nil {
putBuffer(payload)
return err
}
}
req := &Request{
capsule: capsule,
payload: payload,
}
req.resp.CID = capsule.CID
req.resp.QueueID = c.queueID
req.resp.Status = uint16(StatusSuccess)
c.pendingCapsules = append(c.pendingCapsules, req)
return nil
}
// ---------- KATO management ----------
func (c *Controller) startKATO() {
c.katoMu.Lock()
defer c.katoMu.Unlock()
if c.katoMs == 0 {
return
}
d := time.Duration(c.katoMs) * time.Millisecond
// Add 50% margin per spec recommendation
d = d + d/2
c.katoTimer = time.AfterFunc(d, func() {
log.Printf("nvme: KATO expired for cntlid=%d, closing connection", c.cntlID)
c.conn.Close()
})
}
func (c *Controller) resetKATO() {
c.katoMu.Lock()
defer c.katoMu.Unlock()
if c.katoTimer != nil {
c.katoTimer.Reset(time.Duration(c.katoMs)*time.Millisecond + time.Duration(c.katoMs)*time.Millisecond/2)
}
}
func (c *Controller) stopKATO() {
c.katoMu.Lock()
defer c.katoMu.Unlock()
if c.katoTimer != nil {
c.katoTimer.Stop()
c.katoTimer = nil
}
}
// ---------- Lifecycle ----------
func (c *Controller) shutdown() {
c.closeOnce.Do(func() {
c.closed.Store(true)
c.stopKATO()
c.state = stateClosed
c.conn.Close()
if c.server != nil {
if c.isAdmin && c.cntlID != 0 {
c.server.unregisterAdmin(c.cntlID)
}
c.server.removeSession(c)
}
})
}