valis / Reference / API reference
Backends - API reference
Exported surface for the backends subsystem. Part of the API reference.
Package valis/src/backends/epoll
Classes
epoll-datagram-source
UDP datagram source. The fd IS the connection's identity (there is no accept step); each receive-from produces a datagram-connection carrying the payload, the peer, and the recovered destination port. Datagrams have no stream semantics, so no stream view is offered on this connection: the datagram-connection class signals stream-not-available if connection-stream is called.
epoll-mux
Concrete multiplexer backed by iolib's event-base (epoll on Linux). The class makes the backend seam operational: every iolib type stays inside this file, and the abstract multiplexer generics specialise here to drive the loop, route readiness, and translate accept(2) and receive-from into valis connections.
epoll-stream-source
TCP stream source. source-ready accepts a non-blocking connection, recovers the original destination port on the accepted (NOT the listening) socket, builds a tcp-connection, and hands it to the executor, all before returning to the event loop.
held-descriptor
One descriptor this process was handed and what the kernel says about it: the descriptor itself, the port it is bound to, and whether it carries a stream or a datagram.
It records no service name. A descriptor carrying one would be the launcher's claim about a host-side arrangement travelling under a new spelling, and asking the socket is how this node stopped reading such claims.
Conditions
descriptor-not-a-bound-socket
Signalled when DESCRIPTOR-BOUND-PORT is asked about a descriptor that cannot answer: one that is closed, one that is not a socket, one carrying a transport that is neither stream nor datagram, or one bound to an address that names no port. The class is narrow on purpose, so a caller can tell a descriptor that said nothing apart from the syscall failures the rest of this backend raises and decide for itself whether that is fatal.
fd-already-registered
Signalled when register-source is called for a fd already present in the mux's sources table. iolib's set-io-handler raises 'FD already monitored' in that situation; this condition catches it earlier with a more useful message and prevents the registry from silently losing a source.
Generic functions
descriptor-not-a-bound-socket-fd
(descriptor-not-a-bound-socket-fd condition)
Undocumented: this exported symbol needs a docstring.
descriptor-not-a-bound-socket-reason
(descriptor-not-a-bound-socket-reason condition)
Undocumented: this exported symbol needs a docstring.
fd-already-registered-fd
(fd-already-registered-fd condition)
Undocumented: this exported symbol needs a docstring.
Functions
close-listening-fd
(close-listening-fd fd)
Release a descriptor minted by MAKE-LISTENING-FD back to the kernel. Its exact counterpart: MAKE-LISTENING-FD hands back a bare integer, so the release is the close(2) syscall, not CL:CLOSE. The latter is a stream operation and signals a TYPE-ERROR on an integer, leaving the descriptor open. Naming it here keeps iolib confined to this backend, so a consumer that mints a LISTEN fd can also release one without importing iolib and without a shadowed CLOSE in its own package.
descriptor-answering
(descriptor-answering held port kind)
The descriptor in HELD bound to PORT over transport KIND, or NIL when this node holds none.
Ask for what a service needs and you get the socket that already answers for it, whoever handed it over and whichever argument carried it. Nothing here matches a service to a descriptor by the name of the parameter it arrived in.
KIND belongs in the key rather than beside it, because one port answers over two transports at once and they are not interchangeable: a query reaching the stream socket whose reply leaves by the datagram one is answered by nobody.
A NIL PORT names no service and matches nothing, so a caller that has no port for a service asks for one and correctly gets nothing back.
descriptor-bound-port
(descriptor-bound-port fd)
Ask the socket at FD what it is. Returns the port it is bound to and, as a second value, its transport kind: :STREAM or :DATAGRAM. Hand it a descriptor whose socket was never bound and you get port 0 reported plainly, because whether an unbound socket is a problem belongs to the caller and not here. Anything that cannot answer at all signals DESCRIPTOR-NOT-A-BOUND-SOCKET.
I ask the descriptor rather than the party that handed it over because a socket cannot be wrong about its own bound port, while anything said about it alongside it is an assertion nothing here can check.
FD is borrowed and comes back as it went in. No socket object is wrapped around it: such a wrapper carries a finalizer that closes the descriptor it holds when the collector reaches it, which for a descriptor valis was handed rather than created is somebody else's socket closing at a moment nothing can predict.
Keeping the syscalls here preserves the seam invariant, so a consumer can ask a descriptor its port without importing iolib.
held-descriptor-fd
(held-descriptor-fd instance)
Undocumented: this exported symbol needs a docstring.
held-descriptor-kind
(held-descriptor-kind instance)
Undocumented: this exported symbol needs a docstring.
held-descriptor-port
(held-descriptor-port instance)
Undocumented: this exported symbol needs a docstring.
interrogate-held-descriptors
(interrogate-held-descriptors fds)
Ask every descriptor in FDS what it is and return one HELD-DESCRIPTOR for each that answered. A second value carries an (fd . reason) pair for each descriptor that answered nothing, so a caller says so in its own log rather than have this report on its behalf.
A NIL entry is not a descriptor and is passed over, so a caller assembles the list from whatever its launch supplied without filtering first.
A descriptor bound to no port is dropped exactly as one that cannot answer at all is. A port this node failed to learn about costs it a rival socket at worst, where a boot that died reading a socket option serves nothing.
The descriptors are borrowed and come back as they went in.
loopback-bind-disposition
(loopback-bind-disposition port &key (host "127.0.0.1"))
What this process gets when it binds PORT on HOST, answered by binding it and letting it go again: :AVAILABLE when the bind succeeds, :REFUSED when the kernel denies it to a process holding no privilege for that port, and :OTHERWISE-ENGAGED for any other refusal.
I ask by binding rather than by comparing PORT against 1024, because the number is not the fact. The same 80 is bindable by a node holding CAPNETBINDSERVICE and refused to one holding nothing, and we ship both. A rule written on the number answers for a node other than the one it is running on, and it answers just as confidently there.
:REFUSED and :OTHERWISE-ENGAGED are kept apart because a caller substituting a development port for one it may never bind must not also substitute one for a port that is merely busy. The first is the standing shape of an unprivileged node; the second is a conflict somebody needs to see.
The bind is the one make-listening-fd performs, which is the one make-stream-source performs, so the answer is about the bind the caller is about to attempt rather than about a socket assembled some other way. The descriptor is released before this returns, which is what makes the probe safe to run ahead of the real bind and also what keeps it a probe: between the two, nothing stops somebody else taking the port.
make-connection-non-blocking
(make-connection-non-blocking connection)
Put CONNECTION's descriptor in non-blocking mode and answer CONNECTION.
The call is made rather than assumed, and that is why it exists at all. A reader driven by readiness notification must never park inside the socket library's own poll loop; the library sets this mode itself today, but a choice somebody else made can be withdrawn in a release, and the loop thread would then block on one slow peer with nothing to say why.
A connection driven through these three primitives must never have its buffered stream view taken, before or after. That view reads ahead into a userspace buffer, and bytes sitting there have already left the kernel, so readiness notification cannot see them. The consequence is a connection that hangs with its data already delivered.
make-datagram-source
(make-datagram-source &key (host "127.0.0.1") port executor)
Construct a UDP datagram source bound to HOST:PORT. The bind step is mandatory (an unbound UDP socket has ephemeral port 0 and the registry has nothing to route on). Captures the bound port via local-port once at construction and caches it on the source so source-ready does not re-syscall on every packet, and so a mid-teardown getsockname cannot crash the loop thread. Returns an epoll-datagram-source ready to register-source.
make-datagram-source-from-fd
(make-datagram-source-from-fd &key fd executor)
Construct a UDP datagram source from an inherited, ALREADY-BOUND file descriptor, the steered-fd analogue of make-datagram-source. There is no make-socket / bind-address step: the fd arrives already bound to its port (an eBPF-steered socket, or one a co-located privileged agent bound and handed off over SCMRIGHTS), and valis never binds a designated low port itself. The address family is recovered from the descriptor, and dest-port is recovered once via local-port and cached on the source exactly as the bind path caches it.
iolib's public make-socket-from-fd cannot wrap a datagram fd (it forwards stream-only initargs the datagram socket class rejects), so the descriptor is adopted through the lower-level create-socket with only the datagram-valid initargs. Keeping every iolib symbol here preserves the seam invariant: the edge controller builds a steered datagram source without naming iolib.
make-epoll-mux
(make-epoll-mux)
Construct a fresh epoll-mux with an empty sources table, a new iolib event-base, and a registered self-pipe wakeup. The event-base instantiation selects the default multiplexer for the platform (epoll on Linux); valis treats that choice as opaque. The self-pipe's read end is registered with the event-base so a stop-loop write to the corresponding write end forces epollwait to return on demand from another thread.
make-listening-fd
(make-listening-fd &key (host "127.0.0.1") port (backlog +default-backlog+))
Bind and listen a passive AFINET stream socket on HOST:PORT and return its raw descriptor as an integer, valis's production helper for minting a designated-port LISTEN fd it owns. Lifts the exact bind-address + listen-on steps make-stream-source uses (SOREUSEADDR so a quick teardown cycle does not fight TIMEWAIT), then hands back a bare descriptor the caller owns: the fd is DUP'd out of the iolib socket because closing that socket closes the descriptor it holds, and this function closes it before returning. Handed the socket's own descriptor, a caller would find it already shut. A PORT of 0 binds an ephemeral port (the kernel picks it); the caller recovers the actual port by getsockname on the fd when it needs it.
This is the src/ equivalent of the test-only make-listening-socket: valis is the only party that binds and listens (the steered-fd handoff consent model), and the resident boot pushes this descriptor to the host agent while keeping its own copy to wrap as the steered source. Keeping the iolib names here preserves the seam invariant (this backend is the one file that names iolib for the source path), so a consumer mints a LISTEN fd without importing iolib.
make-stream-source
(make-stream-source &key (host "127.0.0.1") port executor (backlog +default-backlog+))
Construct a TCP stream source bound to HOST:PORT. HOST is a dotted-quad
string ("127.0.0.1" by default, the listener binds loopback only
until real authentication lands); PORT is an (unsigned-byte 16);
BACKLOG is the listen(2) queue
depth, defaulting to default-backlog (128, matching SOMAXCONN on
Linux). SOREUSEADDR is set on the passive socket so a quick
stop-listener / start-listener cycle does not fight TIMEWAIT
(close-source releases the fd; SOREUSEADDR lets the next bind on
the same port succeed even before the kernel finishes its TCP-state
timer). Returns an epoll-stream-source ready to register-source.
make-stream-source-from-fd
(make-stream-source-from-fd &key fd executor)
Construct a TCP stream source from an inherited, ALREADY-LISTENING file descriptor, the steered-fd path. Unlike make-stream-source there is no make-socket / bind-address / listen-on / SOREUSEADDR step: the fd arrives already in LISTEN state (an eBPF-steered socket, or a socket a co-located agent created and handed off), and binding it a second time would signal. make-socket-from-fd wraps the descriptor as a :passive iolib socket, recovering its address family and type by getsockname on the fd. The resulting epoll-stream-source is the SAME class the bind path builds, so source-ready's destination-port recovery (local-port on the ACCEPTED client, never the listening socket) is inherited unchanged and the steered four-tuple survives exactly as the loopback path proves it.
Keeping make-socket-from-fd here preserves the seam invariant: this is the one file that names an iolib symbol, so the edge controller can build a steered source without importing iolib.
peer-address-octets
(peer-address-octets peer-address)
Convert an iolib peer address (as returned by receive-from or accept-connection) into (values RAW-OCTETS FAMILY-KEYWORD): a (simple-array (unsigned-byte 8)) of 4 octets with :ipv4, or 16 octets with :ipv6, the shape runciter's make-client-address keys RRL on. iolib yields an IPv6 address as eight 16-bit words, so the IPv6 case is widened to its 16 network-order octets. Keeping this conversion here holds the seam invariant: a caller obtains RRL-ready octets without naming an iolib symbol.
peer-address-string
(peer-address-string peer-address)
Render an iolib peer address (as returned by receive-from or accept-connection) to its canonical textual form: a dotted-quad for IPv4 ("192.0.2.10"), an RFC 5952 compressed literal for IPv6. This is the address form the transfer allowlist stores and matches a pulling secondary by (a case-folded string compare), so a serving-side authorization check presents the peer in exactly the shape the enrollment recorded. Keeping the rendering here holds the seam invariant: a caller names the peer as text without naming an iolib symbol.
read-available
(read-available connection buffer &key (start 0) end)
Read ONCE from CONNECTION into BUFFER between START and END, answering the octet count read, 0 when the descriptor would block, or :EOF when the peer closed in an orderly way. BUFFER is a simple octet vector.
One syscall per call and the caller loops, which is what bounds a drain by what actually arrived rather than by what a peer might yet send. A descriptor that went away underneath the call answers :EOF rather than signalling: the drain runs inside a readiness callback, where an escaping condition would take the whole event loop with it.
A connection read this way must never have its buffered stream view taken, before or after. That view reads ahead into a userspace buffer, and bytes sitting there have already left the kernel, so readiness notification cannot see them. The consequence is a connection that hangs with its data already delivered.
reply-datagram
(reply-datagram connection octets)
Send OCTETS back to CONNECTION's recorded peer over the datagram source's UDP socket, the send counterpart to source-ready's receive-from, and the one path a UDP answer leaves by. CONNECTION carries the peer address and port receive-from recovered and the source's socket; sending to that exact four-tuple lands the reply at the peer that sent the query and nowhere else. Keeping send-to here holds the seam invariant: a caller replies without naming an iolib symbol. Returns the number of octets sent.
write-available
(write-available connection buffer &key (start 0) end)
Write ONCE from BUFFER between START and END to CONNECTION, answering the octet count that actually left, 0 when the descriptor would block, or :EOF when it went away underneath the call. BUFFER is a simple octet vector.
The count is very often short of what was asked, because a peer that is slow to read fills the descriptor's room and the rest has to wait for writability. A caller that treats the answer as all-or-nothing silently drops the remainder, so the count is the whole contract here.
A connection written this way must never have its buffered stream view taken, before or after. That view holds bytes in a userspace buffer of its own, so two writers would interleave on the wire and neither would know.
Variables
+default-backlog+
Default TCP listen(2) backlog for stream sources. 128 matches the historical Linux SOMAXCONN default and is large enough that a burst of loopback connects through one accept-until-EWOULDBLOCK pass does not lose connections to a too-shallow accept queue. Callers can override per source via make-stream-source's :backlog keyword.