mirror of
https://github.com/soypat/lneto.git
synced 2026-08-03 06:27:52 +00:00
99e9d90a60
* 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
477 lines
16 KiB
Go
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
|
|
}
|