Skip to content
Closed
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
53 changes: 43 additions & 10 deletions frameworks/vanilla-io_uring/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ FROM debian:stable-slim AS build

RUN apt-get -qq update && \
apt-get -qy install --no-install-recommends \
ca-certificates build-essential git libpq-dev liburing-dev && \
ca-certificates build-essential git libpq-dev liburing-dev \
cmake curl bzip2 python3 && \
rm -rf /var/lib/apt/lists/*

# Pinned, reproducible V at master commit 02015a7c (built from source). Master is
Expand All @@ -24,25 +25,57 @@ RUN git clone https://github.com/vlang/v /opt/v && \
# main moved, but that hit api.github.com every build and a transient 504/rate-limit
# failed the whole build (MDA2AV/HttpArena#895). Bump this commit to pick up lib fixes.
#
# vanilla main @15bd57e picks up the allocation fixes from a lib-wide audit:
# enghitalo/vanilla#72 (pg_async: read wire ints at offset, drop per-read slices),
# #73 (static_assets: zero-copy conditional-GET/Range parsing) and #74 (TLS: pool the
# per-connection read/response buffers). pg_async#72 is the one that touches this
# backend's async-DB row-decode hot path.
# Pinned to vanilla main @b189036 — the lib-wide alloc audit (#72 pg_async, #73
# static_assets, #74 TLS buffer pooling) PLUS kTLS record offload (enghitalo/vanilla#79,
# ancestor 5137a9a). The io_uring backend has no TLS, so the json-tls listener runs on
# the lib's epoll backend on :8081 (see main.v); kTLS makes its steady-state read/write
# plain recv/send with the kernel doing AES-128-GCM. pg_async#72 touches this backend's
# async-DB row-decode hot path.
RUN git clone https://github.com/enghitalo/vanilla /root/.vmodules/vanilla && \
git -C /root/.vmodules/vanilla checkout 15bd57e5ae8cf1383bd386826e48e08a10f6d4b4
git -C /root/.vmodules/vanilla checkout b189036212e4283ef2cffe42b318b556f8a3d1bc

# Mbed TLS 4 for the json-tls profile (the epoll TLS listener on :8081). The vanilla
# `tls` C shim (built with -d vanilla_tls) targets the Mbed TLS 4.x API; Debian apt
# ships only 2.28 (ABI-incompatible), so build 4.x from the version-pinned release
# tarball into /usr/local. THREAD-SAFETY: enable MBEDTLS_THREADING_C +
# MBEDTLS_THREADING_PTHREAD — the TLS workers share Mbed TLS's global PSA key store,
# which races (heap-use-after-free) under concurrent handshakes without the mutex.
ARG MBEDTLS_VERSION=4.1.0
RUN curl -fsSL -o /tmp/mbedtls.tar.bz2 \
"https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MBEDTLS_VERSION}/mbedtls-${MBEDTLS_VERSION}.tar.bz2" && \
tar -xf /tmp/mbedtls.tar.bz2 -C /tmp && \
cd "/tmp/mbedtls-${MBEDTLS_VERSION}" && \
python3 tf-psa-crypto/scripts/config.py set MBEDTLS_THREADING_C && \
python3 tf-psa-crypto/scripts/config.py set MBEDTLS_THREADING_PTHREAD && \
cmake -S "/tmp/mbedtls-${MBEDTLS_VERSION}" -B /tmp/mbedtls-build \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_SHARED_MBEDTLS_LIBRARY=On -DUSE_STATIC_MBEDTLS_LIBRARY=Off \
-DENABLE_TESTING=Off -DENABLE_PROGRAMS=Off \
-DCMAKE_INSTALL_PREFIX=/usr/local && \
cmake --build /tmp/mbedtls-build -j"$(nproc)" && \
cmake --install /tmp/mbedtls-build && ldconfig && \
rm -rf /tmp/mbedtls.tar.bz2 "/tmp/mbedtls-${MBEDTLS_VERSION}" /tmp/mbedtls-build

WORKDIR /app
COPY . .
# Default GC (NOT -gc none, NOT -prealloc): this runs on the vanilla HTTP server's own
# manual buffer management, not the stdlib veb/fasthttp per-request arena.
RUN v -prod . -o server
# manual buffer management. -d vanilla_tls compiles the real Mbed TLS backend for the
# json-tls listener (without it the tls module is a stub and mbedtls isn't linked).
RUN v -prod -d vanilla_tls . -o server

FROM debian:stable-slim
RUN apt-get -qq update && \
apt-get -qy install --no-install-recommends libpq5 liburing2 && \
rm -rf /var/lib/apt/lists/*
# Mbed TLS 4 shared libs for the json-tls (epoll) listener (libmbedcrypto pulls in
# libtfpsacrypto transitively). ldconfig so the loader resolves them with no
# LD_LIBRARY_PATH.
COPY --from=build /usr/local/lib/libmbedcrypto.so* /usr/local/lib/libmbedx509.so* /usr/local/lib/libmbedtls.so* /usr/local/lib/libtfpsacrypto.so* /usr/local/lib/
RUN ldconfig
COPY --from=build /app/server /server

EXPOSE 8080
# 8080 = io_uring HTTP (non-TLS profiles); 8081 = json-tls (HTTP/1.1 over TLS, epoll
# backend). The harness bind-mounts the cert/key at /certs (override via
# TLS_CERT/TLS_KEY); with no cert mounted the server self-signs.
EXPOSE 8080 8081
CMD ["/server"]
77 changes: 77 additions & 0 deletions frameworks/vanilla-io_uring/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module main

import vanilla.http_server
import vanilla.http_server.http1_1.request_parser
import vanilla.http_server.tls
import db.pg
import json
import os
Expand Down Expand Up @@ -704,6 +705,29 @@ fn parse_db_url(u string) pg.Config {
}
}

// load_tls_config builds the json-tls server's TLS config. It reads the cert/key
// the HttpArena harness bind-mounts at /certs (overridable via TLS_CERT/TLS_KEY).
// If NO cert is mounted (local dev), it falls back to a fresh self-signed cert —
// the benchmark/validate clients use `curl -k` / wrk, which never verify it. If a
// cert IS present but the key is missing/unreadable, it FAILS LOUDLY rather than
// silently self-signing. TLS 1.3 + ALPN http/1.1 are fixed by the tls shim.
fn load_tls_config() &tls.Config {
cert_path := os.getenv_opt('TLS_CERT') or { '/certs/server.crt' }
key_path := os.getenv_opt('TLS_KEY') or { '/certs/server.key' }
cert := os.read_bytes(cert_path) or {
eprintln('vanilla-io_uring: no TLS cert at ${cert_path} (${err}); using ephemeral self-signed')
return tls.new_self_signed() or {
panic('vanilla-io_uring: self-signed TLS bring-up failed: ${err}')
}
}
key := os.read_bytes(key_path) or {
panic('vanilla-io_uring: TLS cert present at ${cert_path} but key unreadable at ${key_path}: ${err}')
}
return tls.new_from_pem(cert, key) or {
panic('vanilla-io_uring: TLS cert/key parse failed: ${err}')
}
}

fn main() {
url := os.getenv_opt('DATABASE_URL') or { 'postgres://bench:bench@localhost:5432/benchmark' }
mut size := (os.getenv_opt('DATABASE_MAX_CONN') or { '64' }).int()
Expand Down Expand Up @@ -758,6 +782,59 @@ fn main() {
gz_mu: sync.new_rwmutex()
}

// ── json-tls profile: /json over HTTPS on :8081 via the epoll + kTLS backend ──
// The lib's io_uring backend has no TLS, so the json-tls listener runs on the
// epoll backend (TLS 1.3 via Mbed TLS; after the handshake the kernel does record
// AES-128-GCM via kTLS where the `tls` module is present, else userspace fallback).
// It serves ONLY /json (404 elsewhere) — minimal TLS surface — reusing the same
// allocation-free write_json_response (read-only: dataset + prefixes). A STATELESS
// request_handler captures `sh`; it never touches the DB/caches, so it runs safely
// alongside the io_uring workers. The io_uring server below keeps the non-TLS
// profiles on :8080.
tls_handler := fn [sh] (req_buffer []u8, fd int, mut out []u8) ! {
mut req := request_parser.HttpRequest{
buffer: req_buffer
}
if !request_parser.decode_into(mut req) {
wb(mut out, bad_request)
return
}
target := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
qpos := target.index_u8(`?`)
route := if qpos < 0 { target } else { unsafe { tos(target.str, qpos) } }
if route.starts_with('/json/') {
count := clamp_count(parse_u_at(route, 6), sh.dataset.len)
mut m := qint(req, qk_m)
if m == 0 {
m = 1
}
sh.write_json_response(mut out, count, m)
return
}
wb(mut out, not_found)
}
// Port is fixed to 8081 by the HttpArena harness; TLS_PORT lets local runs pick a
// free port. run() blocks, so the TLS server runs on its own thread (value-mut
// receiver → spawn via a closure with a local mut copy; the two servers are
// independent — own socket, workers and backend).
mut tls_port := (os.getenv_opt('TLS_PORT') or { '8081' }).int()
if tls_port <= 0 {
tls_port = 8081
}
tls_server := http_server.new_server(http_server.ServerConfig{
port: tls_port
io_multiplexing: .epoll
limits: http_server.Limits{
max_request_bytes: 64 * 1024
}
request_handler: tls_handler
tls_config: load_tls_config()
})!
spawn fn [tls_server] () {
mut s := tls_server
s.run()
}()

mut server := http_server.new_server(http_server.ServerConfig{
port: 8080
io_multiplexing: .io_uring
Expand Down
3 changes: 2 additions & 1 deletion frameworks/vanilla-io_uring/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"language": "V",
"type": "engine",
"engine": "io_uring",
"description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg pooled Go-style DB (db.exec_param_many over a connection pool). Built with the default GC on pinned V master c0624b274 (built from source). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.",
"description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; json-tls serves /json over HTTP/1.1 + TLS 1.3 on :8081 via the lib's epoll backend (the io_uring backend has no TLS) with kTLS record offload where the kernel `tls` module is present; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg pooled Go-style DB (db.exec_param_many over a connection pool). Built with the default GC on pinned V master c0624b274 (built from source). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.",
"repo": "https://github.com/enghitalo/vanilla",
"enabled": true,
"tests": [
Expand All @@ -12,6 +12,7 @@
"limited-conn",
"json",
"json-comp",
"json-tls",
"upload",
"static",
"async-db",
Expand Down