Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bench/iouring/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Built artifacts (regenerate via ./build.sh)
iouring_smoke
11 changes: 11 additions & 0 deletions bench/iouring/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Linux smoke-test image for the io_uring AcceptLoop in zig/src/iouring.zig.
#
# Pre-built static aarch64-linux-musl binary; no compiler needed at image
# time. Build the binary on the host first with:
# ./bench/iouring/build.sh
FROM alpine:3.20

COPY iouring_smoke /usr/local/bin/iouring_smoke
RUN chmod +x /usr/local/bin/iouring_smoke

ENTRYPOINT ["/usr/local/bin/iouring_smoke"]
59 changes: 59 additions & 0 deletions bench/iouring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# io_uring smoke test

End-to-end correctness check for the Linux `io_uring` `IORING_OP_ACCEPT_MULTISHOT`
accept loop in [`zig/src/iouring.zig`](../../zig/src/iouring.zig).

This is **not a benchmark.** It exists to prove that:

1. `zig/src/iouring.zig` compiles cleanly for `aarch64-linux-musl`.
2. `std.os.linux.IoUring` works on the kernel the test is run against.
3. `IORING_OP_ACCEPT_MULTISHOT` actually delivers all expected accepts when
N clients connect to a listen socket.

The smoke binary opens a TCP listener on `127.0.0.1:18080`, runs the
`AcceptLoop` on a worker thread, dials the listener N times from the main
thread, and asserts the accept callback fired N times. Exits 0 on success,
non-zero otherwise.

## Run

Requires Apple `container` 0.11+ (or any compatible OCI runtime — set
`RUNTIME=docker` / `RUNTIME=podman`). On macOS, start the container service
first:

```bash
container system start
./bench/iouring/run.sh
```

The script:

1. Cross-compiles `iouring_smoke` for `aarch64-linux-musl` on the host.
2. Builds the OCI image from this directory.
3. Runs the smoke binary inside a fresh container.

A passing run prints something like:

```
listening on 127.0.0.1:18080 (fd=3)
client 1/16 connected
...
client 16/16 connected
io_uring AcceptLoop saw 16 accepts (wanted >= 16)
OK
==> io_uring smoke test PASSED
```

The kernel version reported on a clean `container run alpine:3.20 uname -a`
on macOS 26 / `container` 0.11 is `Linux ... 6.18.5 ... aarch64`, well above
the 5.19 minimum for `IORING_OP_ACCEPT_MULTISHOT`.

## Limitations

* Only the accept loop is exercised. Per-connection `recv` / `send` over
`io_uring` is not implemented yet — see the staged plan in
[`zig/src/iouring.zig`](../../zig/src/iouring.zig).
* No request/response payload is sent; the test closes accepted fds
immediately.
* No latency or throughput numbers are produced. Per `AGENTS.md`, do not
cite this script in any benchmark table or release note.
30 changes: 30 additions & 0 deletions bench/iouring/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Build the io_uring smoke-test binary for Linux (aarch64-linux-musl by
# default; override with TARGET=...). Designed to run on macOS via Apple
# `container`, on a Linux dev box natively, or in CI on a Linux runner.
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT"

TARGET="${TARGET:-aarch64-linux-musl}"
OUT="bench/iouring/iouring_smoke"

# Stub turbo_build_options so iouring.zig compiles standalone without the
# whole turbonet build graph.
STUB_DIR="$(mktemp -d)"
trap 'rm -rf "$STUB_DIR"' EXIT
cat > "$STUB_DIR/turbo_build_options.zig" <<EOF
pub const iouring_enabled: bool = true;
EOF

echo "==> cross-compiling iouring smoke for $TARGET"
zig build-exe -target "$TARGET" -O ReleaseSafe -femit-bin="$OUT" \
--dep iouring \
-Mroot=bench/iouring/iouring_smoke.zig \
--dep turbo_build_options \
-Miouring=zig/src/iouring.zig \
-Mturbo_build_options="$STUB_DIR/turbo_build_options.zig"

file "$OUT"
echo "==> built $OUT"
134 changes: 134 additions & 0 deletions bench/iouring/iouring_smoke.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! End-to-end smoke test for the iouring AcceptLoop on Linux.
//!
//! Pipeline:
//! 1. Open a non-blocking TCP listen socket on 127.0.0.1:<port>.
//! 2. Spawn a worker thread running `iouring.Linux.AcceptLoop.run`.
//! Each accepted fd is counted then closed immediately.
//! 3. From the main thread, open N TCP connections to the listener.
//! 4. Wait until the AcceptLoop has counted N accepts, then stop it and
//! assert that we saw exactly N.
//!
//! Exits 0 on success, non-zero on any failure. Designed to be a one-shot
//! sanity check inside an Apple `container` Linux VM (kernel 6.x), not a
//! benchmark.

const std = @import("std");
const builtin = @import("builtin");
const iouring = @import("iouring");

const linux = std.os.linux;
const posix = std.posix;

const N_CONNECTIONS: usize = 16;
const PORT: u16 = 18080;

const Counter = struct {
accepts: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
loop: ?*iouring.Linux.AcceptLoop = null,
};

pub fn main() !void {
if (builtin.os.tag != .linux) {
std.debug.print("skip: not Linux\n", .{});
return;
}
if (!iouring.Available) {
std.debug.print("skip: iouring.Available == false\n", .{});
return;
}

// ── 1. listen socket ──
const listen_fd = try createListenSocket(PORT);
defer _ = linux.close(listen_fd);
std.debug.print("listening on 127.0.0.1:{d} (fd={d})\n", .{ PORT, listen_fd });

// ── 2. accept loop on its own thread ──
var counter = Counter{};
var loop = try iouring.Linux.AcceptLoop.init(
listen_fd,
onAccept,
@ptrCast(&counter),
iouring.DEFAULT_SQ_ENTRIES,
);
defer loop.deinit();
counter.loop = &loop;

const t = try std.Thread.spawn(.{}, runLoop, .{&loop});
defer t.join();

// ── 3. connect N times ──
for (0..N_CONNECTIONS) |i| {
try dialOnce(PORT);
std.debug.print(" client {d}/{d} connected\n", .{ i + 1, N_CONNECTIONS });
}

// ── 4. wait + assert ──
var waited_ms: usize = 0;
while (counter.accepts.load(.acquire) < N_CONNECTIONS and waited_ms < 5_000) {
var ts: linux.timespec = .{ .sec = 0, .nsec = 10 * std.time.ns_per_ms };
_ = linux.nanosleep(&ts, null);
waited_ms += 10;
}

loop.stop();

// Touch the loop one more time so copy_cqes wakes up. A no-op connect is
// the cheapest way to force a CQE.
dialOnce(PORT) catch {};

const got = counter.accepts.load(.acquire);
std.debug.print("io_uring AcceptLoop saw {d} accepts (wanted >= {d})\n", .{ got, N_CONNECTIONS });
if (got < N_CONNECTIONS) {
Comment on lines +77 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude wake-up connect from smoke-test acceptance count

The test adds an extra dialOnce(PORT) after loop.stop() and then validates got >= N_CONNECTIONS, so a dropped accept among the original N clients can be masked by this wake-up connection and still pass. In the failure case where one initial connection is missed, the post-stop dial can bring the counter back to N and produce a false green smoke test, which undermines this check’s purpose as a correctness gate.

Useful? React with 👍 / 👎.

std.debug.print("FAIL\n", .{});
std.process.exit(1);
}
std.debug.print("OK\n", .{});
}

fn runLoop(loop: *iouring.Linux.AcceptLoop) void {
loop.run() catch |err| {
std.debug.print("loop.run errored: {s}\n", .{@errorName(err)});
};
}

fn onAccept(ctx: *anyopaque, fd: posix.fd_t) void {
const counter: *Counter = @ptrCast(@alignCast(ctx));
_ = counter.accepts.fetchAdd(1, .acq_rel);
_ = linux.close(fd);
}

fn createListenSocket(port: u16) !posix.fd_t {
const fd_signed = linux.socket(linux.AF.INET, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0);
if (fd_signed < 0) return error.SocketFailed;
const fd: posix.fd_t = @intCast(fd_signed);

const yes: c_int = 1;
_ = linux.setsockopt(fd, linux.SOL.SOCKET, linux.SO.REUSEADDR, std.mem.asBytes(&yes), @sizeOf(c_int));

var addr: linux.sockaddr.in = .{
.family = linux.AF.INET,
.port = std.mem.nativeToBig(u16, port),
.addr = std.mem.nativeToBig(u32, 0x7F000001), // 127.0.0.1
.zero = .{0} ** 8,
};
if (linux.bind(fd, @ptrCast(&addr), @sizeOf(linux.sockaddr.in)) != 0) return error.BindFailed;
if (linux.listen(fd, 128) != 0) return error.ListenFailed;
return fd;
}

fn dialOnce(port: u16) !void {
const fd_signed = linux.socket(linux.AF.INET, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0);
if (fd_signed < 0) return error.SocketFailed;
const fd: posix.fd_t = @intCast(fd_signed);
defer _ = linux.close(fd);

var addr: linux.sockaddr.in = .{
.family = linux.AF.INET,
.port = std.mem.nativeToBig(u16, port),
.addr = std.mem.nativeToBig(u32, 0x7F000001),
.zero = .{0} ** 8,
};
if (linux.connect(fd, @ptrCast(&addr), @sizeOf(linux.sockaddr.in)) != 0) {
return error.ConnectFailed;
}
}
39 changes: 39 additions & 0 deletions bench/iouring/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# End-to-end: build the io_uring smoke binary, package it into an OCI image,
# run it inside Apple `container` (or any compatible runtime), and check the
# exit code.
#
# This validates that:
# * zig/src/iouring.zig compiles for Linux
# * std.os.linux.IoUring works on the kernel inside Apple's lightweight VM
# * IORING_OP_ACCEPT_MULTISHOT delivers all expected accepts
#
# This is a correctness check, NOT a benchmark. Per AGENTS.md, no perf
# numbers should be cited from this script.
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
BENCH_DIR="$REPO_ROOT/bench/iouring"
RUNTIME="${RUNTIME:-container}"
IMAGE="${IMAGE:-turboapi-iouring-smoke:latest}"
NAME="${NAME:-turboapi-iouring-smoke}"

cd "$REPO_ROOT"

# 1. Build the static Linux binary on the host.
"$BENCH_DIR/build.sh"

# 2. Build the OCI image.
echo "==> building $IMAGE via $RUNTIME"
"$RUNTIME" build -t "$IMAGE" -f "$BENCH_DIR/Containerfile" "$BENCH_DIR"

# 3. Run it; non-zero exit => smoke test failed.
echo "==> running smoke test"
"$RUNTIME" rm -f "$NAME" 2>/dev/null || true
if "$RUNTIME" run --rm --name "$NAME" "$IMAGE"; then
echo "==> io_uring smoke test PASSED"
else
rc=$?
echo "==> io_uring smoke test FAILED (exit $rc)" >&2
exit "$rc"
fi
14 changes: 14 additions & 0 deletions zig/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ pub fn build(b: *std.Build) void {
const lib_path = b.option([]const u8, "py-libdir", "Python lib path (required)") orelse
@panic("pass -Dpy-libdir=<path> or use: python zig/build_turbonet.py");

// ── Optional Linux io_uring accept loop ──
// -Diouring=true enables IORING_OP_ACCEPT_MULTISHOT on the listen socket
// and dispatches accepted fds onto the existing thread pool. No-op on
// non-Linux targets even when set. Off by default; the per-connection
// recv/send fast path is *not* on io_uring yet — see zig/src/iouring.zig
// for the staged plan.
const iouring_enabled = b.option(bool, "iouring", "Enable Linux io_uring accept loop (Linux only, off by default)") orelse false;

const turbo_options = b.addOptions();
turbo_options.addOption(bool, "iouring_enabled", iouring_enabled);
const turbo_options_mod = turbo_options.createModule();

const py_lib_name: []const u8 = if (is_free_threaded)
"python3.14t"
else if (std.mem.eql(u8, py_version, "3.14"))
Expand Down Expand Up @@ -67,6 +79,7 @@ pub fn build(b: *std.Build) void {
lib.root_module.addImport("model", model_mod);
lib.root_module.addImport("pg", pg_mod);
lib.root_module.addImport("turboapi-core", core_mod);
lib.root_module.addImport("turbo_build_options", turbo_options_mod);

lib.root_module.addIncludePath(.{ .cwd_relative = include_path });
lib.root_module.addRPathSpecial("@loader_path");
Expand Down Expand Up @@ -111,6 +124,7 @@ pub fn build(b: *std.Build) void {
tests.root_module.addImport("model", model_mod);
tests.root_module.addImport("pg", pg_mod);
tests.root_module.addImport("turboapi-core", core_mod);
tests.root_module.addImport("turbo_build_options", turbo_options_mod);
tests.root_module.addIncludePath(.{ .cwd_relative = include_path });
tests.root_module.addLibraryPath(.{ .cwd_relative = lib_path });
tests.root_module.linkSystemLibrary(py_lib_name, .{});
Expand Down
Loading