diff --git a/frameworks/vanilla-io_uring/Dockerfile b/frameworks/vanilla-io_uring/Dockerfile index 980002d7b..2e23ec42f 100644 --- a/frameworks/vanilla-io_uring/Dockerfile +++ b/frameworks/vanilla-io_uring/Dockerfile @@ -24,13 +24,16 @@ 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. +# vanilla main @b189036 (incl. enghitalo/vanilla#80 url_prefix mount + #81 +# core.queue_buf borrowed send for small assets): this entry +# now serves /static/* through the audited static_assets module — precompressed +# .br/.gz negotiation per Accept-Encoding + ETag/Vary, with small assets emitted via +# core.queue_buf borrowed send (the #75 mechanism, now folded into the module) so the +# preloaded bytes are sent directly, never copied through the per-connection write +# buffer. Replaces the hand-rolled identity-only map. Also includes #79 (kTLS, inert +# here: built without -d vanilla_tls). 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 WORKDIR /app COPY . . diff --git a/frameworks/vanilla-io_uring/main.v b/frameworks/vanilla-io_uring/main.v index 0ca0010b9..71275e7ce 100644 --- a/frameworks/vanilla-io_uring/main.v +++ b/frameworks/vanilla-io_uring/main.v @@ -2,6 +2,7 @@ module main import vanilla.http_server import vanilla.http_server.http1_1.request_parser +import vanilla.http_server.static_assets import db.pg import json import os @@ -47,17 +48,12 @@ struct Fortune { message string } -// A static asset preloaded into memory with its full HTTP response. -struct StaticFile { - response []u8 -} - struct Shared { mut: db &pg.DB = unsafe { nil } dataset []DatasetItem - prefixes []string // per item: `{…,"total":` (everything but the request-dependent total) - assets map[string]StaticFile // /static/ -> prebuilt response + prefixes []string // per item: `{…,"total":` (everything but the request-dependent total) + asv static_assets.AssetServer // /static/* via the audited module (negotiation + queue_buf borrowed send) cache map[int]string // crud cache-aside: id -> item JSON cache_mu &sync.RwMutex = unsafe { nil } // json-comp cache: the gzipped response for a given (count, m) is fully @@ -185,11 +181,14 @@ fn handle(req_buffer []u8, _fd int, mut out []u8, mut sh Shared) ! { } else if route == '/fortunes' { write_resp(mut out, 'text/html; charset=utf-8', sh.fortunes()) } else if route.starts_with('/static/') { - if f := sh.assets[route[8..]] { - out << f.response - } else { - out << not_found - } + // Canonical static serving via the lib's static_assets module, mounted at + // /static/: negotiates the precompressed .br/.gz sibling per Accept-Encoding + // (the arena sends `br;q=1`, so this ships the ~4x smaller .br body instead of + // the raw file) and emits small assets via core.queue_buf borrowed send — the + // worker sends the preloaded, immutable bytes DIRECTLY (no copy through the + // per-connection write buffer). ONE audited path shared with vanilla-epoll, + // replacing the hand-rolled identity-only map that ignored Accept-Encoding. + sh.asv.respond_into(req_buffer, mut out) or { out << not_found } } else if route == '/crud/items' { if method == 'POST' { sh.write_crud_create(mut out, req) @@ -668,20 +667,6 @@ fn accepts_gzip(req request_parser.HttpRequest) bool { } // content_type maps a file extension to a MIME type for the static handler. -fn content_type(name string) string { - ext := name.all_after_last('.') - return match ext { - 'css' { 'text/css' } - 'js' { 'application/javascript' } - 'json' { 'application/json' } - 'html' { 'text/html' } - 'svg' { 'image/svg+xml' } - 'webp' { 'image/webp' } - 'woff2' { 'font/woff2' } - else { 'application/octet-stream' } - } -} - // parse_db_url turns postgres://user:pass@host:port/dbname into a pg.Config. fn parse_db_url(u string) pg.Config { mut s := u @@ -733,25 +718,23 @@ fn main() { prefixes << enc#[..-1] + ',"total":' } - // Preload static assets into memory as ready-to-send responses (originals - // only; skip the precompressed .gz/.br siblings — we serve identity). - mut assets := map[string]StaticFile{} static_dir := os.getenv_opt('STATIC_DIR') or { '/data/static' } - for name in os.ls(static_dir) or { []string{} } { - if name.ends_with('.gz') || name.ends_with('.br') { - continue - } - bytes := os.read_bytes('${static_dir}/${name}') or { continue } - assets[name] = StaticFile{ - response: static_response(content_type(name), bytes) - } - } + // Canonical static server: loads every asset PLUS its .br/.gz siblings once, + // mounts them at /static/, and negotiates Accept-Encoding per request (serving + // the precompressed body when accepted, emitted via core.queue_buf borrowed + // send). Replaces the former identity-only map that ignored Accept-Encoding and + // always shipped the raw file. spa_fallback off: the arena set has no SPA. + asv := static_assets.new(static_assets.Config{ + root: static_dir + url_prefix: '/static/' + spa_fallback: '' + }) or { panic('vanilla-io_uring: static_assets init failed: ${err}') } mut sh := Shared{ db: db dataset: dataset prefixes: prefixes - assets: assets + asv: asv cache: map[int]string{} cache_mu: sync.new_rwmutex() gz_cache: map[u64][]u8{} @@ -772,13 +755,3 @@ fn main() { } // static_response prebuilds the full HTTP response for a static file. -fn static_response(ctype string, body []u8) []u8 { - mut sb := strings.new_builder(body.len + 96) - sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: ') - sb.write_string(ctype) - sb.write_string('\r\nContent-Length: ') - sb.write_decimal(i64(body.len)) - sb.write_string('\r\nConnection: keep-alive\r\n\r\n') - unsafe { sb.write_ptr(body.data, body.len) } - return sb -}