Files over the ring
ioxide.file serves static assets the way the I/O model wants:
small, hot files straight from immutable memory snapshots; large files as positional
io_uring reads - no thread pool either way.
Asset snapshots
AssetCache opens every file under a root once, keyed by URL path. Files up to
a threshold (default 256KB) get their entire HTTP response - status line,
Content-Type, Content-Length, body - baked into native memory at snapshot build. Serving one
is a dictionary lookup (span-based, straight from the request bytes, no allocation) and a
send. Larger files keep a descriptor for ring reads.
// One snapshot, shared across all reactors.
var assets = new StaticAssets("/srv/www");
reactor.OnStart = r =>
{
r.AddService(assets);
// Per-reactor read concurrency for the large-file path.
AssetReader.CreatePool(r, readers: 4, bufferBytes: 1 << 20);
};
Serving
var assets = reactor.GetService<StaticAssets>();
var readers = reactor.GetService<RingPool<AssetReader>>();
if (assets.TryGet(pathBytes, out var asset))
{
if (asset.Response != 0)
{
// Hot path: the baked response - no file I/O, no header formatting.
await SendChunked(conn, asset.Response, asset.ResponseLength);
}
else
{
// Large file: positional read off the ring through a rented reader.
var reader = await readers.RentAsync();
try
{
int read = await reader.ReadAsync(asset.Fd, offset: 0);
WriteHeader(conn, asset, read);
await SendChunked(conn, reader.Buffer, read);
}
finally
{
readers.Return(reader);
}
}
}
The reader pool is what keeps concurrent requests from serializing: each rented
AssetReader owns its buffer and in-flight slot, and the cache's descriptors are
shared and positional, so any number of readers - within a reactor and across reactors - can
read the same file at once.
Atomic reloads
Snapshots are immutable; deploys swap them whole. StaticAssets.Reload()
builds a fresh AssetCache, swaps it in atomically, and disposes the old one
after a grace period - a reactor always sees a consistent snapshot, and Content-Length always
matches the body it ships. Wire it to SIGHUP and a deploy is
kill -HUP $(pidof server):
PosixSignalRegistration.Create(PosixSignal.SIGHUP, ctx =>
{
ctx.Cancel = true; // handled - don't let the default action terminate us
assets.Reload();
});
file mode.