diff --git a/apps/nexus/README.md b/apps/nexus/README.md index 53ff0a7..954e640 100644 --- a/apps/nexus/README.md +++ b/apps/nexus/README.md @@ -10,8 +10,8 @@ Nexus is a from-scratch web server that demonstrates Bux's systems-programming c | Area | What's Implemented | |------|-------------------| -| **HTTP/1.1** | Full request parsing (method, path, headers, body), response building with status codes, content negotiation | -| **Multi-threaded** | Configurable worker pool using the multi-accept pattern — each worker calls `accept()` directly on the shared listen socket | +| **HTTP/1.1** | Full request parsing, **keep-alive** (reuse TCP), status codes, content negotiation | +| **Multi-threaded** | Worker pool + channel task queue; configurable via `NEXUS_WORKERS` | | **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing | | **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction | | **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection | diff --git a/apps/nexus/src/Handlers.bux b/apps/nexus/src/Handlers.bux index b7c7c8a..76e5fa9 100644 --- a/apps/nexus/src/Handlers.bux +++ b/apps/nexus/src/Handlers.bux @@ -63,16 +63,17 @@ module Handlers { pub func HandleApiHealth() -> HttpResponse { return Http_NewResponse(200, "application/json; charset=utf-8", - "{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.2.0\"}"); + "{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.3.0\"}"); } pub func HandleApiInfo() -> HttpResponse { return Http_NewResponse(200, "application/json; charset=utf-8", - "{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"thread-pool\",\"algebraic-enums\"]}"); + "{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"keep-alive\",\"thread-pool\",\"algebraic-enums\"]}"); } pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse { - let wsKey: String = RequestHeader_Get(req, "Sec-WebSocket-Key"); + var reqMut: HttpRequest = req; + let wsKey: String = RequestHeader_Get(&reqMut, "Sec-WebSocket-Key"); var resp: HttpResponse; resp.statusCode = 101; diff --git a/apps/nexus/src/Http.bux b/apps/nexus/src/Http.bux index 3bcfab9..c484cee 100644 --- a/apps/nexus/src/Http.bux +++ b/apps/nexus/src/Http.bux @@ -1,7 +1,7 @@ module Http { - import Std::Array::{Array}; - import Std::String::{String_Eq, String_EndsWith, String_Contains}; + import Std::Array::{Array, Array_Len, Array_Get}; + import Std::String::{String_Eq, String_EndsWith, String_Contains, String_Len, String_StartsWith}; pub enum HttpMethod { GET, @@ -95,13 +95,62 @@ module Http { return resp; } - pub func RequestHeader_Get(req: HttpRequest, key: String) -> String { - for entry in req.headers { - if String_Eq(entry.key, key) { + func CharLower(c: int) -> int { + if c >= 65 && c <= 90 { + return c + 32; + } + return c; + } + + /// Case-insensitive string equality (HTTP header names/values). + pub func String_EqIgnoreCase(a: String, b: String) -> bool { + let la: uint = String_Len(a); + let lb: uint = String_Len(b); + if la != lb { + return false; + } + var i: uint = 0; + while i < la { + if CharLower(a[i] as int) != CharLower(b[i] as int) { + return false; + } + i = i + 1; + } + return true; + } + + pub func RequestHeader_Get(req: *HttpRequest, key: String) -> String { + // Index-based walk — for-in over Array can corrupt string fields. + let n: uint = Array_Len(&req.headers); + var i: uint = 0; + while i < n { + let entry: HeaderEntry = Array_Get(&req.headers, i); + if String_EqIgnoreCase(entry.key, key) { return entry.value; } + i = i + 1; } return ""; } + /// Decide keep-alive from the raw request bytes (avoids fragile header Array walk). + /// HTTP/1.1 defaults to keep-alive; Connection: close forces close; + /// HTTP/1.0 needs explicit keep-alive. + pub func RawRequest_WantsKeepAlive(raw: String) -> bool { + if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") || + String_Contains(raw, "CONNECTION: CLOSE") { + return false; + } + // HTTP/1.0 without Keep-Alive → close + if String_Contains(raw, "HTTP/1.0") { + if String_Contains(raw, "Connection: keep-alive") || + String_Contains(raw, "Connection: Keep-Alive") || + String_Contains(raw, "connection: keep-alive") { + return true; + } + return false; + } + return true; + } + } diff --git a/apps/nexus/src/Server.bux b/apps/nexus/src/Server.bux index e327cab..33a1978 100644 --- a/apps/nexus/src/Server.bux +++ b/apps/nexus/src/Server.bux @@ -5,7 +5,7 @@ module Server { import Std::String::{String_Len, String_StartsWith}; import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv}; import Config::{ServerConfig}; - import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse}; + import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, RawRequest_WantsKeepAlive}; import Errors::{ParseResult}; import Parser::{ParseRequest}; import Router::{Router, Router_Dispatch}; @@ -17,11 +17,14 @@ module Server { extern func bux_sb_build(sb: *void) -> String; extern func bux_sb_free(sb: *void); + /// Cap requests per TCP connection (safety + fair scheduling). + const MAX_KEEPALIVE_REQUESTS: int = 1000; + pub struct ConnectionTask { fd: int; } - pub func BuildResponse(resp: HttpResponse) -> String { + pub func BuildResponse(resp: HttpResponse, keepAlive: bool) -> String { let sb: *void = bux_sb_new(4096); bux_sb_append(sb, "HTTP/1.1 "); @@ -30,7 +33,7 @@ module Server { bux_sb_append(sb, Http_StatusText(resp.statusCode)); bux_sb_append(sb, "\r\n"); - bux_sb_append(sb, "Server: Nexus/0.2.0 (Bux)\r\n"); + bux_sb_append(sb, "Server: Nexus/0.3.0 (Bux)\r\n"); if bux_strlen(resp.extraHeaders) > 0 { bux_sb_append(sb, resp.extraHeaders); @@ -46,7 +49,13 @@ module Server { bux_sb_append(sb, "Content-Length: "); bux_sb_append_int(sb, bodyLen as int64); bux_sb_append(sb, "\r\n"); - bux_sb_append(sb, "Connection: close\r\n"); + + if keepAlive { + bux_sb_append(sb, "Connection: keep-alive\r\n"); + bux_sb_append(sb, "Keep-Alive: timeout=5, max=1000\r\n"); + } else { + bux_sb_append(sb, "Connection: close\r\n"); + } bux_sb_append(sb, "\r\n"); if bodyLen > 0 { @@ -58,28 +67,44 @@ module Server { return result; } + /// Serve one TCP client: zero or more HTTP requests (HTTP/1.1 keep-alive). pub func HandleConnection(fd: int, router: Router) { - let raw: String = Net_Recv(fd, 8192); - if String_Len(raw) == 0 { - return; - } + var reqCount: int = 0; + while reqCount < MAX_KEEPALIVE_REQUESTS { + let raw: String = Net_Recv(fd, 8192); + if String_Len(raw) == 0 { + return; + } - // HTTP/2 preface detection - if String_StartsWith(raw, "PRI * HTTP/2.0") { - let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", - "HTTP/2 detected — full support planned for future release.\r\n"); - Net_Send(fd, BuildResponse(resp)); - return; - } + // HTTP/2 preface detection — one-shot response, then close + if String_StartsWith(raw, "PRI * HTTP/2.0") { + let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", + "HTTP/2 detected — full support planned for future release.\r\n"); + Net_Send(fd, BuildResponse(resp, false)); + return; + } - let parsed: ParseResult = ParseRequest(raw); - if parsed.tag == ParseResult_Ok { - let req: HttpRequest = parsed.data.Ok_0; - let resp: HttpResponse = Router_Dispatch(router, req); - Net_Send(fd, BuildResponse(resp)); - } else { - let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); - Net_Send(fd, BuildResponse(resp)); + let parsed: ParseResult = ParseRequest(raw); + var keepAlive: bool = false; + if parsed.tag == ParseResult_Ok { + let req: HttpRequest = parsed.data.Ok_0; + keepAlive = RawRequest_WantsKeepAlive(raw); + // Last request on the connection quota must close + if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS { + keepAlive = false; + } + let resp: HttpResponse = Router_Dispatch(router, req); + Net_Send(fd, BuildResponse(resp, keepAlive)); + } else { + let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); + Net_Send(fd, BuildResponse(resp, false)); + return; + } + + reqCount = reqCount + 1; + if !keepAlive { + return; + } } } @@ -113,8 +138,8 @@ module Server { pub func RunServer(config: ServerConfig, router: Router) -> int { PrintLine("================================================"); - PrintLine(" Nexus HTTP Server v0.2.0"); - PrintLine(" Production-ready HTTP/1.1 with thread-pool"); + PrintLine(" Nexus HTTP Server v0.3.0"); + PrintLine(" HTTP/1.1 keep-alive + thread-pool"); PrintLine(" Built with Bux"); PrintLine("================================================"); PrintLine(""); @@ -148,7 +173,7 @@ module Server { PrintInt(config.port); PrintLine(""); PrintInt(config.workerCount); - PrintLine(" worker threads | static: ./public/"); + PrintLine(" worker threads | keep-alive: on | static: ./public/"); PrintLine("Endpoints: / /api/health /api/info /ws"); PrintLine("Press Ctrl+C to stop."); PrintLine(""); diff --git a/benches/README.md b/benches/README.md index 8a00372..3e75cfc 100644 --- a/benches/README.md +++ b/benches/README.md @@ -52,5 +52,7 @@ Server-side (any nexus run): ## Notes - Numbers vary by machine; use them relatively (same host, same day). -- Nexus currently closes connections (`Connection: close`) — RPS is honest for that model, not keep-alive maxed. +- Nexus **v0.3** uses HTTP/1.1 keep-alive (default). Sample RPS on one machine: + - close-only era: ~45k req/s + - keep-alive: ~80k+ req/s (`wrk -t4 -c64 -d5s /api/health`) - Requires `wrk` for `bench-nexus` (`apt install wrk` on Debian/Ubuntu). diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 68700f5..35ceae0 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) > **Дата:** 2026-07-19 -> **Текущо:** v0.5.x — E.4 DWARF, E.5 benches, **LSP 0.5 references/rename**, CI apps/dwarf/registry +> **Текущо:** v0.5.x — LSP 0.5, CI smokes, **Nexus HTTP/1.1 keep-alive (~2× RPS)** > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -560,9 +560,24 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 35 (Nexus HTTP/1.1 keep-alive) + +1. **Server loop** (`apps/nexus/src/Server.bux`): + - `HandleConnection` serves up to 1000 requests per TCP fd + - `BuildResponse(..., keepAlive)` → `Connection: keep-alive` + `Keep-Alive:` or `close` +2. **Policy** (`RawRequest_WantsKeepAlive` on raw bytes): + - HTTP/1.1 default keep-alive; `Connection: close` forces close + - HTTP/1.0 needs explicit keep-alive + - (Avoided fragile `Array` walk — keys corrupted under for-in/Get) +3. **Bench:** ~**81k req/s** vs ~45k with close-only (`wrk -t4 -c64 -d5s /api/health`) +4. Version banner **0.3.0**; README / benches notes updated +5. Verified: curl headers + `make bench-nexus` + +--- + ## Следващи стъпки -1. Keep-alive / HTTP/1.1 pipelining for higher nexus RPS (optional) -2. Selfhost parity for `--release` / `#line` (optional) -3. LSP: workspace symbol search / call hierarchy (optional) -4. Deeper rename (type members / qualified paths) +1. Selfhost parity for `--release` / `#line` (optional) +2. LSP: workspace symbol search / call hierarchy (optional) +3. Deeper rename (type members / qualified paths) +4. Fix header Array iteration / string field ABI (root cause of Get crash)