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 |
|
| Area | What's Implemented |
|
||||||
|------|-------------------|
|
|------|-------------------|
|
||||||
| **HTTP/1.1** | Full request parsing (method, path, headers, body), response building with status codes, content negotiation |
|
| **HTTP/1.1** | Full request parsing, **keep-alive** (reuse TCP), status codes, content negotiation |
|
||||||
| **Multi-threaded** | Configurable worker pool using the multi-accept pattern — each worker calls `accept()` directly on the shared listen socket |
|
| **Multi-threaded** | Worker pool + channel task queue; configurable via `NEXUS_WORKERS` |
|
||||||
| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing |
|
| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing |
|
||||||
| **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction |
|
| **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 |
|
| **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 {
|
pub func HandleApiHealth() -> HttpResponse {
|
||||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
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 {
|
pub func HandleApiInfo() -> HttpResponse {
|
||||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
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 {
|
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;
|
var resp: HttpResponse;
|
||||||
resp.statusCode = 101;
|
resp.statusCode = 101;
|
||||||
|
|||||||
+54
-5
@@ -1,7 +1,7 @@
|
|||||||
module Http {
|
module Http {
|
||||||
|
|
||||||
import Std::Array::{Array};
|
import Std::Array::{Array, Array_Len, Array_Get};
|
||||||
import Std::String::{String_Eq, String_EndsWith, String_Contains};
|
import Std::String::{String_Eq, String_EndsWith, String_Contains, String_Len, String_StartsWith};
|
||||||
|
|
||||||
pub enum HttpMethod {
|
pub enum HttpMethod {
|
||||||
GET,
|
GET,
|
||||||
@@ -95,13 +95,62 @@ module Http {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub func RequestHeader_Get(req: HttpRequest, key: String) -> String {
|
func CharLower(c: int) -> int {
|
||||||
for entry in req.headers {
|
if c >= 65 && c <= 90 {
|
||||||
if String_Eq(entry.key, key) {
|
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;
|
return entry.value;
|
||||||
}
|
}
|
||||||
|
i = i + 1;
|
||||||
}
|
}
|
||||||
return "";
|
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::String::{String_Len, String_StartsWith};
|
||||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||||
import Config::{ServerConfig};
|
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 Errors::{ParseResult};
|
||||||
import Parser::{ParseRequest};
|
import Parser::{ParseRequest};
|
||||||
import Router::{Router, Router_Dispatch};
|
import Router::{Router, Router_Dispatch};
|
||||||
@@ -17,11 +17,14 @@ module Server {
|
|||||||
extern func bux_sb_build(sb: *void) -> String;
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
extern func bux_sb_free(sb: *void);
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
/// Cap requests per TCP connection (safety + fair scheduling).
|
||||||
|
const MAX_KEEPALIVE_REQUESTS: int = 1000;
|
||||||
|
|
||||||
pub struct ConnectionTask {
|
pub struct ConnectionTask {
|
||||||
fd: int;
|
fd: int;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub func BuildResponse(resp: HttpResponse) -> String {
|
pub func BuildResponse(resp: HttpResponse, keepAlive: bool) -> String {
|
||||||
let sb: *void = bux_sb_new(4096);
|
let sb: *void = bux_sb_new(4096);
|
||||||
|
|
||||||
bux_sb_append(sb, "HTTP/1.1 ");
|
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, Http_StatusText(resp.statusCode));
|
||||||
bux_sb_append(sb, "\r\n");
|
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 {
|
if bux_strlen(resp.extraHeaders) > 0 {
|
||||||
bux_sb_append(sb, resp.extraHeaders);
|
bux_sb_append(sb, resp.extraHeaders);
|
||||||
@@ -46,7 +49,13 @@ module Server {
|
|||||||
bux_sb_append(sb, "Content-Length: ");
|
bux_sb_append(sb, "Content-Length: ");
|
||||||
bux_sb_append_int(sb, bodyLen as int64);
|
bux_sb_append_int(sb, bodyLen as int64);
|
||||||
bux_sb_append(sb, "\r\n");
|
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");
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
if bodyLen > 0 {
|
if bodyLen > 0 {
|
||||||
@@ -58,28 +67,44 @@ module Server {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serve one TCP client: zero or more HTTP requests (HTTP/1.1 keep-alive).
|
||||||
pub func HandleConnection(fd: int, router: Router) {
|
pub func HandleConnection(fd: int, router: Router) {
|
||||||
let raw: String = Net_Recv(fd, 8192);
|
var reqCount: int = 0;
|
||||||
if String_Len(raw) == 0 {
|
while reqCount < MAX_KEEPALIVE_REQUESTS {
|
||||||
return;
|
let raw: String = Net_Recv(fd, 8192);
|
||||||
}
|
if String_Len(raw) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP/2 preface detection
|
// HTTP/2 preface detection — one-shot response, then close
|
||||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||||
"HTTP/2 detected — full support planned for future release.\r\n");
|
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||||
Net_Send(fd, BuildResponse(resp));
|
Net_Send(fd, BuildResponse(resp, false));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: ParseResult = ParseRequest(raw);
|
let parsed: ParseResult = ParseRequest(raw);
|
||||||
if parsed.tag == ParseResult_Ok {
|
var keepAlive: bool = false;
|
||||||
let req: HttpRequest = parsed.data.Ok_0;
|
if parsed.tag == ParseResult_Ok {
|
||||||
let resp: HttpResponse = Router_Dispatch(router, req);
|
let req: HttpRequest = parsed.data.Ok_0;
|
||||||
Net_Send(fd, BuildResponse(resp));
|
keepAlive = RawRequest_WantsKeepAlive(raw);
|
||||||
} else {
|
// Last request on the connection quota must close
|
||||||
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS {
|
||||||
Net_Send(fd, BuildResponse(resp));
|
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 {
|
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||||
PrintLine("================================================");
|
PrintLine("================================================");
|
||||||
PrintLine(" Nexus HTTP Server v0.2.0");
|
PrintLine(" Nexus HTTP Server v0.3.0");
|
||||||
PrintLine(" Production-ready HTTP/1.1 with thread-pool");
|
PrintLine(" HTTP/1.1 keep-alive + thread-pool");
|
||||||
PrintLine(" Built with Bux");
|
PrintLine(" Built with Bux");
|
||||||
PrintLine("================================================");
|
PrintLine("================================================");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
@@ -148,7 +173,7 @@ module Server {
|
|||||||
PrintInt(config.port);
|
PrintInt(config.port);
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintInt(config.workerCount);
|
PrintInt(config.workerCount);
|
||||||
PrintLine(" worker threads | static: ./public/");
|
PrintLine(" worker threads | keep-alive: on | static: ./public/");
|
||||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||||
PrintLine("Press Ctrl+C to stop.");
|
PrintLine("Press Ctrl+C to stop.");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
|
|||||||
+3
-1
@@ -52,5 +52,7 @@ Server-side (any nexus run):
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Numbers vary by machine; use them relatively (same host, same day).
|
- 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).
|
- Requires `wrk` for `bench-nexus` (`apt install wrk` on Debian/Ubuntu).
|
||||||
|
|||||||
+20
-5
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 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.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден 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)
|
1. Selfhost parity for `--release` / `#line` (optional)
|
||||||
2. Selfhost parity for `--release` / `#line` (optional)
|
2. LSP: workspace symbol search / call hierarchy (optional)
|
||||||
3. LSP: workspace symbol search / call hierarchy (optional)
|
3. Deeper rename (type members / qualified paths)
|
||||||
4. 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