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

FROM alpine:3.20 AS build
ARG RETRY=6
ARG TARGETARCH
ARG RETRY_DELAY=3
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

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" \
| tar -xJ -C /opt; \
mv "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}" /opt/zig
ENV PATH="/opt/zig:${PATH}"

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

# Resolve zix with zig fetch, then build in the same layer. The mirror and version fill the
# build.zig.zon placeholder, zig fetch downloads it and prints the hash. Codeberg first, GitHub second.
#
# 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; \
cp build.zig.zon build.zig.zon.tmpl; \
save_zix() { \
base="$1"; \
attempt=1; \
while [ "${attempt}" -le "${RETRY}" ]; do \
sed -e "s|{URL_ZIX_SOURCE}|${base}|g" -e "s|{ZIX_VERSION}|${ZIX_VERSION}|g" \
build.zig.zon.tmpl > build.zig.zon; \
if hash="$(zig fetch "${base}/${ZIX_VERSION}.tar.gz")"; then \
awk -v h="${hash}" '{ print } /\.url = /{ print " .hash = \"" h "\"," }' \
build.zig.zon > build.zig.zon.next; \
mv build.zig.zon.next build.zig.zon; \
return 0; \
fi; \
echo "zix: ${base} attempt ${attempt}/${RETRY} failed" >&2; \
attempt=$((attempt + 1)); \
[ "${attempt}" -le "${RETRY}" ] && sleep "${RETRY_DELAY}"; \
done; \
return 1; \
}; \
save_zix "https://codeberg.org/prothegee/zix/archive" \
|| { echo "zix: codeberg exhausted ${RETRY} attempts, trying github" >&2; \
save_zix "https://github.com/prothegee/zix/archive/refs/heads"; } \
|| { echo "zix: github exhausted ${RETRY} attempts" >&2; exit 1; }; \
rm -f build.zig.zon.tmpl; \
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. 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-http2 /zix-http2
COPY --from=build /etc/zix-tls /etc/zix-tls
EXPOSE 8082 8443
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_arena,
.version = "0.1.0",
.fingerprint = 0x84cceaddc218682f,
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
.dependencies = .{
.zix = .{
.url = "{URL_ZIX_SOURCE}/{ZIX_VERSION}.tar.gz",
},
},
}
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-http2",
"language": "Zig",
"type": "engine",
"engine": "zix uring",
"description": "Zig HTTP/2 server on the zix.Http2 engine (no std.http). h2c runs under the .URING model: shared-nothing, each worker owns its SO_REUSEPORT listener, io_uring loop, and connections, multiplexing streams through a resumable frame state machine. h2 over TLS 1.3 (ALPN h2, baked self-signed Ed25519 cert) terminates in the per-core tls_mux. Serves baseline (sum the query), json (render dataset items), and static (chunked DATA frames from disk).",
"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