feat(nexus): HTTP/1.1 keep-alive (~2× throughput)
Reuse TCP connections for multiple requests, honor Connection close, and raise wrk health RPS from ~45k to ~80k+ on the same machine. - Loop HandleConnection up to 1000 reqs/fd with Connection keep-alive - RawRequest_WantsKeepAlive (HTTP/1.1 default; HTTP/1.0 needs keep-alive) - Nexus 0.3.0 banner; benches note before/after RPS
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
+54
-5
@@ -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<struct{String}> can corrupt string fields.
|
||||
let n: uint = Array_Len<HeaderEntry>(&req.headers);
|
||||
var i: uint = 0;
|
||||
while i < n {
|
||||
let entry: HeaderEntry = Array_Get<HeaderEntry>(&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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-26
@@ -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("");
|
||||
|
||||
+3
-1
@@ -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).
|
||||
|
||||
+20
-5
@@ -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<HeaderEntry>` 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)
|
||||
|
||||
Reference in New Issue
Block a user