Files
Pat Whittingslow 99e9d90a60 Netdev revamp (#145)
* begin working on netdev solution

* need to roll back some assumptions in next commit

* dual poll/async mode for netdev.Runner

* add wake on rx semantics

* remove TODO

* more reworking of Runner

* work on applying @MDr164 suggestions and a couple extra revamps

* add newline to end of test file
2026-07-09 14:04:10 -03:00

477 lines
16 KiB
Go

package netdev
import (
"context"
"errors"
"runtime"
"sync/atomic"
"time"
"github.com/soypat/lneto"
)
var (
errRunnerAcquired = errors.New("runner currently running")
errNotDriven = errors.New("runner needs to be poll/async driven")
errWakeNeedsAsync = errors.New("runner wake needs to be async driven")
errNoBackoffNeedsWake = errors.New("wake mechanic not set for omitting backoff")
errAsyncHandlingWithRun = errors.New("incompatible use of EnableAsyncHandling with Run- use with RunOnce")
errEgressInvalidWrite = errors.New("EgressPackets returned invalid written data given frameOffset and argument buffer size")
)
// RunnerFlags selects how a [Runner] drives its [Interface]. See [RunnerConfig.Flags].
type RunnerFlags uint32
const (
// Signals Interface needs to be driven via [DevEthernet.EthPoll].
// [RunnerAsync] can be set too to signal packets received via callback instead of written to EthPoll buffer.
RunnerInterfacePoll RunnerFlags = 1 << iota
// Interface data channel driven exclusively by callback passed to [DevEthernet.SetEthRecvHandler].
// If set [DevEthernet.EthPoll] must not write data to argument buffer.
RunnerInterfaceAsync
// Runner backs off but also wakes up on receiving data asynchronously.
// Needs [RunnerInterfaceAsync] to be set to be effective.
RunnerAsyncWakeOnRx
// Runner will not use the backoff to queue timer wakeups. When setting this option
// the user is responsible for waking up the stack so that it can transmit even when not receiving data.
// Needs [RunnerAsyncWakeOnRx] to be set to be effective.
RunnerNoBackoff
)
// HasAll reports whether every bit in query is set.
func (rf RunnerFlags) HasAll(query RunnerFlags) bool { return rf&query == query }
// HasAny reports whether at least one bit in query is set.
func (rf RunnerFlags) HasAny(query RunnerFlags) bool { return rf&query != 0 }
// Validate reports whether the flag combination is a usable [Runner] configuration.
func (rf RunnerFlags) Validate() error {
driven := rf.HasAny(RunnerInterfaceAsync | RunnerInterfacePoll)
wake := rf.HasAny(RunnerAsyncWakeOnRx)
if !driven {
return errNotDriven
} else if wake && !rf.HasAll(RunnerInterfaceAsync) {
return errWakeNeedsAsync
} else if rf.HasAny(RunnerNoBackoff) && !wake {
return errNoBackoffNeedsWake
}
return nil
}
// Runner orchestrates an Interface and a Stack asynchronously.
// The complexity of Runner stems from it needing to handle three types of devices, see [RunnerConfig.Flags] documentation.
type Runner[C any] struct {
running atomic.Uint32
bufs bufferSelect
backoff lneto.BackoffStrategy
reconnect *C
// pktlost is incremented each time an incoming packet is lost due to insufficient buffer size.
// Does not include packets dropped by the stack ([lneto.ErrPacketDrop]); the stack counts those itself.
pktlost atomic.Uint64
// rx includes ALL data received, even dropped data. xnet.StackAsync keeps track of actual processed data.
rx atomic.Uint64
// rxStackErrs/rxPollErrs/txStackErrs/txSendErrs count receive/transmit path
// errors which are intentionally not propagated out of the run loop nor
// printed in the datapath. See [RunnerStatistics] for per-counter semantics.
rxStackErrs, rxPollErrs atomic.Uint64
txStackErrs, txSendErrs atomic.Uint64
// bufsaux is used as an argument to stack processing so that no allocations are performed
bufsaux [1][]byte
sizesaux [1]int
// flags is atomic since [Runner.Wake] reads it from arbitrary goroutines
// concurrently with Configure. Configure stores it last so a visible
// RunnerAsyncWakeOnRx bit guarantees a non-nil wake channel.
flags atomic.Uint32
wake chan struct{}
waketimer *time.Timer
asyncH *Interface[C]
}
// RunnerConfig configures a [Runner]. Used in [Runner.Configure].
type RunnerConfig[C any] struct {
// Buffers are the Rx/Tx packet buffers. At least one is required. Use
// [Interface.RunnerBuffers] to get correctly sized, aligned buffers.
Buffers [][]byte
// ReconnectParams is stored for use during link reconnection. Optional.
ReconnectParams *C
// Backoff is the idle wait strategy between loop iterations. Required.
Backoff lneto.BackoffStrategy
// Flags must be set to be Async, Poll driven, or both.
// This selects the kind of device operation:
// - [RunnerInterfacePoll]: Entirely poll driven Rx [DevEthernet] i.e: ESP32 family.
// - [RunnerInterfaceAsync]: Entirely async driven Rx (IRQ) [DevEthernet] i.e: LAN8720.
// - [RunnerInterfacePoll]|[RunnerInterfaceAsync]: Hybrid Rx [DevEthernet] that are poll driven but can receive data outside EthPoll method. i.e: CYW43439.
Flags RunnerFlags
}
// RunnerStatistics keeps track of send/receive statistics.
// It is an incomplete view that should be combined with the likes of xnet.StackAsync.ReadStatistics.
// It also does not have a view into packets dropped by the [DevEthernet] because of insufficient/slow polling.
type RunnerStatistics struct {
// Rx total bytes received from interface including packets dropped.
Rx uint64
// RxPacketsDropped incremented by 1 each time there is not enough resources to process an incoming packet.
// This does NOT include packets dropped by the Stack with [lneto.ErrPacketDrop].
RxPacketsDropped uint64
// RxPollErrs increments by 1 each time polling the [Interface] returns an error.
// Is irrelevant for callback driven [DevEthernet].
RxPollErrs uint64
// RxStackErrs increments by 1 each time stack returns a non [lneto.ErrPacketDrop] error.
RxStackErrs uint64
// TxStackErrs increments by 1 each time stack returns an error generating egress packets.
TxStackErrs uint64
// TxSendErrs increments by 1 each time sending a frame over [Interface] returns error.
TxSendErrs uint64
// BufAcquireFail increments by 1 each time a buffer is unable to be acquired for Rx/Tx.
BufAcquireFail uint64
}
// ReadStatistics reads statistics of Runner into [RunnerStatistics].
func (r *Runner[C]) ReadStatistics(stats *RunnerStatistics) {
*stats = RunnerStatistics{
Rx: r.rx.Load(),
RxPacketsDropped: r.pktlost.Load(),
RxPollErrs: r.rxPollErrs.Load(),
RxStackErrs: r.rxStackErrs.Load(),
TxStackErrs: r.txStackErrs.Load(),
TxSendErrs: r.txSendErrs.Load(),
BufAcquireFail: uint64(r.bufs.missedAcquire.Load()),
}
}
func (r *Runner[C]) getFlags() RunnerFlags { return RunnerFlags(r.flags.Load()) }
// Wake unblocks a [Runner] sleeping in [RunnerAsyncWakeOnRx] mode so it services the
// stack immediately instead of waiting out the backoff. Signals coalesce and never block.
// Safe to call from any goroutine.
// Returns an error if the Runner is not configured for wake mode.
func (r *Runner[C]) Wake() error {
if !r.getFlags().HasAny(RunnerAsyncWakeOnRx) {
return lneto.ErrInvalidConfig
}
select {
case r.wake <- struct{}{}: // signal waiting runner
default: // already pending — coalesce, never block
}
return nil
}
// Configure validates cfg and applies it to the Runner. Call before [Runner.Run].
// Returns an error on invalid flags, missing buffers/backoff, or while the Runner is running.
func (r *Runner[C]) Configure(cfg RunnerConfig[C]) error {
if err := cfg.Flags.Validate(); err != nil {
return err
}
if len(cfg.Buffers) < 1 {
return lneto.ErrInvalidConfig
} else if cfg.Backoff == nil {
return lneto.ErrMissingHALConfig
}
if !r.acquire() {
return errRunnerAcquired
}
defer r.release()
r.flags.Store(0) // Disable Wake during reconfiguration.
r.teardownAsync()
r.bufs.reset(cfg.Buffers)
r.backoff = cfg.Backoff
r.reconnect = cfg.ReconnectParams
if cfg.Flags.HasAny(RunnerAsyncWakeOnRx) && r.wake == nil {
r.wake = make(chan struct{}, 1)
r.waketimer = time.NewTimer(24 * time.Hour)
}
r.flags.Store(uint32(cfg.Flags)) // Publish flags last; see field comment.
return nil
}
// RunOnce performs a single Rx-then-Tx service cycle and returns the bytes received
// and transmitted. It does no backoff, wake wait, or state reset: the caller controls
// pacing and must [Runner.Configure] (with a non-nil stack) before the first call.
// Returns an error if a [Runner.Run] or another RunOnce is already in progress.
//
// Unlike [Runner.Run], RunOnce does not install the async receive handler. For a
// poll-driven interface ([RunnerInterfacePoll]) it works as-is; for an async interface
// ([RunnerInterfaceAsync]) call [Runner.EnableAsyncHandling] once beforehand so delivered
// frames are captured.
func (r *Runner[C]) RunOnce(iface *Interface[C], stack Stack) (nrx, ntx int, err error) {
if stack == nil {
return 0, 0, lneto.ErrInvalidConfig
}
if !r.acquire() {
return 0, 0, errRunnerAcquired
}
defer r.release()
flags := r.getFlags()
async := flags.HasAny(RunnerInterfaceAsync)
poll := flags.HasAny(RunnerInterfacePoll)
bufsize := iface.bufsize()
nrx, ntx, err = r.service(iface, stack, bufsize, poll, async)
return nrx, ntx, err
}
// EnableAsyncHandling installs the Runner's async receive handler on iface so that
// frames delivered via [DevEthernet.SetEthRecvHandler] are captured into the buffer
// pool. Use it to drive an async interface with [Runner.RunOnce], which (unlike
// [Runner.Run]) does not install the handler itself. [Runner.Run] manages the handler
// on its own and does not need this.
//
// Returns [lneto.ErrUnsupported] if the Runner is not configured async
// ([RunnerInterfaceAsync]), or an error if a Run/RunOnce is in progress.
func (r *Runner[C]) EnableAsyncHandling(iface *Interface[C]) error {
if !r.acquire() {
return errRunnerAcquired
}
defer r.release()
if !r.getFlags().HasAny(RunnerInterfaceAsync) {
return lneto.ErrUnsupported
}
r.asyncH = iface
iface.dev.SetEthRecvHandler(r.recvEthHandler)
return nil
}
// DisableAsyncHandling removes the receive handler installed by [Runner.EnableAsyncHandling],
// stopping async frame delivery into the buffer pool. Call before reconfiguring or tearing
// down the Runner. No-op if async handling was not enabled.
func (r *Runner[C]) DisableAsyncHandling() error {
if !r.acquire() {
return errRunnerAcquired
}
defer r.release()
r.teardownAsync()
return nil
}
func (r *Runner[C]) teardownAsync() {
if r.asyncH != nil {
r.asyncH.dev.SetEthRecvHandler(nil)
r.asyncH = nil
}
}
// Run drives iface and stack until ctx is cancelled, doing one Rx then Tx per iteration
// and backing off when idle. Only one Run (and not concurrent with Configure) may execute
// at a time. Returns ctx.Err().
func (r *Runner[C]) Run(ctx context.Context, iface *Interface[C], stack Stack) error {
if stack == nil {
return lneto.ErrInvalidConfig
}
if !r.acquire() {
return errRunnerAcquired
}
defer r.release()
if r.asyncH != nil {
return errAsyncHandlingWithRun
}
r.bufs.releaseAll()
r.rx.Store(0)
r.pktlost.Store(0)
r.rxStackErrs.Store(0)
r.rxPollErrs.Store(0)
r.txStackErrs.Store(0)
r.txSendErrs.Store(0)
bufsize := iface.bufsize()
flags := r.getFlags()
async := flags.HasAny(RunnerInterfaceAsync)
poll := flags.HasAny(RunnerInterfacePoll)
wake := flags.HasAny(RunnerAsyncWakeOnRx)
backoffEnabled := !flags.HasAny(RunnerNoBackoff)
if wake {
r.waketimer.Stop()
}
backoff := r.backoff
if async {
iface.dev.SetEthRecvHandler(r.recvEthHandler)
// Only tear down the handler Run itself installed. In poll-only mode
// any handler on the device is not Run's to clear.
defer iface.dev.SetEthRecvHandler(nil)
}
// backoffs stores number of consecutive times no data was sent/received.
var backoffs uint
for ctx.Err() == nil {
nrx, ntx, err := r.service(iface, stack, bufsize, poll, async)
if err != nil {
return err
}
if nrx > 0 || ntx > 0 {
backoffs = 0
} else if wake {
if backoffEnabled {
d := backoff(backoffs)
backoffs++
switch d {
case lneto.BackoffFlagGosched:
runtime.Gosched()
fallthrough
case lneto.BackoffFlagNop:
continue
default:
d = max(d, 100*time.Microsecond)
}
// Claude say:
// Reset without draining waketimer.C assumes Go 1.23+ timer
// semantics (stale expiries do not linger in the channel). On
// runtimes with older semantics (e.g. TinyGo) worst case is a
// single spurious early wakeup, which is benign here.
r.waketimer.Reset(d)
}
select {
case <-r.wake:
backoffs = 0 // woke early on data.
case <-ctx.Done():
case <-r.waketimer.C:
}
if backoffEnabled {
r.waketimer.Stop()
}
} else {
backoff.Do(backoffs)
backoffs++
}
}
return ctx.Err()
}
func (r *Runner[C]) service(iface *Interface[C], stack Stack, bufsize int, poll, async bool) (nrx, ntx int, err error) {
nrx, err = r.doRx(iface, stack, poll, async)
// Now do Tx, but first acquire buffer.
txbuf := r.bufs.acquire(bufsize)
if txbuf == nil {
// We got blocked by Rx. Try draining rx.
for range len(r.bufs.bufs) {
n, err := r.doRx(iface, stack, poll, async)
if err != nil {
break
}
nrx += n
}
txbuf = r.bufs.acquire(bufsize)
if txbuf == nil {
// Buffers still held by in-flight Rx. Skip Tx this cycle rather
// than steal a slot the receive handler may be writing into.
return nrx, 0, nil
}
}
r.bufsaux = [1][]byte{txbuf}
err = stack.EgressPackets(r.bufsaux[:], r.sizesaux[:], iface.frameOff)
ntx = r.sizesaux[0]
if err != nil {
r.txStackErrs.Add(1)
} else if ntx > 0 {
if ntx+iface.frameOff > len(txbuf) {
r.bufs.release(txbuf)
return nrx, 0, errEgressInvalidWrite
}
err = iface.dev.SendOffsetEthFrame(r.bufsaux[0][:ntx+iface.frameOff])
if err != nil {
r.txSendErrs.Add(1)
}
}
r.bufs.release(txbuf) // Release buffer.
return nrx, ntx, nil
}
// PrintDebug
//
// Deprecated: Might be given other shape in future, but this is not how we do debugging. use freely meanwhile.
func (r *Runner[C]) PrintDebug() {
flags := r.getFlags()
print("RUNNER: rx:", r.rx.Load(),
" pktlost:", r.pktlost.Load(),
" rxErrs:", r.rxStackErrs.Load(),
" txErrs:", r.txSendErrs.Load(),
" devPoll:", flags.HasAny(RunnerInterfacePoll),
" devAsync:", flags.HasAny(RunnerInterfaceAsync),
" devWakeRx:", flags.HasAny(RunnerAsyncWakeOnRx),
"\n")
}
// acquire takes the single-use lock, returning false if already held.
func (r *Runner[C]) acquire() bool {
return r.running.CompareAndSwap(0, 1)
}
// release frees the lock taken by acquire. Panics if not held.
func (r *Runner[C]) release() {
if r.running.Load()&1 == 0 {
panic("release of unacquired resource")
}
r.running.Store(0)
}
// recvEthHandler is called asynchronously. Should be as fast as possible. Do not block inside.
func (r *Runner[C]) recvEthHandler(incomingEthernet []byte) {
r.rx.Add(uint64(len(incomingEthernet))) // rx includes dropped data. xnet.StackAsync keeps track of actual received statistics.
if !r.bufs.goroPutRx(incomingEthernet) {
// No free buffer or frame oversize, packet dropped.
r.pktlost.Add(1)
return
}
r.Wake()
}
// doRx services one Rx cycle. In poll mode it reads a frame from the device into a buffer
// and ingresses it. In async mode it drains frames delivered by recvEthHandler, pumping a
// poll-driven device with EthPoll(nil) when buffers are free. Device errors are counted
// in rxPollErrs; the returned error is the first stack ingress error encountered.
func (r *Runner[C]) doRx(iface *Interface[C], stack Stack, poll, async bool) (n int, gerr error) {
if !async { // poll guaranteed to be set as per RunnerFlags.Validate.
// Poll-only doRx.
buf := r.bufs.acquire(iface.frameSize)
if buf == nil {
return 0, nil
}
eoff, efrm, err := iface.dev.EthPoll(buf)
if err != nil {
r.rxPollErrs.Add(1)
}
if efrm > 0 {
r.bufsaux = [1][]byte{buf[:eoff+efrm]}
gerr = stack.IngressPackets(r.bufsaux[:], eoff)
if gerr != nil && gerr != lneto.ErrPacketDrop {
r.rxStackErrs.Add(1)
}
}
r.bufs.release(buf)
r.rx.Add(uint64(efrm))
return efrm, gerr
}
// Async branch.
n, gerr = r.processAsyncRx(stack)
if poll && r.bufs.numFree() > 0 {
// Manual polling required by device.
// Data not transmitted via this channel as per RunnerFlags documentation.
_, _, err := iface.dev.EthPoll(nil)
if err != nil {
r.rxPollErrs.Add(1)
}
} else {
return n, gerr
}
n2, err := r.processAsyncRx(stack)
if gerr == nil {
gerr = err
}
return n + n2, gerr
}
// processAsyncRx ingresses one frame previously copied to a buffer by recvEthHandler.
// Returns the frame length, or 0 if none is pending.
func (r *Runner[C]) processAsyncRx(stack Stack) (int, error) {
buf := r.bufs.getRx()
if buf == nil {
return 0, nil
}
r.bufsaux = [1][]byte{buf}
err := stack.IngressPackets(r.bufsaux[:], 0)
if err != nil && err != lneto.ErrPacketDrop {
r.rxStackErrs.Add(1)
}
r.bufs.release(buf)
return len(buf), err
}