Skip to content
Draft
4 changes: 4 additions & 0 deletions frameworks/zix-grpc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache
zig-out
zig-package
vendor
53 changes: 30 additions & 23 deletions frameworks/zix-grpc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,40 @@ 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

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}"

# 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.
# Four ordered attempts before giving up: curl the archive tarball from github then
# codeberg, then a shallow git clone from github then codeberg. The github archive
# redirects to codeload.github.com (which the benchmark runner may not resolve), so
# curl can fall through to the codeberg tarball, and git clone talks to github.com
# and codeberg.org directly as the deeper fallback. RETRY and RETRY_DELAY bound
# every attempt.
WORKDIR /server
COPY build.zig build.zig.zon ./
COPY src ./src

# Vendor zix ${ZIX_VERSION} in its own layer so source-only rebuilds skip the fetch.
# Tries curl tarball then git clone, github before codeberg.
# RETRY / RETRY_DELAY bound 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 +52,25 @@ 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
# Add +aes+pclmul: x86_64_v3 omits AES-NI / PCLMUL,
# so zix TLS would compile the ~40x slower software AES-GCM.
# 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

# Self-signed Ed25519 cert generated at image build, baked at /etc/zix-tls.
RUN set -eu; \
mkdir -p /etc/zix-tls; \
openssl genpkey -algorithm ED25519 -out /etc/zix-tls/server.key; \
openssl req -new -x509 -key /etc/zix-tls/server.key -out /etc/zix-tls/server.crt \
-days 3650 -subj "/CN=localhost"

FROM alpine:3.20
COPY --from=build /src/zig-out/bin/zix-grpc /zix-grpc
EXPOSE 8080
COPY --from=build /server/zig-out/bin/zix-grpc /zix-grpc
COPY --from=build /etc/zix-tls /etc/zix-tls
EXPOSE 8080 8443/tcp
ENTRYPOINT ["/zix-grpc"]
12 changes: 7 additions & 5 deletions frameworks/zix-grpc/meta.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"display_name": "zix-grpc",
"display_name": "zix",
"language": "Zig",
"type": "engine",
"engine": "zix",
"description": "Zig gRPC server (h2c) on the zix.Grpc engine built on zix.Http2 (no std.http). Shared-nothing by design: each worker owns its own SO_REUSEPORT listener, io_uring completion ring, and connections, with no shared state or locking across cores, multiplexing HTTP/2 streams per connection. Replies use comptime-cached HPACK blocks. Implements GetSum (unary) and StreamSum (server-streaming).",
"repo": "https://codeberg.org/prothegee/zix",
"engine": "zix.Grpc URING Dispatch Model",
"description": "Zig gRPC server on the zix.Grpc engine (native h2 framing), .URING dispatch: shared-nothing per-core io_uring with SO_REUSEPORT, unary plus server-streaming with DATA-frame coalescing. Dual listener (config.tls_port): h2c 8080 plus gRPC over TLS 1.3 on 8443, one process.",
"repo": "https://github.com/prothegee/zix",
"enabled": true,
"tests": [
"unary-grpc",
"stream-grpc"
"stream-grpc",
"unary-grpc-tls",
"stream-grpc-tls"
],
"maintainers": ["prothegee"]
}
75 changes: 45 additions & 30 deletions frameworks/zix-grpc/src/main.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
//! HttpArena: zix-grpc
//!
//! zix HttpArena gRPC (h2c) entry point.
//! zix HttpArena gRPC entry point.
//!
//! Intent: demonstrate zix.Grpc (EPOLL dispatch model) against the HttpArena
//! gRPC benchmark suite (unary, server-streaming).
//! Intent: demonstrate zix.Grpc (URING dispatch model) against the HttpArena
//! gRPC benchmark suite (unary, server-streaming), cleartext h2c and over TLS.
//!
//! ONE server, two listeners through config.tls_port (dual listener):
//! - h2c cleartext on PORT (8080). Serves unary-grpc and stream-grpc.
//! - gRPC over TLS 1.3 on TLS_PORT (8443), ALPN h2, self-signed Ed25519 cert
//! baked at /etc/zix-tls, terminated on the same per-core .URING workers
//! (no second launch, no doubled workers or fd tables).
//! Serves unary-grpc-tls and stream-grpc-tls.
//!
//! Design choices:
//! - GetSum: unary SumRequest{a, b} -> SumReply{a + b}. The compute is a single
Expand All @@ -12,32 +19,21 @@
//! - StreamSum: server-streaming, count replies of a + b + i.
//! - max_streams is wide enough that a client opening many parallel streams is
//! never refused at startup.

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

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

const IP: []const u8 = "::";
const PORT: u16 = 8080;
/// Required for ipv4 and ipv6
const LISTEN_IP: []const u8 = "::";
const DISPATCH_MODEL: zix.Grpc.DispatchModel = .URING;
const KERNEL_BACKLOG: u31 = 1024 * 16;
const WORKERS: usize = 0;

/// 0 selects the engine default EPOLL pool (max(10, cpu*2)). Each worker owns one connection
/// while it is active. After the Phase 1 syscall cuts the unary path is CPU-bound, not
/// connection-bound: a modest cpu-relative pool tops out throughput, while an oversized pool
/// (thread-per-connection) thrashes the scheduler and collapses it. So keep the default.
const POOL_SIZE: usize = 0;

/// Advertise enough concurrent streams that a client opening many in parallel (h2load uses
/// -m 100) is never refused at startup. Must be >= the load generator's stream count or those
/// streams get REFUSED_STREAM. Per-stream buffers are tiny (below), so a wide table is cheap.
const MAX_STREAMS: usize = 128;
const WORKERS: usize = 0;

/// gRPC sum messages are a few bytes. A small per-stream body buffer keeps the wide stream
/// table affordable in memory (MAX_STREAMS * MAX_BODY per connection).
const MAX_BODY: usize = 4 * 1024;
const TLS_PORT: u16 = 8443;
const TLS_CERT_DEFAULT: []const u8 = "/etc/zix-tls/server.crt";
const TLS_KEY_DEFAULT: []const u8 = "/etc/zix-tls/server.key";

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

Expand Down Expand Up @@ -108,20 +104,39 @@ fn streamSumHandler(headers: []const zix.Http2.Header, ctx: *zix.Grpc.Context) v

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

const Routes = &[_]zix.Grpc.Route{
.{ .path = "/benchmark.BenchmarkService/GetSum", .handler = getSumHandler },
.{ .path = "/benchmark.BenchmarkService/StreamSum", .handler = streamSumHandler, .is_server_streaming = true },
};

pub fn main(process: std.process.Init) !void {
var server = try zix.Grpc.Server.init(&[_]zix.Grpc.Route{
.{ .path = "/benchmark.BenchmarkService/GetSum", .handler = getSumHandler },
.{ .path = "/benchmark.BenchmarkService/StreamSum", .handler = streamSumHandler, .is_server_streaming = true },
}, .{
var allocator_tls = std.heap.ArenaAllocator.init(std.heap.smp_allocator);
defer allocator_tls.deinit();

var tls = zix.Tls.Context.init(allocator_tls.allocator(), process.io, .{
.cert_path = TLS_CERT_DEFAULT,
.key_path = TLS_KEY_DEFAULT,
.alpn = &.{.H2},
.min_version = .TLS_1_3,
}) catch |e| {
std.debug.print("Error tls context: {}\n", .{e});
return;
};
defer tls.deinit();

var server = zix.Grpc.Server.init(Routes, .{
.io = process.io,
.ip = LISTEN_IP,
.ip = IP,
.port = PORT,
.tls = &tls,
.tls_port = TLS_PORT,
.dispatch_model = DISPATCH_MODEL,
.kernel_backlog = KERNEL_BACKLOG,
.workers = WORKERS,
.pool_size = POOL_SIZE,
.max_streams = MAX_STREAMS,
.max_body = MAX_BODY,
.kernel_backlog = 24 * 1024,
.max_streams = 1024,
.max_frame_size = 24 * 1024,
.max_recv_buf = 64 * 1024,
.max_body = 32 * 1024,
.tls_write_buf_initial_bytes = 32 * 1024,
});
defer server.deinit();

Expand Down
42 changes: 35 additions & 7 deletions site/data/frameworks.json
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,34 @@
"type": "engine",
"engine": "veb"
},
"WebFramework C": {
"dir": "web-framework-cc",
"description": "Web framework in C++ with different language APIs(Python, C, C++, C#)",
"repo": "https://github.com/LazyPanda07/WebFramework",
"type": "production",
"engine": "WebFramework"
},
"WebFramework C++": {
"dir": "web-framework-cpp",
"description": "Web framework in C++ with different language APIs(Python, C, C++, C#)",
"repo": "https://github.com/LazyPanda07/WebFramework",
"type": "production",
"engine": "WebFramework"
},
"WebFramework C#": {
"dir": "web-framework-csharp",
"description": "Web framework in C++ with different language APIs(Python, C, C++, C#)",
"repo": "https://github.com/LazyPanda07/WebFramework",
"type": "production",
"engine": "WebFramework"
},
"WebFramework Python": {
"dir": "web-framework-python",
"description": "Web framework in C++ with different language APIs(Python, C, C++, C#)",
"repo": "https://github.com/LazyPanda07/WebFramework",
"type": "production",
"engine": "WebFramework"
},
"workerman-websocket": {
"dir": "workerman-websocket",
"description": "An asynchronous event driven PHP socket framework. Supports HTTP, Websocket, SSL and other custom protocols.",
Expand Down Expand Up @@ -982,20 +1010,20 @@
"type": "engine",
"engine": "io_uring"
},
"zix-grpc": {
"dir": "zix-grpc",
"description": "Zig gRPC server (h2c) on the zix.Grpc engine built on zix.Http2 (no std.http). Shared-nothing by design: each worker owns its own SO_REUSEPORT listener, io_uring completion ring, and connections, with no shared state or locking across cores, multiplexing HTTP/2 streams per connection. Replies use comptime-cached HPACK blocks. Implements GetSum (unary) and StreamSum (server-streaming).",
"repo": "https://codeberg.org/prothegee/zix",
"type": "engine",
"engine": "zix"
},
"zix": {
"dir": "zix",
"description": "Zig HTTP/1.1 server on the zix.Http1 raw engine (no std.http). Shared-nothing: each worker runs its own SO_REUSEPORT multishot accept plus io_uring completion loop and owns its connections. The /json endpoint serves from the per-worker response cache, and request bodies larger than the read buffer are drained rather than buffered.",
"repo": "https://github.com/prothegee/zix",
"type": "engine",
"engine": "zix",
"variants": [
{
"dir": "zix-grpc",
"description": "Zig gRPC server on the zix.Grpc engine (native h2 framing), .URING dispatch: shared-nothing per-core io_uring with SO_REUSEPORT, unary plus server-streaming with DATA-frame coalescing. Dual listener (config.tls_port): h2c 8080 plus gRPC over TLS 1.3 on 8443, one process.",
"repo": "https://github.com/prothegee/zix",
"type": "engine",
"engine": "zix.Grpc URING Dispatch Model"
},
{
"dir": "zix-http2",
"description": "Zig HTTP/2 server on the zix.Http2 raw engine, .URING dispatch: shared-nothing per-core io_uring with SO_REUSEPORT, per-worker stream-slot pool. Dual listener (config.tls_port): h2c 8082 plus h2 over TLS 1.3 (ALPN h2) on 8443, one process.",
Expand Down
Loading