Skip to content
Open
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-grpc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache
zig-out
zig-package
vendor
28 changes: 16 additions & 12 deletions frameworks/zix-grpc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ ARG RETRY=6
ARG TARGETARCH
ARG RETRY_DELAY=3
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 \
Expand All @@ -19,15 +19,11 @@ RUN set -eu; \
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.
# 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.
#
# 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; \
fetch() { \
rm -rf /src/vendor/zix; mkdir -p /src/vendor/zix; \
Expand Down Expand Up @@ -61,9 +57,17 @@ RUN set -eu; \
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. Ed25519 handshake
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
COPY --from=build /etc/zix-tls /etc/zix-tls
EXPOSE 8080
ENTRYPOINT ["/zix-grpc"]
10 changes: 6 additions & 4 deletions frameworks/zix-grpc/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
"display_name": "zix-grpc",
"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 uring",
"description": "Zig gRPC server on the zix.Grpc engine (built on zix.Http2, no std.http). Shared-nothing: each worker owns its SO_REUSEPORT listener, io_uring loop, and connections, multiplexing HTTP/2 streams per connection with comptime-cached HPACK replies. h2c runs under the .URING model. Over TLS 1.3 (ALPN h2, baked self-signed Ed25519 cert) the per-core tls_mux terminates TLS in place. Implements GetSum (unary) and StreamSum (server-streaming).",
"repo": "https://github.com/prothegee/zix",
"enabled": true,
"tests": [
"unary-grpc",
"stream-grpc"
"stream-grpc",
"unary-grpc-tls",
"stream-grpc-tls"
],
"maintainers": ["prothegee"]
}
73 changes: 62 additions & 11 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.
//!
//! Two listeners run in parallel:
//! - h2c cleartext on PORT (8080) under the .URING dispatch model. Serves unary-grpc and
//! stream-grpc.
//! - gRPC over TLS 1.3 on H2_TLS_PORT (8443), ALPN h2, with a self-signed Ed25519 cert baked at
//! /etc/zix-tls. Serves unary-grpc-tls and stream-grpc-tls. The TLS path is the per-core tls_mux
//! (one worker per core, no thread-per-connection), shared by the .EPOLL and .URING models.
//!
//! Design choices:
//! - GetSum: unary SumRequest{a, b} -> SumReply{a + b}. The compute is a single
Expand All @@ -18,16 +25,21 @@ const zix = @import("zix");
// --------------------------------------------------------- //

const PORT: u16 = 8080;
const H2_TLS_PORT: u16 = 8443;
/// 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.
// TLS cert / key, a self-signed Ed25519 pair baked at /etc/zix-tls at image build. Overridable via env so the same
// binary runs locally.
const TLS_CERT_DEFAULT: []const u8 = "/etc/zix-tls/server.crt";
const TLS_KEY_DEFAULT: []const u8 = "/etc/zix-tls/server.key";

/// Per-core worker count for .URING (this entry's model) and .EPOLL. 0 selects one worker per CPU,
/// which the shared-nothing io_uring loop wants: the unary path is CPU-bound, so a per-core count
/// tops out throughput while oversubscription only thrashes the scheduler. Keep the default.
const POOL_SIZE: usize = 0;

/// Advertise enough concurrent streams that a client opening many in parallel (h2load uses
Expand Down Expand Up @@ -108,11 +120,50 @@ 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 },
};

// gRPC over TLS 1.3 listener: the per-core tls_mux terminates TLS (ALPN h2) in place (one worker per
// core, no thread-per-connection). A missing or unreadable cert degrades gracefully: this thread
// returns and the cleartext server keeps running.
fn tlsServer(io: std.Io, tls: *zix.Tls.Context) void {
var server = zix.Grpc.Server.init(ROUTES, .{
.io = io,
.ip = LISTEN_IP,
.port = H2_TLS_PORT,
.tls = tls,
.dispatch_model = DISPATCH_MODEL,
.kernel_backlog = KERNEL_BACKLOG,
.max_streams = MAX_STREAMS,
.max_body = MAX_BODY,
}) catch return;
defer server.deinit();

server.run() catch {};
}

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

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 },
}, .{
// gRPC over TLS on H2_TLS_PORT (ALPN h2) with the baked Ed25519 cert. The cleartext server runs
// regardless: a missing cert just leaves the TLS port without a listener.
const cert_path = process.environ_map.get("ARENA_TLS_CERT") orelse TLS_CERT_DEFAULT;
const key_path = process.environ_map.get("ARENA_TLS_KEY") orelse TLS_KEY_DEFAULT;

var tls_ctx: ?zix.Tls.Context = zix.Tls.Context.init(std.heap.smp_allocator, process.io, .{
.cert_path = cert_path,
.key_path = key_path,
.alpn = &.{.H2},
.min_version = .TLS_1_3,
}) catch null;

if (tls_ctx) |*tls| {
_ = std.Thread.spawn(.{}, tlsServer, .{ process.io, tls }) catch {};
}

var server = try zix.Grpc.Server.init(ROUTES, .{
.io = process.io,
.ip = LISTEN_IP,
.port = PORT,
Expand Down