Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions frameworks/zix-ws/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache
zig-out
zig-package
vendor
31 changes: 18 additions & 13 deletions frameworks/zix-ws/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ FROM alpine:3.20 AS build
ARG RETRY=6
ARG TARGETARCH
ARG RETRY_DELAY=3
ARG TIMEOUT_SEC=180
ARG ZIG_VERSION=0.16.0
ARG ZIX_VERSION=0.4.x-rc3
RUN apk add --no-cache ca-certificates curl git tar xz
ARG ZIX_VERSION=0.5.x-rc1
RUN apk add --no-cache ca-certificates curl git tar xz openssl

WORKDIR /server
RUN set -eu; \
case "${TARGETARCH:-amd64}" in \
amd64) ZIG_ARCH=x86_64 ;; \
arm64) ZIG_ARCH=aarch64 ;; \
*) echo "unsupported arch: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
curl -fSL -m ${TIMEOUT_SEC} "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C /opt; \
mv "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}" /opt/zig
ENV PATH="/opt/zig:${PATH}"

COPY build.zig build.zig.zon ./
COPY src ./src

# Vendor zix X.Y.Z, separate layer so source-only rebuilds skip the fetch.
# The Http1 raw engine work this image needs (large-body drain plus the per-worker
# response cache used by the /json endpoint) must be present on the X.Y.Z branch.
Expand All @@ -30,15 +35,15 @@ ENV PATH="/opt/zig:${PATH}"
# every attempt.
RUN set -eu; \
fetch() { \
rm -rf /src/vendor/zix; mkdir -p /src/vendor/zix; \
curl -fsSL --retry ${RETRY} --retry-delay ${RETRY_DELAY} --retry-all-errors "$1" -o /tmp/zix.tar.gz \
&& tar -xz --strip-components=1 -C /src/vendor/zix -f /tmp/zix.tar.gz; \
rm -rf /server/vendor/zix; mkdir -p /server/vendor/zix; \
curl -fSL --retry ${RETRY} --retry-delay ${RETRY_DELAY} --retry-all-errors "$1" -o /tmp/zix.tar.gz \
&& tar -xz --strip-components=1 -C /server/vendor/zix -f /tmp/zix.tar.gz; \
}; \
clone() { \
attempt=0; \
while [ "${attempt}" -lt "${RETRY}" ]; do \
rm -rf /src/vendor/zix; \
git clone --depth 1 --branch "${ZIX_VERSION}" "$1" /src/vendor/zix && return 0; \
rm -rf /server/vendor/zix; \
git clone --depth 1 --branch "${ZIX_VERSION}" "$1" /server/vendor/zix && return 0; \
attempt=$((attempt + 1)); \
sleep "${RETRY_DELAY}"; \
done; \
Expand All @@ -53,17 +58,17 @@ RUN set -eu; \
clone "https://codeberg.org/prothegee/zix.git" \
|| { echo "FAILED: git clone ${RETRY} times from codeberg" >&2; exit 1; }; }; }; }

WORKDIR /src
COPY build.zig build.zig.zon ./
COPY src ./src
# +aes+pclmul: x86_64_v3 omits AES-NI / PCLMUL,
# so zix TLS would compile the ~40x slower software AES-GCM.
# +adx speeds the RSA / Montgomery path. Every x86_64_v3 CPU has them, so it is safe.
RUN set -eu; \
case "${TARGETARCH:-amd64}" in \
amd64) ZIG_TARGET=x86_64-linux-musl; ZIG_CPU=x86_64_v3 ;; \
arm64) ZIG_TARGET=aarch64-linux-musl; ZIG_CPU=baseline ;; \
esac; \
zig build -Dtarget="${ZIG_TARGET}" -Dcpu="${ZIG_CPU}" --release=fast
zig build -Dtarget="${ZIG_TARGET}" -Dcpu="${ZIG_CPU}+aes+pclmul+adx" --release=fast

FROM alpine:3.20
COPY --from=build /src/zig-out/bin/zix-ws /zix-ws
COPY --from=build /server/zig-out/bin/zix-ws /zix-ws
EXPOSE 8080
ENTRYPOINT ["/zix-ws"]
6 changes: 3 additions & 3 deletions frameworks/zix-ws/meta.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"display_name": "zix-ws",
"display_name": "zix",
"language": "Zig",
"type": "engine",
"engine": "zix",
"description": "Zig WebSocket echo server on the zix.Http1 raw engine (no std.http). Shared-nothing by design: each worker owns its own SO_REUSEPORT multishot accept, io_uring completion ring, and connections, with no shared state or locking across cores. The upgrade and echo are engine-owned: frames are read from a shared per-worker provided-buffer ring (an idle connection holds no buffer), and a pipelined burst is coalesced into one write.",
"engine": "zix.Http1 WebSocket URING Dispatch Model",
"description": "Zig WebSocket echo on the zix.Http1 engine-owned WebSocket path, .URING dispatch: GET /ws upgrades, WebSocket.serve echoes inside the engine, a pipelined burst coalesced into one write.",
"repo": "https://codeberg.org/prothegee/zix",
"enabled": true,
"tests": [
Expand Down
57 changes: 19 additions & 38 deletions frameworks/zix-ws/src/main.zig
Original file line number Diff line number Diff line change
@@ -1,57 +1,36 @@
//! HttpArena: zix-ws
//!
//! zix HttpArena WebSocket entry point.
//!
//! Intent: demonstrate the engine-owned WebSocket path of zix.Http1 (EPOLL
//! dispatch model) against the HttpArena echo and echo-pipeline suites.
//!
//! Design choices:
//! - GET /ws upgrades, then zix.Http1.WebSocket.serve drives the echo loop
//! inside the engine: frames are echoed on readiness and a pipelined burst is
//! coalesced into a single write.
//! - No response cache: echo is per-connection, not a broadcast fanout, so there
//! is nothing to precompute or share across connections.
//! zix.Http1 engine-owned WebSocket (.URING)
//! against the HttpArena echo and echo-pipeline suites.
//! GET /ws upgrades,
//! then WebSocket.serve drives the echo loop inside the engine (frames echoed on
//! readiness, a pipelined burst coalesced into one write).
//! No response cache: echo is per-connection, nothing to precompute or share.

const std = @import("std");
const zix = @import("zix");

// --------------------------------------------------------- //

const IP: []const u8 = "::";
const PORT: u16 = 8080;
const LISTEN_IP: []const u8 = "::";
const DISPATCH_MODEL: zix.Http1.DispatchModel = .URING;
const KERNEL_BACKLOG: u31 = 16 * 1024;

/// Per-machine tuning profile (ADR-041 increment 5): .lean for the 12-thread /
/// 32 GB dev box, .throughput for the 64-core / 251 GB competition box. Only the
/// HTTP handshake recv buffer differs (the WS frame buffer is already 32 KiB).
/// Select .throughput for the 64-core deployment.
const Profile = enum { lean, throughput };
const PROFILE: Profile = .lean;

const MAX_RECV_BUF: usize = switch (PROFILE) {
.lean => 4 * 1024,
.throughput => 16 * 1024,
};
const WS_RECV_BUF: usize = 32 * 1024;
const MAX_HEADERS: u8 = 16;
const WORKERS: usize = 0;

// --------------------------------------------------------- //

fn badRequest(fd: std.posix.fd_t) void {
zix.Http1.writeSimple(fd, 400, "text/plain", "bad request") catch {};
zix.Http1.sendSimpleFD(fd, 400, "text/plain", "bad request") catch {};
}

fn notFound(fd: std.posix.fd_t) void {
zix.Http1.writeSimple(fd, 404, "text/plain", "Not Found") catch {};
zix.Http1.sendSimpleFD(fd, 404, "text/plain", "Not Found") catch {};
}

// --------------------------------------------------------- //

// Echo every text/binary frame back. Ping/close are handled by the engine.
fn wsOnFrame(fd: std.posix.fd_t, opcode: u8, payload: []const u8) void {
zix.Http1.WebSocket.send(fd, @enumFromInt(opcode), payload) catch {};
zix.Http1.WebSocket.sendFD(fd, @enumFromInt(opcode), payload) catch {};
}

// GET /ws : WebSocket upgrade then engine-owned echo.
Expand All @@ -66,7 +45,7 @@ fn wsHandler(head: *const zix.Http1.ParsedHead, body: []const u8, fd: std.posix.
}

zix.Http1.WebSocket.serve(fd, ws_key.?, wsOnFrame) catch {
zix.Http1.writeSimple(fd, 500, "text/plain", "handshake failed") catch {};
zix.Http1.sendSimpleFD(fd, 500, "text/plain", "handshake failed") catch {};
return;
};
}
Expand All @@ -88,15 +67,17 @@ pub fn main(process: std.process.Init) !void {

var server = zix.Http1.Server.init(dispatch, .{
.io = process.io,
.ip = LISTEN_IP,
.ip = IP,
.port = PORT,
.dispatch_model = DISPATCH_MODEL,
.kernel_backlog = KERNEL_BACKLOG,
.max_recv_buf = MAX_RECV_BUF,
.ws_recv_buf = WS_RECV_BUF,
.max_headers = MAX_HEADERS,
.workers = WORKERS,
.workers = 0,
.kernel_backlog = 16 * 1024,
.max_recv_buf = 6 * 1024,
.ws_recv_buf = 32 * 1024,
.max_headers = 8,
.send_date_header = false,
.uring_send_buf_size = 16 * 1024,
.process_queue_len = 2_000_000,
});
defer server.deinit();

Expand Down