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
63 changes: 55 additions & 8 deletions frameworks/vanilla-epoll/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). Moved off
Expand All @@ -22,22 +23,68 @@ RUN git clone https://github.com/vlang/v /opt/v && \
# (or rate-limit) failed the whole build (MDA2AV/HttpArena#895). To pick up upstream
# library fixes, bump this commit.
#
# 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).
# Pinned to vanilla main @5137a9a — the lib-wide alloc audit (#72 pg_async, #73
# static_assets, #74 TLS buffer pooling) PLUS kTLS record offload for json-tls
# (merged via enghitalo/vanilla#79): after the Mbed TLS handshake the kernel does
# TLS 1.3 record AES-128-GCM (setsockopt TLS_TX+TLS_RX), so the epoll worker's
# steady-state read/write are plain recv/send — no per-record userspace crypto and
# no PSA key-store mutex on the hot path. Falls back to userspace TLS where the
# `tls` kernel module is absent.
RUN git clone https://github.com/enghitalo/vanilla /root/.vmodules/vanilla && \
git -C /root/.vmodules/vanilla checkout 15bd57e5ae8cf1383bd386826e48e08a10f6d4b4
git -C /root/.vmodules/vanilla checkout 5137a9a9276c85325b28302f5a799cb3da46efe7

# Mbed TLS 4 for the json-tls profile. The vanilla `tls` module's C shim
# (vanilla_tls.c, built with `-d vanilla_tls`) targets the Mbed TLS 4.x API
# (PSA, TLS 1.3); Debian apt only ships 2.28, which is ABI/API-incompatible, so
# build 4.x from the version-pinned release tarball (self-contained — needs no
# Python framework) as SHARED libs into /usr/local, matching the #flag in
# tls_mbedtls_d_vanilla_tls.c.v (-L/usr/local/lib -lmbedtls -lmbedx509
# -lmbedcrypto). 4.x also produces libtfpsacrypto (the TF-PSA-Crypto split),
# linked transitively. Headers land in /usr/local/include for the build.
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}" && \
# THREAD-SAFETY (load-bearing): vanilla runs N TLS worker threads, each driving
# TLS 1.3 handshakes through Mbed TLS's PSA crypto, whose key store is a GLOBAL,
# process-wide table. Without MBEDTLS_THREADING_C the concurrent handshakes race
# on the key slots — a heap-use-after-free under load (confirmed with ASan:
# psa_wipe_key_slot on one thread frees a slot another thread is mid-memcpy in
# psa_hmac_setup; crashes ~c1024 with "double free or corruption"). THREADING_C +
# THREADING_PTHREAD guard the key store with a pthread mutex. In 4.x the crypto
# (and so its config) lives in the tf-psa-crypto submodule.
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 . .
RUN v -prod -gc none . -o server
# -d vanilla_tls compiles the real Mbed TLS backend (the json-tls listener on
# :8081); without it the tls module is a stub and the binary stays mbedtls-free.
RUN v -prod -gc none -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 endpoint (libmbedcrypto pulls in
# libtfpsacrypto transitively). ldconfig so the loader resolves them with no
# LD_LIBRARY_PATH. Plain-HTTP builds don't link mbedtls, but this image always
# enables -d vanilla_tls, so the runtime needs them.
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 = plaintext HTTP; 8081 = json-tls (HTTP/1.1 over TLS). The harness
# bind-mounts the cert/key at /certs and the server reads /certs/server.{crt,key}
# (override via TLS_CERT/TLS_KEY); with no cert mounted it self-signs.
EXPOSE 8080 8081
CMD ["/server"]
101 changes: 96 additions & 5 deletions frameworks/vanilla-epoll/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module main
import vanilla.http_server
import vanilla.http_server.http1_1.request_parser
import vanilla.http_server.core
import vanilla.http_server.tls
import vanilla.pg_async
import json
import os
Expand Down Expand Up @@ -848,30 +849,40 @@ fn (mut w WorkerCtx) render_crud_update(mut out []u8, id int) {

// ── /json (non-DB) ───────────────────────────────────────────────────────────

fn (w &WorkerCtx) write_json_response(mut out []u8, count int, m i64) {
// write_json_into is the transport-agnostic /json serializer: it only APPENDS
// response bytes to `out`, so the plaintext path (via WorkerCtx) and the json-tls
// path (a stateless TLS handler) share it verbatim. `ro` is read-only and nothing
// per-request is heap-allocated. Content-Length is precomputed from the SAME
// values the body emits, so the framed length can never desync from the body
// (no response-splitting / smuggling surface).
fn write_json_into(ro &SharedRO, mut out []u8, count int, m i64) {
// 21 = len('{"items":[') + len('],"count":') + '}'; plus the count's own digits
mut clen := 21 + digits(i64(count))
if count > 0 {
clen += count - 1
}
for i in 0 .. count {
t := w.ro.dataset[i].price * w.ro.dataset[i].quantity * m
clen += w.ro.prefixes[i].len + digits(t) + 1
t := ro.dataset[i].price * ro.dataset[i].quantity * m
clen += ro.prefixes[i].len + digits(t) + 1
}
ws(mut out,
'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: application/json\r\nContent-Length: ')
wi(mut out, i64(clen))
ws(mut out, '\r\nConnection: keep-alive\r\n\r\n{"items":[')
for i in 0 .. count {
ws(mut out, w.ro.prefixes[i])
wi(mut out, w.ro.dataset[i].price * w.ro.dataset[i].quantity * m)
ws(mut out, ro.prefixes[i])
wi(mut out, ro.dataset[i].price * ro.dataset[i].quantity * m)
ws(mut out, if i < count - 1 { '},' } else { '}' })
}
ws(mut out, '],"count":')
wi(mut out, i64(count))
ws(mut out, '}')
}

fn (w &WorkerCtx) write_json_response(mut out []u8, count int, m i64) {
write_json_into(w.ro, mut out, count, m)
}

fn (mut w WorkerCtx) write_json_gzip(mut out []u8, count int, m i64) {
key := (u64(u32(count)) << 32) | u64(u32(m))
mut cached := []u8{}
Expand Down Expand Up @@ -1281,6 +1292,30 @@ fn parse_db_url(u string) pg_async.ConnConfig {
}
}

// 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, so a real misconfiguration can't slip through as "works
// but with the wrong identity". 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-epoll: no TLS cert at ${cert_path} (${err}); using ephemeral self-signed')
return tls.new_self_signed() or {
panic('vanilla-epoll: self-signed TLS bring-up failed: ${err}')
}
}
key := os.read_bytes(key_path) or {
panic('vanilla-epoll: TLS cert present at ${cert_path} but key unreadable at ${key_path}: ${err}')
}
return tls.new_from_pem(cert, key) or {
panic('vanilla-epoll: TLS cert/key parse failed: ${err}')
}
}

fn main() {
url := os.getenv_opt('DATABASE_URL') or { 'postgres://bench:bench@localhost:5432/benchmark' }
cfg := parse_db_url(url)
Expand Down Expand Up @@ -1343,6 +1378,62 @@ fn main() {
gz_mu: sync.new_rwmutex()
}

// ── json-tls profile: the same /json handler over HTTPS on :8081 ───────────
// A SECOND server because tls_config is server-wide. It serves ONLY /json
// (404 for everything else) so the TLS port exposes the minimal surface the
// profile needs — no /static, /upload, /crud or DB routes. The handler is a
// STATELESS request_handler (no make_state, sidestepping the TLS worker's
// stateful path) capturing the read-only `ro`; it reuses write_json_into
// verbatim, so the bytes are identical to the plaintext /json. Mbed TLS 1.3,
// ALPN http/1.1 (set by the tls shim) → curl --http1.1 negotiates 1.1.
tls_handler := fn [ro] (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), ro.dataset.len)
mut m := qint(req, qk_m)
if m == 0 {
m = 1
}
write_json_into(ro, 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 (the harness injects nothing, so the default is the contract).
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{
// json-tls requests are tiny GETs; a small ceiling bounds per-conn
// memory and shrinks the DoS surface (the TLS port has no upload path).
max_request_bytes: 64 * 1024
}
request_handler: tls_handler
tls_config: load_tls_config()
})!
// run() blocks in the accept loop, so the TLS server runs on its own thread
// while the plaintext server.run() below blocks main. run() has a value-mut
// receiver, so spawn it via a closure with a local mut copy (each Server is
// independent — own socket, workers and counters).
spawn fn [tls_server] () {
mut s := tls_server
s.run()
}()

mut server := http_server.new_server(http_server.ServerConfig{
port: 8080
io_multiplexing: .epoll
Expand Down
3 changes: 2 additions & 1 deletion frameworks/vanilla-epoll/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"language": "V",
"type": "engine",
"engine": "epoll",
"description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking epoll 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 ConnectionPool. Pinned V 0.5.1 (prebuilt release). 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 epoll 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 terminates TLS 1.3 (Mbed TLS 4) on :8081 with ALPN http/1.1, reusing the same allocation-free /json serializer; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg ConnectionPool. Pinned V 0.5.1 (prebuilt release). 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
24 changes: 12 additions & 12 deletions site/data/api-16-1024.json
Original file line number Diff line number Diff line change
Expand Up @@ -1481,28 +1481,28 @@
{
"framework": "vanilla-epoll",
"language": "V",
"rps": 243035,
"avg_latency": "1.75ms",
"p99_latency": "14.80ms",
"cpu": "1379.2%",
"memory": "165MiB",
"rps": 245137,
"avg_latency": "1.69ms",
"p99_latency": "14.30ms",
"cpu": "1406.9%",
"memory": "180MiB",
"connections": 1024,
"threads": 64,
"duration": "5s",
"pipeline": 1,
"bandwidth": "1.20GB/s",
"input_bw": "13.67MB/s",
"reconnects": 728796,
"status_2xx": 3645532,
"bandwidth": "1.21GB/s",
"input_bw": "13.79MB/s",
"reconnects": 735222,
"status_2xx": 3677055,
"status_3xx": 0,
"status_4xx": 0,
"status_5xx": 0,
"tpl_baseline": 1367533,
"tpl_json": 1367615,
"tpl_baseline": 1380046,
"tpl_json": 1378497,
"tpl_db": 0,
"tpl_upload": 0,
"tpl_static": 0,
"tpl_async_db": 910375
"tpl_async_db": 918508
},
{
"framework": "vanilla-io_uring",
Expand Down
24 changes: 12 additions & 12 deletions site/data/api-4-256.json
Original file line number Diff line number Diff line change
Expand Up @@ -1481,28 +1481,28 @@
{
"framework": "vanilla-epoll",
"language": "V",
"rps": 65503,
"avg_latency": "1.88ms",
"p99_latency": "13.70ms",
"cpu": "360.2%",
"memory": "100MiB",
"rps": 68078,
"avg_latency": "1.94ms",
"p99_latency": "13.90ms",
"cpu": "374.1%",
"memory": "114MiB",
"connections": 256,
"threads": 64,
"duration": "5s",
"pipeline": 1,
"bandwidth": "330.72MB/s",
"input_bw": "3.69MB/s",
"reconnects": 196427,
"status_2xx": 982555,
"bandwidth": "343.61MB/s",
"input_bw": "3.83MB/s",
"reconnects": 204208,
"status_2xx": 1021176,
"status_3xx": 0,
"status_4xx": 0,
"status_5xx": 0,
"tpl_baseline": 368543,
"tpl_json": 368583,
"tpl_baseline": 383232,
"tpl_json": 382788,
"tpl_db": 0,
"tpl_upload": 0,
"tpl_static": 0,
"tpl_async_db": 245429
"tpl_async_db": 255155
},
{
"framework": "vanilla-io_uring",
Expand Down
18 changes: 9 additions & 9 deletions site/data/async-db-1024.json
Original file line number Diff line number Diff line change
Expand Up @@ -1239,19 +1239,19 @@
{
"framework": "vanilla-epoll",
"language": "V",
"rps": 282557,
"avg_latency": "859us",
"p99_latency": "7.12ms",
"cpu": "3034.2%",
"memory": "142MiB",
"rps": 291066,
"avg_latency": "876us",
"p99_latency": "7.44ms",
"cpu": "3114.4%",
"memory": "151MiB",
"connections": 1024,
"threads": 64,
"duration": "5s",
"pipeline": 1,
"bandwidth": "1.06GB/s",
"input_bw": "18.86MB/s",
"reconnects": 113136,
"status_2xx": 2825573,
"bandwidth": "1.10GB/s",
"input_bw": "19.43MB/s",
"reconnects": 116639,
"status_2xx": 2910666,
"status_3xx": 0,
"status_4xx": 0,
"status_5xx": 0
Expand Down
16 changes: 8 additions & 8 deletions site/data/baseline-4096.json
Original file line number Diff line number Diff line change
Expand Up @@ -1731,19 +1731,19 @@
{
"framework": "vanilla-epoll",
"language": "V",
"rps": 4033627,
"avg_latency": "1.02ms",
"p99_latency": "6.57ms",
"cpu": "6307.1%",
"memory": "152MiB",
"rps": 4063080,
"avg_latency": "1.01ms",
"p99_latency": "6.55ms",
"cpu": "6302.1%",
"memory": "154MiB",
"connections": 4096,
"threads": 64,
"duration": "5s",
"pipeline": 1,
"bandwidth": "411.47MB/s",
"input_bw": "311.59MB/s",
"bandwidth": "414.49MB/s",
"input_bw": "313.86MB/s",
"reconnects": 0,
"status_2xx": 20168136,
"status_2xx": 20315403,
"status_3xx": 0,
"status_4xx": 0,
"status_5xx": 0
Expand Down
Loading