204 lines
5.3 KiB
Go
204 lines
5.3 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package nfs4server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"net"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
||
|
|
"sourcedock.dev/petrbalvin/nfs/internal/rpc"
|
||
|
|
)
|
||
|
|
|
||
|
|
// cbTimeout bounds one CB call: the client that does not answer in time
|
||
|
|
// loses the connection's callback service.
|
||
|
|
const cbTimeout = 5 * time.Second
|
||
|
|
|
||
|
|
// cbQueue bounds the callback work one connection may hold in flight.
|
||
|
|
const cbQueue = 32
|
||
|
|
|
||
|
|
// A cbResult is the outcome of one delivered CB_COMPOUND.
|
||
|
|
type cbResult struct {
|
||
|
|
res nfs4.CompoundRes
|
||
|
|
bodies [][]byte
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
|
||
|
|
// A cbWork is one queued CB_COMPOUND: the operations, the channel the
|
||
|
|
// synchronous caller waits on, and the action to run once delivery
|
||
|
|
// ends, whatever its outcome. A nil result channel makes the work fire
|
||
|
|
// and forget.
|
||
|
|
type cbWork struct {
|
||
|
|
tag string
|
||
|
|
clientid uint64
|
||
|
|
ops [][]byte
|
||
|
|
result chan cbResult
|
||
|
|
done func(cbResult)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A connCB is the per connection callback machinery: one worker
|
||
|
|
// goroutine delivers every CB_COMPOUND of the connection in order, the
|
||
|
|
// write side is shared with the request replies, and the pending map
|
||
|
|
// routes CB replies back to the worker. The worker exists so the
|
||
|
|
// dispatch path never blocks on a reply only the connection's own read
|
||
|
|
// loop can route: a CB issued while serving a request of the same
|
||
|
|
// connection would otherwise wait on itself.
|
||
|
|
type connCB struct {
|
||
|
|
conn net.Conn
|
||
|
|
writeMu *sync.Mutex
|
||
|
|
cbProgram uint32
|
||
|
|
|
||
|
|
mu sync.Mutex
|
||
|
|
nextXID uint32
|
||
|
|
pending map[uint32]chan []byte
|
||
|
|
|
||
|
|
work chan cbWork
|
||
|
|
}
|
||
|
|
|
||
|
|
func newConnCB(conn net.Conn, writeMu *sync.Mutex) *connCB {
|
||
|
|
c := &connCB{
|
||
|
|
conn: conn,
|
||
|
|
writeMu: writeMu,
|
||
|
|
pending: make(map[uint32]chan []byte),
|
||
|
|
work: make(chan cbWork, cbQueue),
|
||
|
|
}
|
||
|
|
go c.worker()
|
||
|
|
return c
|
||
|
|
}
|
||
|
|
|
||
|
|
// worker delivers the queued callbacks one at a time, which keeps the
|
||
|
|
// back channel slot discipline of one call in flight per slot.
|
||
|
|
func (c *connCB) worker() {
|
||
|
|
for w := range c.work {
|
||
|
|
r := c.deliver(w.tag, w.clientid, w.ops)
|
||
|
|
if w.done != nil {
|
||
|
|
w.done(r)
|
||
|
|
}
|
||
|
|
if w.result != nil {
|
||
|
|
w.result <- r
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// deliver sends one CB_COMPOUND over the connection and returns the
|
||
|
|
// decoded result. It runs on the worker goroutine only.
|
||
|
|
func (c *connCB) deliver(tag string, clientid uint64, ops [][]byte) cbResult {
|
||
|
|
c.mu.Lock()
|
||
|
|
xid := c.nextXID
|
||
|
|
c.nextXID++
|
||
|
|
ch := make(chan []byte, 1)
|
||
|
|
c.pending[xid] = ch
|
||
|
|
c.mu.Unlock()
|
||
|
|
|
||
|
|
header := rpc.Call{XID: xid, Program: c.program(), Version: nfs4.Version,
|
||
|
|
Procedure: nfs4.ProcCompound, Cred: rpc.AuthNull}
|
||
|
|
req, err := rpc.AppendCall(nil, header)
|
||
|
|
if err != nil {
|
||
|
|
c.dropPending(xid)
|
||
|
|
return cbResult{err: err}
|
||
|
|
}
|
||
|
|
args := nfs4.AppendCBCompoundArgs(nil, tag, nfs4.MinorVersion, 0, ops)
|
||
|
|
if err := c.write(append(req, args...)); err != nil {
|
||
|
|
c.dropPending(xid)
|
||
|
|
return cbResult{err: err}
|
||
|
|
}
|
||
|
|
|
||
|
|
var rec []byte
|
||
|
|
select {
|
||
|
|
case rec = <-ch:
|
||
|
|
case <-time.After(cbTimeout):
|
||
|
|
c.dropPending(xid)
|
||
|
|
return cbResult{err: fmt.Errorf("cb: no reply in %s", cbTimeout)}
|
||
|
|
}
|
||
|
|
|
||
|
|
reply, err := rpc.DecodeReply(rec)
|
||
|
|
if err != nil {
|
||
|
|
return cbResult{err: err}
|
||
|
|
}
|
||
|
|
if reply.Rejected || reply.Status != rpc.AcceptSuccess {
|
||
|
|
return cbResult{err: fmt.Errorf("cb: rpc status %d, rejected %v", reply.Status, reply.Rejected)}
|
||
|
|
}
|
||
|
|
r, bodies, err := nfs4.DecodeCompoundResBodiesCB(reply.Body)
|
||
|
|
return cbResult{res: r, bodies: bodies, err: err}
|
||
|
|
}
|
||
|
|
|
||
|
|
// tryQueue enqueues one work item, or fails when the queue is full.
|
||
|
|
func (c *connCB) tryQueue(w cbWork) error {
|
||
|
|
select {
|
||
|
|
case c.work <- w:
|
||
|
|
return nil
|
||
|
|
default:
|
||
|
|
return fmt.Errorf("cb: the callback queue is full")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// post queues one fire and forget CB_COMPOUND with an action to run
|
||
|
|
// once delivery ends.
|
||
|
|
func (c *connCB) post(tag string, clientid uint64, ops [][]byte, done func(cbResult)) error {
|
||
|
|
return c.tryQueue(cbWork{tag: tag, clientid: clientid, ops: ops, done: done})
|
||
|
|
}
|
||
|
|
|
||
|
|
// setProgram binds the callback program negotiated by CREATE_SESSION.
|
||
|
|
func (c *connCB) setProgram(program uint32) {
|
||
|
|
c.mu.Lock()
|
||
|
|
c.cbProgram = program
|
||
|
|
c.mu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// program reports the bound callback program.
|
||
|
|
func (c *connCB) program() uint32 {
|
||
|
|
c.mu.Lock()
|
||
|
|
defer c.mu.Unlock()
|
||
|
|
return c.cbProgram
|
||
|
|
}
|
||
|
|
|
||
|
|
// route delivers a record the demultiplexer identified as a reply to one
|
||
|
|
// of this connection's CB calls.
|
||
|
|
func (c *connCB) route(rec []byte) bool {
|
||
|
|
xid, _, err := rpc.PeekHeader(rec)
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
c.mu.Lock()
|
||
|
|
ch, ok := c.pending[xid]
|
||
|
|
if ok {
|
||
|
|
delete(c.pending, xid)
|
||
|
|
}
|
||
|
|
c.mu.Unlock()
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
ch <- rec
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *connCB) dropPending(xid uint32) {
|
||
|
|
c.mu.Lock()
|
||
|
|
delete(c.pending, xid)
|
||
|
|
c.mu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// wirePool recycles the record marking buffers of outgoing replies. The
|
||
|
|
// buffer lives only for the write: the connection copies it to the
|
||
|
|
// kernel, nothing retains a reference, and the cap keeps one oversized
|
||
|
|
// reply from pinning its memory forever.
|
||
|
|
var wirePool = sync.Pool{New: func() any { b := make([]byte, 0, 16<<10); return b }}
|
||
|
|
|
||
|
|
// maxWireBuf bounds the buffers the pool holds on to.
|
||
|
|
const maxWireBuf = maxRecord + 16
|
||
|
|
|
||
|
|
func (c *connCB) write(data []byte) error {
|
||
|
|
c.writeMu.Lock()
|
||
|
|
defer c.writeMu.Unlock()
|
||
|
|
buf := wirePool.Get().([]byte)[:0]
|
||
|
|
buf = rpc.AppendRecord(buf, data)
|
||
|
|
_, err := c.conn.Write(buf)
|
||
|
|
if cap(buf) <= maxWireBuf {
|
||
|
|
wirePool.Put(buf)
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|