# flare.udp.batch

> A module of flare_mlake's API.

Batched UDP I/O: ``recvmmsg`` / ``sendmmsg`` + GSO segmentation.

A QUIC server's hot loop is dominated by per-datagram syscalls: one
``recvfrom`` per inbound packet, one ``sendto`` per outbound packet.
Under a burst that is N syscalls for N datagrams. Linux's
``recvmmsg(2)`` / ``sendmmsg(2)`` move a whole vector of datagrams
across the user/kernel boundary in a single call, and UDP generic
segmentation offload (GSO, ``UDP_SEGMENT``) lets one ``sendmsg`` hand
the kernel a big buffer that it slices into many equal-sized wire
datagrams. Together these cut the syscall count on a saturated path by
~Nx.

These syscalls are Linux-only and version-gated (``recvmmsg`` 2.6.33,
``sendmmsg`` 3.0, ``UDP_SEGMENT`` 4.18). This module is the capability
probe + safe wrapper:

* :func:`udp_batch_supported` -- compile-time Linux gate. On any other
  target the helpers are never reached; callers use the per-datagram
  path.
* :class:`UdpBatchUnsupported` -- raised when a *running* kernel lacks
  the syscall (``ENOSYS``). Callers catch it once and latch back to the
  per-datagram path for the rest of the process lifetime.
* :class:`BatchReceiver` -- owns the ``struct mmsghdr[]`` + ``iovec[]``
  + name + data arrays and drains a burst with one ``recvmmsg``.
* :func:`send_batch` -- one ``sendmmsg`` for a vector of
  (payload, destination) pairs.
* :func:`send_segmented` -- one GSO ``sendmsg`` that slices ``data``
  into ``seg_size`` wire datagrams to a single destination.

Struct layout (x86_64 / aarch64 Linux, the only targets these calls
run on):

    struct iovec   { void *base; size_t len; }              // 16 B
    struct msghdr  { void *name; socklen_t namelen; (pad)   //  0,  8
                     struct iovec *iov; size_t iovlen;       // 16, 24
                     void *control; size_t controllen;       // 32, 40
                     int flags; (pad) }                      // 48  -> 56 B
    struct mmsghdr { struct msghdr hdr; unsigned len; (pad)} // 56  -> 64 B

The arrays are modeled as flat ``Pointer[UInt8]`` regions with
the fields poked at fixed offsets, mirroring
``flare.runtime.iovec.IoVecBuf``.

The name slots are sized for IPv4 + IPv6 (28 B), but the wire structs
are pinned to the 64-bit Linux ABI -- the only ABI where these
syscalls exist in this codebase. A 32-bit Linux port would need
different offsets, branching the comptime offsets on pointer width.

## Structs

### `UdpBatchUnsupported`

```mojo
struct UdpBatchUnsupported
```

Raised when the running kernel lacks ``recvmmsg`` / ``sendmmsg`` (``ENOSYS``). The caller falls back to per-datagram I/O.

Implements: `Copyable`, `Movable`, `Writable`

Fields:

- `op: String`

#### `__init__`

```mojo
def __init__(out self, op: String)
```

#### `write_to`

```mojo
def write_to[W: Writer](self, mut writer: W)
```

### `BatchReceiver`

```mojo
struct BatchReceiver
```

Owns the ``mmsghdr`` vector + backing buffers for a single ``recvmmsg`` drain of up to ``capacity`` datagrams.

Reuse one receiver across reactor ticks: :meth:`recv` re-arms the
per-message lengths in place, so steady-state has zero allocation.

Example:
```mojo
var rx = BatchReceiver(capacity=64, max_payload=1500)
var n = rx.recv(sock.fd())
for i in range(n):
    var dg = rx.message(i)        # Span[UInt8] of the payload
    var who = rx.sender(i)        # SocketAddr of the source
```

Implements: `Movable`

#### `__init__`

```mojo
def __init__(out self, capacity: Int, max_payload: Int)
```

Allocate the vector for ``capacity`` datagrams of at most ``max_payload`` bytes each.

Args:

- `capacity`: Max datagrams drained per :meth:`recv`. Must be > 0.
- `max_payload`: Per-datagram buffer size (e.g. the connection's
    max UDP payload). Must be > 0.

Raises: Error: If either argument is not positive, or if
``capacity * max_payload`` (the data region) or
``capacity * 64`` (the largest per-slot array, the
``mmsghdr`` vector) does not fit in an ``Int``. Such a
product would wrap to a small allocation while every iovec
still announced ``max_payload`` bytes to ``recvmmsg``.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

#### `recv`

```mojo
def recv(mut self, fd: Int, wait: Bool = False) -> Int
```

Drain up to ``capacity`` datagrams with one non-blocking ``recvmmsg``; with ``wait``, block (up to the socket's ``SO_RCVTIMEO``) for the first, then take what else is queued.

Returns the number of datagrams received (0 when the socket
queue is empty -- ``EAGAIN`` / ``EWOULDBLOCK``).

Raises: UdpBatchUnsupported: kernel lacks ``recvmmsg`` (``ENOSYS``).
NetworkError: any other libc errno.

#### `count`

```mojo
def count(self) -> Int
```

Number of datagrams from the last :meth:`recv`.

#### `message`

```mojo
def message(self, i: Int) -> Span[UInt8, MutUntrackedOrigin]
```

Borrow the ``i``-th received datagram's payload.

The span aliases the receiver's internal buffer and is only
valid until the next :meth:`recv`. Bounds-checked under
``-D ASSERT=safe``.

#### `ecn`

```mojo
def ecn(self, i: Int) -> Int
```

The ECN codepoint (low two bits of the IPv4 TOS / IPv6 traffic class) the ``i``-th datagram arrived with; 0 unless the socket asked for it (:meth:`flare.udp.UdpSocket.set_ecn`).

#### `sender`

```mojo
def sender(self, i: Int) -> SocketAddr
```

Decode the ``i``-th datagram's source address.

## Functions

### `udp_batch_supported`

```mojo
def udp_batch_supported() -> Bool
```

``True`` when batched UDP syscalls can be attempted.

Compile-time Linux gate. A ``True`` result still does not promise
the *running* kernel implements the syscall -- the first call may
raise :class:`UdpBatchUnsupported` (``ENOSYS``), which the caller
latches to disable the path. On macOS / other targets this is
``False`` and the wrappers are never invoked.

### `send_batch`

```mojo
def send_batch(fd: Int, payloads: List[List[UInt8]], addrs: List[SocketAddr]) -> Int
```

Send a vector of (payload, destination) pairs with one ``sendmmsg``.

Args:

- `fd`: UDP socket fd.
- `payloads`: One byte buffer per datagram.
- `addrs`: Matching destination addresses (``len == len(payloads)``).

Raises: UdpBatchUnsupported: kernel lacks ``sendmmsg`` (``ENOSYS``).
NetworkError: any other libc errno.

### `send_segmented`

```mojo
def send_segmented(fd: Int, addr: SocketAddr, data: Span[UInt8], seg_size: Int, ecn: Int = Int(0)) -> Int
```

Send ``data`` to ``addr`` as GSO segments of ``seg_size`` bytes.

One ``sendmsg`` hands the kernel the whole ``data`` buffer plus a
``UDP_SEGMENT`` control message; the kernel (or NIC) slices it into
wire datagrams of ``seg_size`` bytes (the final one may be shorter).
``seg_size`` 0 sends one datagram. A non-zero ``ecn`` codepoint
(RFC 3168) marks every datagram via an ``IP_TOS`` / ``IPV6_TCLASS``
control message, whatever the socket's own setting.

Returns bytes accepted by the kernel.

Raises: UdpBatchUnsupported: kernel lacks ``UDP_SEGMENT`` (``EINVAL`` on
the cmsg is mapped here as unsupported).
NetworkError: any other libc errno.

## Aliases

### `SOL_UDP`

```mojo
comptime SOL_UDP = IPPROTO_UDP
```

### `UDP_SEGMENT`

```mojo
comptime UDP_SEGMENT = Int32(103)
```
