From 721a593b61cce20e3d0900c7e41aa60cb38c44c9 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 15 Jun 2026 18:58:35 +0100 Subject: [PATCH 01/12] Add genhttp-11-ioxide framework entry GenHTTP 11 on a custom io_uring server engine (the ioxide runtime) instead of the default socket engine. Built from the GenHTTP ioxide-engine branch (Kaliumhexacyanoferrat/GenHTTP#860): the Dockerfile clones that branch and the app references its engine + IO/Layouting/Webservices modules from source. Subscribes to the HTTP/1.1 profiles the engine supports: baseline, pipelined, limited-conn. Co-Authored-By: Claude Opus 4.8 --- frameworks/genhttp-11-ioxide/Dockerfile | 19 ++++++++++++++ frameworks/genhttp-11-ioxide/Program.cs | 14 ++++++++++ frameworks/genhttp-11-ioxide/Project.cs | 23 ++++++++++++++++ frameworks/genhttp-11-ioxide/README.md | 26 +++++++++++++++++++ .../genhttp-11-ioxide/Tests/Baseline.cs | 15 +++++++++++ frameworks/genhttp-11-ioxide/genhttp.csproj | 24 +++++++++++++++++ frameworks/genhttp-11-ioxide/meta.json | 18 +++++++++++++ 7 files changed, 139 insertions(+) create mode 100644 frameworks/genhttp-11-ioxide/Dockerfile create mode 100644 frameworks/genhttp-11-ioxide/Program.cs create mode 100644 frameworks/genhttp-11-ioxide/Project.cs create mode 100644 frameworks/genhttp-11-ioxide/README.md create mode 100644 frameworks/genhttp-11-ioxide/Tests/Baseline.cs create mode 100644 frameworks/genhttp-11-ioxide/genhttp.csproj create mode 100644 frameworks/genhttp-11-ioxide/meta.json diff --git a/frameworks/genhttp-11-ioxide/Dockerfile b/frameworks/genhttp-11-ioxide/Dockerfile new file mode 100644 index 000000000..2904613b8 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# GenHTTP with the ioxide io_uring engine (PR #860). Cloned to /src/genhttp, +# which is where genhttp.csproj's $(GenHttpSrc) project references resolve. +# Needs an SDK with Roslyn 5.3+ for GenHTTP's MemoryView source generator; +# the :10.0 tag provides it. +RUN git clone --depth 1 --branch ioxide-engine https://github.com/Kaliumhexacyanoferrat/GenHTTP.git genhttp + +WORKDIR /src/app +COPY . . +RUN dotnet publish genhttp.csproj -c Release --no-self-contained -o /app + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app . + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "genhttp.dll"] diff --git a/frameworks/genhttp-11-ioxide/Program.cs b/frameworks/genhttp-11-ioxide/Program.cs new file mode 100644 index 000000000..127fd505f --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Program.cs @@ -0,0 +1,14 @@ +using System.Net; + +using genhttp; + +using GenHTTP.Engine.Ioxide; + +var app = Project.Create(); + +var host = Host.Create() + .Handler(app); + +host.Bind(IPAddress.Any, 8080); + +await host.RunAsync(); diff --git a/frameworks/genhttp-11-ioxide/Project.cs b/frameworks/genhttp-11-ioxide/Project.cs new file mode 100644 index 000000000..19dad3916 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Project.cs @@ -0,0 +1,23 @@ +using GenHTTP.Api.Content; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Webservices; + +using genhttp.Tests; + +namespace genhttp; + +public static class Project +{ + + // Only the HTTP/1.1 plaintext endpoints exercised by this entry's profiles: + // baseline / limited-conn -> /baseline11 (Baseline webservice: GET/POST sum) + // pipelined -> /pipeline (fixed "ok") + public static IHandlerBuilder Create() + => Layout.Create() + .Add("pipeline", Content.From(Resource.FromString("ok"))) + .AddService("baseline11") + .AddService("baseline2"); + +} diff --git a/frameworks/genhttp-11-ioxide/README.md b/frameworks/genhttp-11-ioxide/README.md new file mode 100644 index 000000000..13c41f6b9 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/README.md @@ -0,0 +1,26 @@ +# genhttp-11-ioxide + +[GenHTTP 11](https://github.com/Kaliumhexacyanoferrat/GenHTTP) running on a custom +**io_uring** server engine (the [ioxide](https://github.com/MDA2AV/ioxide) runtime) +instead of GenHTTP's default socket engine. + +The engine runs GenHTTP's own HTTP/1.1 conversation directly on ioxide's per-connection +duplex pipe — thread-per-core, one io_uring reactor per core, with chunked transfer-encoding, +keep-alive, a per-second cached `Date` header and a per-reactor request pool. It is built from +the GenHTTP `ioxide-engine` branch ([PR #860](https://github.com/Kaliumhexacyanoferrat/GenHTTP/pull/860)): +the Dockerfile clones that branch and the app references its engine plus the +IO / Layouting / Webservices modules from source. + +## Profiles + +HTTP/1.1 only (the engine does not yet do TLS, HTTP/2 or WebSocket): + +- `baseline` — mixed GET/POST with query parsing (`/baseline11` sum webservice) +- `pipelined` — 16× batched pipelining (`/pipeline`) +- `limited-conn` — short-lived connections that close after 10 requests + +## Build note + +Requires a .NET SDK with Roslyn 5.3+ (GenHTTP's `MemoryView` source generator references +`Microsoft.CodeAnalysis 5.3`); the `mcr.microsoft.com/dotnet/sdk:10.0` image used by the +Dockerfile provides it. diff --git a/frameworks/genhttp-11-ioxide/Tests/Baseline.cs b/frameworks/genhttp-11-ioxide/Tests/Baseline.cs new file mode 100644 index 000000000..1e31cbf42 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Tests/Baseline.cs @@ -0,0 +1,15 @@ +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Baseline +{ + + [ResourceMethod] + public int Sum(int a, int b) => a + b; + + [ResourceMethod(Method.Post)] + public int Sum(int a, int b, [FromBody] int c) => a + b + c; + +} diff --git a/frameworks/genhttp-11-ioxide/genhttp.csproj b/frameworks/genhttp-11-ioxide/genhttp.csproj new file mode 100644 index 000000000..fcd9b9d52 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/genhttp.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + true + + + /src/genhttp + + + + + + + + + + diff --git a/frameworks/genhttp-11-ioxide/meta.json b/frameworks/genhttp-11-ioxide/meta.json new file mode 100644 index 000000000..a852b4f5f --- /dev/null +++ b/frameworks/genhttp-11-ioxide/meta.json @@ -0,0 +1,18 @@ +{ + "display_name": "genhttp-11-ioxide", + "language": "C#", + "type": "emerging", + "mode": "tuned", + "engine": "io_uring", + "description": "GenHTTP 11 running on a custom io_uring server engine (the ioxide runtime) instead of the default socket engine. Built from the GenHTTP ioxide-engine branch (PR #860). HTTP/1.1 only.", + "repo": "https://github.com/Kaliumhexacyanoferrat/GenHTTP/pull/860", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn" + ], + "maintainers": [ + "Kaliumhexacyanoferrat" + ] +} From d0793cb3001ee78f506e7b43bbbb6a5e22acb1c4 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 17 Jun 2026 20:31:19 +0100 Subject: [PATCH 02/12] genhttp-11-ioxide: target .NET 11 - TFM net10.0 -> net11.0 (matches the GenHTTP ioxide-engine branch on net11) - Dockerfile: sdk 10.0 -> 11.0.100-preview.5, runtime 10.0 -> 11.0.0-preview.5 - README build note + meta description updated for .NET 11 --- frameworks/genhttp-11-ioxide/Dockerfile | 9 +++++---- frameworks/genhttp-11-ioxide/README.md | 7 ++++--- frameworks/genhttp-11-ioxide/genhttp.csproj | 5 +++-- frameworks/genhttp-11-ioxide/meta.json | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/frameworks/genhttp-11-ioxide/Dockerfile b/frameworks/genhttp-11-ioxide/Dockerfile index 2904613b8..ef9042295 100644 --- a/frameworks/genhttp-11-ioxide/Dockerfile +++ b/frameworks/genhttp-11-ioxide/Dockerfile @@ -1,17 +1,18 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:11.0.100-preview.5 AS build WORKDIR /src # GenHTTP with the ioxide io_uring engine (PR #860). Cloned to /src/genhttp, # which is where genhttp.csproj's $(GenHttpSrc) project references resolve. -# Needs an SDK with Roslyn 5.3+ for GenHTTP's MemoryView source generator; -# the :10.0 tag provides it. +# This entry targets net11.0; the cloned ioxide-engine branch must too. The +# :11.0.100-preview.5 SDK provides the .NET 11 framework plus Roslyn 5.3+ for +# GenHTTP's MemoryView source generator. RUN git clone --depth 1 --branch ioxide-engine https://github.com/Kaliumhexacyanoferrat/GenHTTP.git genhttp WORKDIR /src/app COPY . . RUN dotnet publish genhttp.csproj -c Release --no-self-contained -o /app -FROM mcr.microsoft.com/dotnet/runtime:10.0 +FROM mcr.microsoft.com/dotnet/runtime:11.0.0-preview.5 WORKDIR /app COPY --from=build /app . diff --git a/frameworks/genhttp-11-ioxide/README.md b/frameworks/genhttp-11-ioxide/README.md index 13c41f6b9..9c7e5425d 100644 --- a/frameworks/genhttp-11-ioxide/README.md +++ b/frameworks/genhttp-11-ioxide/README.md @@ -21,6 +21,7 @@ HTTP/1.1 only (the engine does not yet do TLS, HTTP/2 or WebSocket): ## Build note -Requires a .NET SDK with Roslyn 5.3+ (GenHTTP's `MemoryView` source generator references -`Microsoft.CodeAnalysis 5.3`); the `mcr.microsoft.com/dotnet/sdk:10.0` image used by the -Dockerfile provides it. +This entry targets **.NET 11** (`net11.0`), matching the GenHTTP `ioxide-engine` branch it +builds from. Requires the .NET 11 SDK with Roslyn 5.3+ (GenHTTP's `MemoryView` source generator +references `Microsoft.CodeAnalysis 5.3`); the `mcr.microsoft.com/dotnet/sdk:11.0.100-preview.5` +image used by the Dockerfile provides both. diff --git a/frameworks/genhttp-11-ioxide/genhttp.csproj b/frameworks/genhttp-11-ioxide/genhttp.csproj index fcd9b9d52..a3e6396d7 100644 --- a/frameworks/genhttp-11-ioxide/genhttp.csproj +++ b/frameworks/genhttp-11-ioxide/genhttp.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net11.0 enable enable true @@ -10,7 +10,8 @@ + ioxide + Glyph11 + GenHTTP.Api/Engine.Shared transitively. The cloned branch + must target net11.0 to match this entry (see README build note). --> /src/genhttp diff --git a/frameworks/genhttp-11-ioxide/meta.json b/frameworks/genhttp-11-ioxide/meta.json index a852b4f5f..e4498322e 100644 --- a/frameworks/genhttp-11-ioxide/meta.json +++ b/frameworks/genhttp-11-ioxide/meta.json @@ -4,7 +4,7 @@ "type": "emerging", "mode": "tuned", "engine": "io_uring", - "description": "GenHTTP 11 running on a custom io_uring server engine (the ioxide runtime) instead of the default socket engine. Built from the GenHTTP ioxide-engine branch (PR #860). HTTP/1.1 only.", + "description": "GenHTTP 11 on .NET 11 running on a custom io_uring server engine (the ioxide runtime) instead of the default socket engine. Built from the GenHTTP ioxide-engine branch (PR #860). HTTP/1.1 only.", "repo": "https://github.com/Kaliumhexacyanoferrat/GenHTTP/pull/860", "enabled": true, "tests": [ From d122fa9e4753161f146f1141620441f134ece1d4 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 17 Jun 2026 23:38:50 +0100 Subject: [PATCH 03/12] genhttp-11-ioxide: add json / db / tls profiles - json, json-comp, upload, static (GenHTTP handlers; json via GenHTTP serialization) - async-db, crud via ioxide.pg (per-reactor pool resolved through the engine seam); in-process cache-aside for crud - json-tls on :8081 via ioxide.tls (kTLS TX) through the connection-transport seam - api-4 / api-16 reuse the above at 4 / 16 reactors - meta.json tests, Dockerfile EXPOSE 8081, README updated --- .../genhttp-11-ioxide/AsyncDbService.cs | 33 ++++ frameworks/genhttp-11-ioxide/CrudService.cs | 131 +++++++++++++ frameworks/genhttp-11-ioxide/Data.cs | 25 +++ frameworks/genhttp-11-ioxide/Db.cs | 97 ++++++++++ frameworks/genhttp-11-ioxide/Dockerfile | 2 +- frameworks/genhttp-11-ioxide/JsonService.cs | 36 ++++ frameworks/genhttp-11-ioxide/Program.cs | 44 ++++- frameworks/genhttp-11-ioxide/Project.cs | 38 +++- frameworks/genhttp-11-ioxide/README.md | 19 +- frameworks/genhttp-11-ioxide/TlsTransport.cs | 173 ++++++++++++++++++ frameworks/genhttp-11-ioxide/UploadService.cs | 48 +++++ frameworks/genhttp-11-ioxide/genhttp.csproj | 6 + frameworks/genhttp-11-ioxide/meta.json | 13 +- 13 files changed, 650 insertions(+), 15 deletions(-) create mode 100644 frameworks/genhttp-11-ioxide/AsyncDbService.cs create mode 100644 frameworks/genhttp-11-ioxide/CrudService.cs create mode 100644 frameworks/genhttp-11-ioxide/Data.cs create mode 100644 frameworks/genhttp-11-ioxide/Db.cs create mode 100644 frameworks/genhttp-11-ioxide/JsonService.cs create mode 100644 frameworks/genhttp-11-ioxide/TlsTransport.cs create mode 100644 frameworks/genhttp-11-ioxide/UploadService.cs diff --git a/frameworks/genhttp-11-ioxide/AsyncDbService.cs b/frameworks/genhttp-11-ioxide/AsyncDbService.cs new file mode 100644 index 000000000..4d8c8ae46 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/AsyncDbService.cs @@ -0,0 +1,33 @@ +using GenHTTP.Modules.Webservices; + +namespace genhttp; + +public sealed class AsyncDbService +{ + + // GET /async-db?min=&max=&limit= -> items with price in [min,max], fetched from Postgres via + // ioxide.pg's per-reactor pool and returned as a typed object GenHTTP serializes. + [ResourceMethod] + public async ValueTask Get(int min = 10, int max = 50, int limit = 50) + { + if (limit < 1) + { + limit = 1; + } + if (limit > 50) + { + limit = 50; + } + + var items = new List(); + + // min/max/limit are int-typed (query-bound), so inline interpolation is injection-safe and + // keeps a stable statement shape — matches the reference async-db endpoint. + var sql = $"SELECT {Db.Columns} FROM items WHERE price BETWEEN {min} AND {max} LIMIT {limit}"; + + await Db.Pool.QueryAsync(sql, default, row => items.Add(Db.MapRow(row))); + + return new Db.DbResponse(items.ToArray(), items.Count); + } + +} diff --git a/frameworks/genhttp-11-ioxide/CrudService.cs b/frameworks/genhttp-11-ioxide/CrudService.cs new file mode 100644 index 000000000..cb2edd089 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/CrudService.cs @@ -0,0 +1,131 @@ +using System.Text.Json; + +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Conversion.Serializers.Json; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +using ioxide.pg; + +using Microsoft.Extensions.Caching.Memory; + +namespace genhttp; + +// Mounted at /crud: +// GET /crud/items?category=&page=&limit= -> paginated list +// GET /crud/items/{id} -> single item, cache-aside (X-Cache: MISS|HIT, 1s TTL) +// POST /crud/items -> upsert (201) +// PUT /crud/items/{id} -> update (200), invalidates the cache entry +// JSON bodies are produced by GenHTTP's serializer (DTO return values, or JsonContent for the +// cached single-item read where a response header is needed). +public sealed class CrudService +{ + private const string SelectGet = "SELECT " + Db.Columns + " FROM items WHERE id = $1"; + + private const string SelectList = "SELECT " + Db.Columns + " FROM items WHERE category = $1 ORDER BY id LIMIT $2 OFFSET $3"; + + private const string Upsert = "INSERT INTO items (id, name, category, price, quantity, active, tags, rating_score, rating_count) " + + "VALUES ($1, $2, $3, $4, $5, true, '[]'::jsonb, 0, 0) " + + "ON CONFLICT (id) DO UPDATE SET name = $2, category = $3, price = $4, quantity = $5"; + + private const string UpdateSql = "UPDATE items SET name = $2, category = $3, price = $4, quantity = $5 WHERE id = $1"; + + [ResourceMethod("items")] + public async ValueTask ListItems(string? category = null, int page = 1, int limit = 10) + { + if (page < 1) + { + page = 1; + } + if (limit < 1) + { + limit = 1; + } + + var items = new List(); + + PgParam[] args = [PgParam.Text(category ?? ""), PgParam.Int(limit), PgParam.Int((long)(page - 1) * limit)]; + await Db.Pool.QueryAsync(SelectList, args, row => items.Add(Db.MapRow(row))); + + return new Db.DbResponse(items.ToArray(), items.Count); + } + + [ResourceMethod("items/:id")] + public async ValueTask GetItem(int id, IRequest request) + { + var key = CacheKey(id); + + if (Db.Cache.TryGetValue(key, out byte[]? cached) && cached is not null) + { + return request.Respond().Content(cached, ContentType.ApplicationJson).Header("X-Cache", "HIT").Build(); + } + + Db.DbItem? item = null; + + PgParam[] args = [PgParam.Int(id)]; + await Db.Pool.QueryAsync(SelectGet, args, row => item = Db.MapRow(row)); + + if (item is null) + { + return request.Respond().Status(ResponseStatus.NotFound).Build(); + } + + // Serialize once with GenHTTP's JSON options and cache the bytes, so a HIT skips both the + // DB round-trip and the re-serialization. + var bytes = JsonSerializer.SerializeToUtf8Bytes(item, JsonFormat.GetDefaultOptions()); + Db.Cache.Set(key, bytes, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1) }); + + return request.Respond().Content(bytes, ContentType.ApplicationJson).Header("X-Cache", "MISS").Build(); + } + + [ResourceMethod(Method.Post, "items")] + public async ValueTask CreateItem(IRequest request) + { + var item = await ReadItem(request); + + PgParam[] args = [PgParam.Int(item.Id), PgParam.Text(item.Name), PgParam.Text(item.Category), PgParam.Int(item.Price), PgParam.Int(item.Quantity)]; + await Db.Pool.QueryAsync(Upsert, args); + + Db.Cache.Remove(CacheKey(item.Id)); + return request.Respond().Status(ResponseStatus.Created).Build(); + } + + [ResourceMethod(Method.Put, "items/:id")] + public async ValueTask UpdateItem(int id, IRequest request) + { + var item = await ReadItem(request); + + PgParam[] args = [PgParam.Int(id), PgParam.Text(item.Name), PgParam.Text(item.Category), PgParam.Int(item.Price), PgParam.Int(item.Quantity)]; + await Db.Pool.QueryAsync(UpdateSql, args); + + Db.Cache.Remove(CacheKey(id)); + return request.Respond().Status(ResponseStatus.Ok).Build(); + } + + private static string CacheKey(int id) => "item:" + id; + + private static async ValueTask ReadItem(IRequest request) + { + var body = request.GetBody(); + if (body is null) + { + return default; + } + + var data = await body.AsMemoryAsync(); + using var doc = JsonDocument.Parse(data); + var root = doc.RootElement; + + return new CrudItem(GetInt(root, "id"), GetString(root, "name"), GetString(root, "category"), GetInt(root, "price"), GetInt(root, "quantity")); + } + + private static int GetInt(JsonElement e, string name) + => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt32() : 0; + + private static string GetString(JsonElement e, string name) + => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : ""; + + private readonly record struct CrudItem(int Id, string Name, string Category, long Price, long Quantity); +} diff --git a/frameworks/genhttp-11-ioxide/Data.cs b/frameworks/genhttp-11-ioxide/Data.cs new file mode 100644 index 000000000..e0c65e396 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Data.cs @@ -0,0 +1,25 @@ +using System.Text.Json; + +namespace genhttp; + +/// +/// Loads the shared benchmark dataset (the same /data/dataset.json the HttpArena entries use). The +/// json / json-comp profiles return these items (plus a per-request total) as typed objects that +/// GenHTTP's own serialization renders — exercising the real response pipeline, not a hand-rolled writer. +/// +public static class Data +{ + public sealed record Rating(int Score, int Count); + + public sealed record Item(int Id, string Name, string Category, long Price, long Quantity, bool Active, string[] Tags, Rating Rating); + + public static Item[] Items { get; private set; } = []; + + public static int Count => Items.Length; + + public static void LoadDataset(string path) + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + Items = JsonSerializer.Deserialize(File.ReadAllBytes(path), options) ?? []; + } +} diff --git a/frameworks/genhttp-11-ioxide/Db.cs b/frameworks/genhttp-11-ioxide/Db.cs new file mode 100644 index 000000000..247be5bee --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Db.cs @@ -0,0 +1,97 @@ +using System.Buffers.Text; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; + +using GenHTTP.Engine.Ioxide; + +using ioxide.pg; + +using Microsoft.Extensions.Caching.Memory; + +namespace genhttp; + +/// +/// Postgres wiring for the async-db / crud profiles. The pool is per-reactor and ring-native — +/// started in each reactor's OnStart (via the engine's onReactorStart hook) and resolved from a +/// handler with . Rows are mapped into typed objects () so +/// GenHTTP's serializer renders the JSON, rather than hand-writing bytes. +/// +public static class Db +{ + public const string Columns = "id, name, category, price, quantity, active, tags, rating_score, rating_count"; + + public sealed record DbItem(int Id, string Name, string Category, long Price, long Quantity, bool Active, string[] Tags, Data.Rating Rating); + + public sealed record DbResponse(DbItem[] Items, int Count); + + public static PgOptions? Options { get; private set; } + + public static bool Enabled => Options is not null; + + // Single shared in-process cache (crud cache-aside, CRUD_CACHE=inproc semantics). + public static readonly IMemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); + + public static PgPool Pool => IoxideReactor.Current.GetService(); + + public static PgOptions? Configure(int reactors) + { + var url = Environment.GetEnvironmentVariable("DATABASE_URL"); + if (string.IsNullOrEmpty(url)) + { + Options = null; + return null; + } + + var uri = new Uri(url); + var userInfo = uri.UserInfo.Split(':', 2); + var maxConn = int.TryParse(Environment.GetEnvironmentVariable("DATABASE_MAX_CONN"), out var mc) ? mc : 256; + + Options = new PgOptions + { + Host = ResolveIPv4(uri.Host), + Port = (ushort)(uri.Port > 0 ? uri.Port : 5432), + User = userInfo[0], + Password = userInfo.Length > 1 ? userInfo[1] : null, + Database = uri.AbsolutePath.TrimStart('/'), + PoolSize = Math.Clamp(maxConn / Math.Max(reactors, 1), 1, 8), + }; + + return Options; + } + + // Map one Postgres row (UTF-8 text fields) into a typed item. + // 0 id, 1 name, 2 category, 3 price, 4 quantity, 5 active(t/f), 6 tags(jsonb), 7 rating_score, 8 rating_count + public static DbItem MapRow(PgRow row) => new( + ParseInt(row.Field(0)), + Encoding.UTF8.GetString(row.Field(1)), + Encoding.UTF8.GetString(row.Field(2)), + ParseLong(row.Field(3)), + ParseLong(row.Field(4)), + row.Field(5).SequenceEqual("t"u8), + JsonSerializer.Deserialize(row.Field(6)) ?? [], + new Data.Rating(ParseInt(row.Field(7)), ParseInt(row.Field(8)))); + + private static int ParseInt(ReadOnlySpan s) => Utf8Parser.TryParse(s, out int v, out _) ? v : 0; + + private static long ParseLong(ReadOnlySpan s) => Utf8Parser.TryParse(s, out long v, out _) ? v : 0; + + private static string ResolveIPv4(string host) + { + if (IPAddress.TryParse(host, out _)) + { + return host; + } + + foreach (var addr in Dns.GetHostAddresses(host)) + { + if (addr.AddressFamily == AddressFamily.InterNetwork) + { + return addr.ToString(); + } + } + + return host; + } +} diff --git a/frameworks/genhttp-11-ioxide/Dockerfile b/frameworks/genhttp-11-ioxide/Dockerfile index ef9042295..ee6e8bf1d 100644 --- a/frameworks/genhttp-11-ioxide/Dockerfile +++ b/frameworks/genhttp-11-ioxide/Dockerfile @@ -16,5 +16,5 @@ FROM mcr.microsoft.com/dotnet/runtime:11.0.0-preview.5 WORKDIR /app COPY --from=build /app . -EXPOSE 8080 +EXPOSE 8080 8081 ENTRYPOINT ["dotnet", "genhttp.dll"] diff --git a/frameworks/genhttp-11-ioxide/JsonService.cs b/frameworks/genhttp-11-ioxide/JsonService.cs new file mode 100644 index 000000000..2c7b4f1d3 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/JsonService.cs @@ -0,0 +1,36 @@ +using GenHTTP.Modules.Webservices; + +namespace genhttp; + +public sealed class JsonService +{ + // The response shape (camelCase, declaration order) is produced by GenHTTP's JSON serializer. + public sealed record JsonItem(int Id, string Name, string Category, long Price, long Quantity, bool Active, string[] Tags, Data.Rating Rating, long Total); + + public sealed record JsonResponse(JsonItem[] Items, int Count); + + // GET /json/{count}?m=N -> dataset items with per-item total = price*quantity*m, returned as a + // typed object that GenHTTP serializes (Conversion module, camelCase). json-comp is the same + // endpoint with Accept-Encoding: br; the host compression concern handles the encoding. + [ResourceMethod(":count")] + public JsonResponse Get(int count, int m = 1) + { + if (count < 1) + { + count = 1; + } + if (count > Data.Count) + { + count = Data.Count; + } + + var items = new JsonItem[count]; + for (var i = 0; i < count; i++) + { + var it = Data.Items[i]; + items[i] = new JsonItem(it.Id, it.Name, it.Category, it.Price, it.Quantity, it.Active, it.Tags, it.Rating, it.Price * it.Quantity * m); + } + + return new JsonResponse(items, count); + } +} diff --git a/frameworks/genhttp-11-ioxide/Program.cs b/frameworks/genhttp-11-ioxide/Program.cs index 127fd505f..612681cbe 100644 --- a/frameworks/genhttp-11-ioxide/Program.cs +++ b/frameworks/genhttp-11-ioxide/Program.cs @@ -1,13 +1,51 @@ +using System.IO.Pipelines; using System.Net; using genhttp; using GenHTTP.Engine.Ioxide; +using GenHTTP.Modules.Compression; -var app = Project.Create(); +using ioxide; +using ioxide.pg; -var host = Host.Create() - .Handler(app); +// Reactor count follows the available CPUs (api-4 / api-16 control this via cpuset pinning); +// override with IOXIDE_REACTORS. +var reactors = int.TryParse(Environment.GetEnvironmentVariable("IOXIDE_REACTORS"), out var rc) ? rc : Environment.ProcessorCount; + +// Shared benchmark dataset for the json / json-comp profiles (harness mounts /data/dataset.json). +Data.LoadDataset(Environment.GetEnvironmentVariable("IOXIDE_DATASET") ?? "/data/dataset.json"); + +// Postgres (async-db / crud) and TLS (json-tls on :8081) are both per-reactor; configure them and +// fold their per-reactor init into one OnStart. +var pg = Db.Configure(reactors); +var tls = Tls.Configure(); + +Action? onReactorStart = (pg is null && !tls) ? null : r => +{ + if (pg != null) + { + PgPool.Start(r, pg); + } + if (tls) + { + Tls.StartService(r); + } +}; + +// json-tls adds a second, TLS-terminating listener on :8081 (kTLS TX); :8080 stays plaintext. +Func>? connectionFactory = null; +if (tls) +{ + connectionFactory = Tls.CreatePipe; +} + +var host = Host.Create( + c => c with { ReactorCount = reactors, ExtraPorts = tls ? [Tls.Port] : c.ExtraPorts }, + onReactorStart, + connectionFactory) + .Handler(Project.Create()) + .Compression(CompressedContent.Default()); host.Bind(IPAddress.Any, 8080); diff --git a/frameworks/genhttp-11-ioxide/Project.cs b/frameworks/genhttp-11-ioxide/Project.cs index 19dad3916..5cbb29493 100644 --- a/frameworks/genhttp-11-ioxide/Project.cs +++ b/frameworks/genhttp-11-ioxide/Project.cs @@ -1,5 +1,6 @@ using GenHTTP.Api.Content; +using GenHTTP.Modules.Files; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; using GenHTTP.Modules.Webservices; @@ -11,13 +12,36 @@ namespace genhttp; public static class Project { - // Only the HTTP/1.1 plaintext endpoints exercised by this entry's profiles: - // baseline / limited-conn -> /baseline11 (Baseline webservice: GET/POST sum) - // pipelined -> /pipeline (fixed "ok") + // HTTP/1.1 endpoints exercised by this entry's profiles: + // baseline / limited-conn -> /baseline11 (Baseline webservice: GET/POST sum) + // pipelined -> /pipeline (fixed "ok") + // json / json-comp -> /json/{count}?m=N (json-comp = json + Accept-Encoding: br) + // upload -> /upload (streamed request-body byte count) + // static -> /static/... (files from IOXIDE_STATIC, when the dir exists) public static IHandlerBuilder Create() - => Layout.Create() - .Add("pipeline", Content.From(Resource.FromString("ok"))) - .AddService("baseline11") - .AddService("baseline2"); + { + var layout = Layout.Create() + .Add("pipeline", Content.From(Resource.FromString("ok"))) + .AddService("baseline11") + .AddService("baseline2") + .AddService("json") + .AddService("upload"); + + // async-db and crud require a configured Postgres (DATABASE_URL). + if (Db.Enabled) + { + layout = layout.AddService("async-db") + .AddService("crud"); + } + + var staticDir = Environment.GetEnvironmentVariable("IOXIDE_STATIC") ?? "/data/static"; + + if (Directory.Exists(staticDir)) + { + layout = layout.Add("static", Assets.From(ResourceTree.FromDirectory(staticDir))); + } + + return layout; + } } diff --git a/frameworks/genhttp-11-ioxide/README.md b/frameworks/genhttp-11-ioxide/README.md index 9c7e5425d..97dbf1b33 100644 --- a/frameworks/genhttp-11-ioxide/README.md +++ b/frameworks/genhttp-11-ioxide/README.md @@ -9,15 +9,30 @@ duplex pipe — thread-per-core, one io_uring reactor per core, with chunked tra keep-alive, a per-second cached `Date` header and a per-reactor request pool. It is built from the GenHTTP `ioxide-engine` branch ([PR #860](https://github.com/Kaliumhexacyanoferrat/GenHTTP/pull/860)): the Dockerfile clones that branch and the app references its engine plus the -IO / Layouting / Webservices modules from source. +IO / Layouting / Webservices / Compression / Files modules from source, and the published +`ioxide.pg` (Postgres) and `ioxide.tls` (TLS) packages. + +Postgres access and TLS termination ride generic per-reactor seams the engine exposes +(`IoxideReactor.Current`, an `onReactorStart` hook, and a connection-transport factory) — the +engine itself stays free of any `ioxide.pg` / `ioxide.tls` dependency. ## Profiles -HTTP/1.1 only (the engine does not yet do TLS, HTTP/2 or WebSocket): +Responses are produced by GenHTTP's own pipeline (routing + serialization), not hand-written: - `baseline` — mixed GET/POST with query parsing (`/baseline11` sum webservice) - `pipelined` — 16× batched pipelining (`/pipeline`) - `limited-conn` — short-lived connections that close after 10 requests +- `json` / `json-comp` — `/json/{count}?m=N` serialized items; json-comp adds Brotli (`Accept-Encoding`) +- `json-tls` — `json` over TLS on `:8081` (`ioxide.tls`, kTLS TX offload) +- `static` — `/static/...` files with encoding negotiation (Modules.IO / Files) +- `upload` — `POST /upload`, streamed request body, returns the byte count +- `async-db` — `/async-db?min=&max=&limit=`, Postgres via `ioxide.pg` (per-reactor pool) +- `crud` — list / get / create / update on `/crud/items`, cache-aside (`X-Cache`, in-process) +- `api-4` / `api-16` — mixed baseline+json+async-db at 4 / 16 reactors + +`json-tls` serves on `:8081` when `TLS_CERT` / `TLS_KEY` (default `/certs`) exist; the DB profiles +need `DATABASE_URL`. Both are provided by the harness sidecars. ## Build note diff --git a/frameworks/genhttp-11-ioxide/TlsTransport.cs b/frameworks/genhttp-11-ioxide/TlsTransport.cs new file mode 100644 index 000000000..3721c1ce8 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/TlsTransport.cs @@ -0,0 +1,173 @@ +using System.Buffers; +using System.IO.Pipelines; + +using GenHTTP.Engine.Ioxide; + +using ioxide; +using ioxide.tls; + +namespace genhttp; + +/// +/// TLS termination for the json-tls profile: a second listener on :8081 with kTLS TX offload. +/// Configured from TLS_CERT / TLS_KEY (defaulting to the harness-mounted /certs). The service is +/// registered per reactor (OnStart) and the connection factory wraps :8081 connections. +/// +public static class Tls +{ + public const ushort Port = 8081; + + public static string? CertPath { get; private set; } + + public static string? KeyPath { get; private set; } + + public static bool Enabled => CertPath is not null; + + public static bool Configure() + { + var cert = Environment.GetEnvironmentVariable("TLS_CERT") ?? "/certs/server.crt"; + var key = Environment.GetEnvironmentVariable("TLS_KEY") ?? "/certs/server.key"; + + if (File.Exists(cert) && File.Exists(key)) + { + CertPath = cert; + KeyPath = key; + return true; + } + + return false; + } + + // onReactorStart: register the ring-native TLS service (OpenSSL ctx) on this reactor. + public static void StartService(Reactor reactor) + => TlsService.Start(reactor, new TlsOptions { CertificatePath = CertPath!, KeyPath = KeyPath! }); + + // connectionFactory: TLS-terminate the :8081 listener; the main (:8080) port stays plaintext. + public static async ValueTask CreatePipe(Connection conn) + { + if (conn.ListenerPort == Port) + { + var session = await IoxideReactor.Current.GetService().AcceptAsync(conn); + return new TlsDuplexPipe(conn, session); + } + + return new ConnectionDualPipe(conn); + } +} + +/// +/// Adapts a TLS connection to the duplex pipe GenHTTP serves over. Inbound: a pump reads raw recv +/// slices, decrypts each via the , and writes the plaintext into a Pipe the +/// engine reads. Outbound: the engine writes plaintext to the connection's writer and kTLS TX (enabled +/// during the handshake) has the kernel produce the records — so no explicit encrypt step. +/// +internal sealed class TlsDuplexPipe : IDuplexPipe, IAsyncDisposable +{ + private readonly Connection _conn; + + private readonly TlsSession _tls; + + private readonly Pipe _inbound; + + private readonly ConnectionDualPipe _outer; // only its writer is used (plaintext + kTLS TX) + + private readonly CancellationTokenSource _cts; + + private readonly Task _pump; + + public TlsDuplexPipe(Connection conn, TlsSession session) + { + _conn = conn; + _tls = session; + _inbound = new Pipe(); + _outer = new ConnectionDualPipe(conn); + _cts = new CancellationTokenSource(); + _pump = PumpAsync(_cts.Token); + } + + public PipeReader Input => _inbound.Reader; + + public PipeWriter Output => _outer.Output; + + private async Task PumpAsync(CancellationToken ct) + { + var writer = _inbound.Writer; + + try + { + // The client's first request can ride in bundled with its Finished flight. + var initial = _tls.DrainPlaintext(); + if (!initial.IsEmpty) + { + writer.Write(initial); + await writer.FlushAsync(ct); + } + + while (!ct.IsCancellationRequested) + { + var snapshot = await _conn.ReadAsync(); + + var produced = false; + + unsafe + { + while (_conn.TryGetItem(snapshot, out var item)) + { + if (item.HasBuffer) + { + var plain = _tls.Decrypt(item.Ptr, item.Len); + if (!plain.IsEmpty) + { + writer.Write(plain); + produced = true; + } + } + + _conn.ReturnBuffer(in item); + } + } + + _conn.ResetRead(); + + if (produced) + { + var flush = await writer.FlushAsync(ct); + if (flush.IsCompleted) + { + break; + } + } + + if (snapshot.IsClosed) + { + break; + } + } + } + catch + { + // connection fault / cancellation — the reader is completed in finally + } + finally + { + await writer.CompleteAsync(); + } + } + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + + try + { + await _pump; + } + catch + { + // ignore teardown faults + } + + _tls.Dispose(); + _cts.Dispose(); + } +} diff --git a/frameworks/genhttp-11-ioxide/UploadService.cs b/frameworks/genhttp-11-ioxide/UploadService.cs new file mode 100644 index 000000000..1de83e99f --- /dev/null +++ b/frameworks/genhttp-11-ioxide/UploadService.cs @@ -0,0 +1,48 @@ +using System.Buffers; + +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +namespace genhttp; + +public sealed class UploadService +{ + + // POST /upload -> streams the request body to its end and returns the byte count as text. + // The body is never buffered whole; it is read in chunks so memory stays bounded regardless + // of upload size (the upload profile sends 500KB..20MB bodies). + [ResourceMethod(Method.Post)] + public async ValueTask Post(IRequest request) + { + long total = 0; + + var body = request.GetBody(); + + if (body is not null) + { + var stream = body.AsStream(); + var buffer = ArrayPool.Shared.Rent(65536); + + try + { + int read; + while ((read = await stream.ReadAsync(buffer)) > 0) + { + total += read; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + return request.Respond() + .Content(total.ToString(), ContentType.TextPlain) + .Build(); + } + +} diff --git a/frameworks/genhttp-11-ioxide/genhttp.csproj b/frameworks/genhttp-11-ioxide/genhttp.csproj index a3e6396d7..e912f618e 100644 --- a/frameworks/genhttp-11-ioxide/genhttp.csproj +++ b/frameworks/genhttp-11-ioxide/genhttp.csproj @@ -5,6 +5,7 @@ net11.0 enable enable + true true + + + From 17a0b16887ba99abf91d8286c9c0708804f5ae62 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Sat, 20 Jun 2026 13:29:20 +0100 Subject: [PATCH 12/12] Update maintainers --- frameworks/genhttp-11-ioxide/meta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks/genhttp-11-ioxide/meta.json b/frameworks/genhttp-11-ioxide/meta.json index a552c27b1..7ecfae511 100644 --- a/frameworks/genhttp-11-ioxide/meta.json +++ b/frameworks/genhttp-11-ioxide/meta.json @@ -22,6 +22,6 @@ "api-16" ], "maintainers": [ - "Kaliumhexacyanoferrat" + "MDA2AV" ] }