Skip to content
Merged
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-http2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache
zig-out
zig-package
vendor
72 changes: 72 additions & 0 deletions frameworks/zix-http2/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# syntax=docker/dockerfile:1.7

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.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 -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

# Resolve zix with zig fetch (hash-verified branch tarball, no git),
# codeberg first then github, RETRY attempts each.
# The shipped build.zig.zon points .zix at vendor/zix for local staging:
# this step swaps that one line for the fetched url + hash,
# so a remote build needs no vendor directory.
RUN set -eu; \
fetch_zix() { \
url="$1"; attempt=1; \
while [ "${attempt}" -le "${RETRY}" ]; do \
if hash="$(zig fetch "${url}")"; then \
sed -i "s|\.path = \"vendor/zix\",|.url = \"${url}\", .hash = \"${hash}\",|" build.zig.zon; \
return 0; \
fi; \
echo "zix: ${url} attempt ${attempt}/${RETRY} failed" >&2; \
attempt=$((attempt + 1)); \
[ "${attempt}" -le "${RETRY}" ] && sleep "${RETRY_DELAY}"; \
done; \
return 1; \
}; \
fetch_zix "https://codeberg.org/prothegee/zix/archive/${ZIX_VERSION}.tar.gz" \
|| { echo "zix: codeberg exhausted ${RETRY} attempts, trying github" >&2; \
fetch_zix "https://github.com/prothegee/zix/archive/refs/heads/${ZIX_VERSION}.tar.gz"; } \
|| { echo "zix: github exhausted ${RETRY} attempts" >&2; exit 1; }

# +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}+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 /server/zig-out/bin/zix-http2 /zix-http2
COPY --from=build /etc/zix-tls /etc/zix-tls
EXPOSE 8443/tcp 8443/udp
ENTRYPOINT ["/zix-http2"]
26 changes: 26 additions & 0 deletions frameworks/zix-http2/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

const zix_dep = b.dependency("zix", .{ .target = target, .optimize = optimize });
const zix_mod = zix_dep.module("zix");

const exe = b.addExecutable(.{
.name = "zix-http2",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.strip = true,
}),
});
exe.root_module.addImport("zix", zix_mod);
b.installArtifact(exe);

const run_step = b.step("run", "Run the HTTP/2 server");
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}
16 changes: 16 additions & 0 deletions frameworks/zix-http2/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.{
.name = .zix_http2_arena,
.version = "0.1.0",
.fingerprint = 0x62ec9e924e0a448,
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
.dependencies = .{
.zix = .{
.path = "vendor/zix",
},
},
}
16 changes: 16 additions & 0 deletions frameworks/zix-http2/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"display_name": "zix",
"language": "Zig",
"type": "engine",
"engine": "zix.Http2 URING Dispatch Model",
"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.",
"repo": "https://github.com/prothegee/zix",
"enabled": true,
"tests": [
"baseline-h2",
"static-h2",
"baseline-h2c",
"json-h2c"
],
"maintainers": ["prothegee"]
}
167 changes: 167 additions & 0 deletions frameworks/zix-http2/src/dataset.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! HttpArena: zix
//!
//! Dataset loader for the /json endpoint.
//!
//! Loads the fixed 50-item benchmark dataset once at startup and pre-renders
//! each item as a JSON object fragment (without the closing brace), so the hot
//! path only appends the per-request total and the closing brace.

const std = @import("std");

pub const ItemCount = 50;

pub const Item = struct {
/// Pre-rendered JSON object for this item, WITHOUT the closing `}`.
/// Caller appends `,"total":<n>}` per request.
prefix: []const u8,
/// price * quantity, pre-multiplied so per-request work is one *m
/// followed by an integer-to-decimal print.
pq: u64,
};

pub const Dataset = struct {
items: []Item,
arena: std.heap.ArenaAllocator,

pub fn deinit(self: *Dataset) void {
self.arena.deinit();
}
};

pub fn load(gpa: std.mem.Allocator, path: []const u8) !Dataset {
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const aa = arena.allocator();

const raw = try readFileAlloc(aa, path, 4 * 1024 * 1024);

var parsed = try std.json.parseFromSlice(std.json.Value, aa, raw, .{});
defer parsed.deinit();

const arr = switch (parsed.value) {
.array => |a| a,
else => return error.BadDataset,
};
if (arr.items.len != ItemCount) return error.BadDataset;

const items = try aa.alloc(Item, ItemCount);
for (arr.items, 0..) |elem, i| {
const obj = switch (elem) {
.object => |o| o,
else => return error.BadDataset,
};
const price = jsonInt(obj.get("price") orelse return error.BadDataset);
const quantity = jsonInt(obj.get("quantity") orelse return error.BadDataset);

var buf: std.ArrayList(u8) = .empty;
try renderItemPrefix(&buf, aa, obj);
items[i] = .{
.prefix = try buf.toOwnedSlice(aa),
.pq = @as(u64, @intCast(price)) * @as(u64, @intCast(quantity)),
};
}

return .{ .items = items, .arena = arena };
}

fn readFileAlloc(aa: std.mem.Allocator, path: []const u8, max: usize) ![]u8 {
var path_z: [std.posix.PATH_MAX]u8 = undefined;
if (path.len >= path_z.len) return error.NameTooLong;
@memcpy(path_z[0..path.len], path);
path_z[path.len] = 0;
const fd = try std.posix.openatZ(std.posix.AT.FDCWD, @ptrCast(&path_z), .{ .ACCMODE = .RDONLY }, 0);
defer _ = std.posix.system.close(fd);

var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(aa);
try buf.ensureTotalCapacity(aa, 64 * 1024);
while (buf.items.len < max) {
try buf.ensureUnusedCapacity(aa, 32 * 1024);
const dst = buf.unusedCapacitySlice();
const n = try std.posix.read(fd, dst);
if (n == 0) break;
buf.items.len += n;
}
return buf.toOwnedSlice(aa);
}

fn jsonInt(v: std.json.Value) i64 {
return switch (v) {
.integer => |n| n,
.float => |f| @intFromFloat(f),
else => 0,
};
}

fn renderItemPrefix(buf: *std.ArrayList(u8), aa: std.mem.Allocator, obj: std.json.ObjectMap) !void {
try buf.append(aa, '{');
var first = true;
var it = obj.iterator();
while (it.next()) |kv| {
if (!first) try buf.append(aa, ',');
first = false;
try writeString(buf, aa, kv.key_ptr.*);
try buf.append(aa, ':');
try writeValue(buf, aa, kv.value_ptr.*);
}
// Intentionally no closing `}`: the caller appends `,"total":N}`.
}

fn writeValue(buf: *std.ArrayList(u8), aa: std.mem.Allocator, v: std.json.Value) !void {
switch (v) {
.null => try buf.appendSlice(aa, "null"),
.bool => |b| try buf.appendSlice(aa, if (b) "true" else "false"),
.integer => |n| try writeInt(buf, aa, n),
.float => |f| {
var tmp: [32]u8 = undefined;
const s = std.fmt.bufPrint(&tmp, "{d}", .{f}) catch unreachable;
try buf.appendSlice(aa, s);
},
.number_string => |ns| try buf.appendSlice(aa, ns),
.string => |s| try writeString(buf, aa, s),
.array => |arr| {
try buf.append(aa, '[');
for (arr.items, 0..) |e, i| {
if (i > 0) try buf.append(aa, ',');
try writeValue(buf, aa, e);
}
try buf.append(aa, ']');
},
.object => |o| {
try buf.append(aa, '{');
var first = true;
var it = o.iterator();
while (it.next()) |kv| {
if (!first) try buf.append(aa, ',');
first = false;
try writeString(buf, aa, kv.key_ptr.*);
try buf.append(aa, ':');
try writeValue(buf, aa, kv.value_ptr.*);
}
try buf.append(aa, '}');
},
}
}

fn writeInt(buf: *std.ArrayList(u8), aa: std.mem.Allocator, n: i64) !void {
var tmp: [24]u8 = undefined;
const s = std.fmt.bufPrint(&tmp, "{d}", .{n}) catch unreachable;
try buf.appendSlice(aa, s);
}

fn writeString(buf: *std.ArrayList(u8), aa: std.mem.Allocator, s: []const u8) !void {
try buf.append(aa, '"');
for (s) |c| {
switch (c) {
'"' => try buf.appendSlice(aa, "\\\""),
'\\' => try buf.appendSlice(aa, "\\\\"),
0x00...0x1f => {
var esc: [6]u8 = undefined;
_ = std.fmt.bufPrint(&esc, "\\u{x:0>4}", .{c}) catch unreachable;
try buf.appendSlice(aa, esc[0..6]);
},
else => try buf.append(aa, c),
}
}
try buf.append(aa, '"');
}
Loading