Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions frameworks/genhttp-11-ioxide/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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.
# 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:11.0.0-preview.5
WORKDIR /app
COPY --from=build /app .

EXPOSE 8080 8081
ENTRYPOINT ["dotnet", "genhttp.dll"]
92 changes: 92 additions & 0 deletions frameworks/genhttp-11-ioxide/Infrastructure/Postgres.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Buffers.Text;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;

using GenHTTP.Engine.Ioxide;

using ioxide;
using ioxide.pg;

namespace genhttp.Infrastructure;

/// <summary>
/// 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 <see cref="Pool" />. Rows arrive as UTF-8 text fields and are mapped into the shared
/// model types, so GenHTTP's serializer renders the JSON rather than a hand-rolled writer.
/// </summary>
public static class Postgres
{
public const string Columns = "id, name, category, price, quantity, active, tags, rating_score, rating_count";

public static PgOptions? Options { get; private set; }

public static bool Enabled => Options is not null;

public static PgPool Pool => IoxideReactor.Current.GetService<PgPool>();

public static void Configure(int reactors)
{
var url = Environment.GetEnvironmentVariable("DATABASE_URL");
if (string.IsNullOrEmpty(url))
{
Options = null;
return;
}

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),
};
}

// onReactorStart: start the ring-native pool on this reactor.
public static void Start(Reactor reactor) => PgPool.Start(reactor, Options!);

// Map one Postgres row (UTF-8 text fields) into a model 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 ProcessedItem MapItem(PgRow row) => new()
{
Id = ParseInt(row.Field(0)),
Name = Encoding.UTF8.GetString(row.Field(1)),
Category = Encoding.UTF8.GetString(row.Field(2)),
Price = ParseInt(row.Field(3)),
Quantity = ParseInt(row.Field(4)),
Active = row.Field(5).SequenceEqual("t"u8),
Tags = JsonSerializer.Deserialize<List<string>>(row.Field(6)),
Rating = new RatingInfo { Score = ParseInt(row.Field(7)), Count = ParseInt(row.Field(8)) },
};

public static int ParseInt(ReadOnlySpan<byte> s) => Utf8Parser.TryParse(s, out int v, out _) ? v : 0;

public static long ParseLong(ReadOnlySpan<byte> 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;
}
}
50 changes: 50 additions & 0 deletions frameworks/genhttp-11-ioxide/Infrastructure/TlsTransport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO.Pipelines;

using GenHTTP.Engine.Ioxide;

using ioxide;
using ioxide.tls;

namespace genhttp.Infrastructure;

/// <summary>
/// TLS configuration for the json-tls profile: a second listener on :8081 with kTLS TX offload,
/// read from TLS_CERT / TLS_KEY (defaulting to the harness-mounted /certs). The transport plumbing
/// lives in the engine (<see cref="IoxideTls"/> / <c>TlsDuplexPipe</c>); this just supplies the
/// cert/key and wires the per-reactor service + the connection factory's port decision.
/// </summary>
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)
=> IoxideTls.StartService(reactor, new TlsOptions { CertificatePath = CertPath!, KeyPath = KeyPath! });

// connectionFactory: TLS-terminate the :8081 listener; the main (:8080) port stays plaintext.
public static ValueTask<IDuplexPipe> CreatePipe(Connection conn)
=> conn.ListenerPort == Port
? IoxideTls.AcceptAsync(conn)
: new ValueTask<IDuplexPipe>(new ConnectionDualPipe(conn));
}
59 changes: 59 additions & 0 deletions frameworks/genhttp-11-ioxide/Model.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace genhttp;

public sealed class DatasetItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public int Price { get; set; }
public int Quantity { get; set; }
public bool Active { get; set; }
public List<string>? Tags { get; set; }
public RatingInfo? Rating { get; set; }
}

public sealed class ProcessedItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public int Price { get; set; }
public int Quantity { get; set; }
public bool Active { get; set; }
public List<string>? Tags { get; set; }
public RatingInfo? Rating { get; set; }
public long Total { get; set; }
}

public sealed class RatingInfo
{
public int Score { get; set; }
public int Count { get; set; }
}

public sealed class ListWithCount<T>(List<T> items)
{

public List<T> Items => items;

public int Count => items.Count;

}


public sealed class CrudListResponse
{
public List<ProcessedItem> Items { get; set; } = [];
public long Total { get; set; }
public int Page { get; set; }
public int Limit { get; set; }
}

public sealed class CrudItem
{
public int? Id { get; set; }
public string? Name { get; set; }
public string? Category { get; set; }
public int Price { get; set; }
public int Quantity { get; set; }
}
64 changes: 64 additions & 0 deletions frameworks/genhttp-11-ioxide/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.IO.Pipelines;
using System.Net;

using genhttp;
using genhttp.Infrastructure;

using GenHTTP.Engine.Ioxide;
using GenHTTP.Modules.Compression;

using ioxide;

// 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;

// 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.
Postgres.Configure(reactors);
var tls = Tls.Configure();

Action<Reactor>? onReactorStart = (!Postgres.Enabled && !tls) ? null : r =>
{
if (Postgres.Enabled)
{
Postgres.Start(r);
}
if (tls)
{
Tls.StartService(r);
}
};

// json-tls adds a second, TLS-terminating listener on :8081 (kTLS TX); :8080 stays plaintext.
Func<Connection, ValueTask<IDuplexPipe>>? connectionFactory = null;
if (tls)
{
connectionFactory = Tls.CreatePipe;
}

// The engine buffers a whole response in one write slab; static assets can exceed the 16 KB default.
// Size the slab above the largest asset (plus GenHTTP's 64 KB file-copy buffer) — only when static is
// mounted, so the high-connection profiles keep the small per-connection buffer.
int? writeSlab = null;
var staticRoot = Environment.GetEnvironmentVariable("IOXIDE_STATIC") ?? "/data/static";
if (Directory.Exists(staticRoot))
{
long largest = 0;
foreach (var file in Directory.EnumerateFiles(staticRoot, "*", SearchOption.AllDirectories))
{
largest = Math.Max(largest, new FileInfo(file).Length);
}
writeSlab = (int)largest + 128 * 1024;
}

var host = Host.Create(
c => c with { ReactorCount = reactors, ExtraPorts = tls ? [Tls.Port] : c.ExtraPorts, WriteSlabSize = writeSlab ?? c.WriteSlabSize },
onReactorStart,
connectionFactory)
.Handler(Project.Create())
.Compression(CompressedContent.Default());

host.Bind(IPAddress.Any, 8080);

await host.RunAsync();
61 changes: 61 additions & 0 deletions frameworks/genhttp-11-ioxide/Project.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using GenHTTP.Api.Content;

using GenHTTP.Modules.IO;
using GenHTTP.Modules.IoxideFiles;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Layouting.Provider;
using GenHTTP.Modules.Webservices;

using genhttp.Infrastructure;
using genhttp.Tests;

namespace genhttp;

public static class Project
{

// 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)
// async-db -> /async-db (Postgres range query, when DATABASE_URL is set)
// crud -> /crud/items (list/read/create/update, when DATABASE_URL is set)
// static -> /static/... (files from IOXIDE_STATIC, when the dir exists)
public static IHandlerBuilder Create()
{
var app = Layout.Create()
.Add("pipeline", Content.From(Resource.FromString("ok")))
.AddService<Baseline>("baseline11")
.AddService<Baseline>("baseline2")
.AddService<Upload>("upload")
.AddService<Json>("json");

// async-db and crud require a configured Postgres (DATABASE_URL).
if (Postgres.Enabled)
{
var crud = Layout.Create()
.AddService<Crud>("items");

app = app.AddService<AsyncDatabase>("async-db")
.Add("crud", crud);
}

return app.AddStaticFiles();
}

private static LayoutBuilder AddStaticFiles(this LayoutBuilder app)
{
var staticDir = Environment.GetEnvironmentVariable("IOXIDE_STATIC") ?? "/data/static";

if (Directory.Exists(staticDir))
{
// Serve static files through ioxide.file (baked native responses + statx revalidation)
// rather than GenHTTP's Modules.Files, whose FileResource overflows the ioxide write slab.
app.Add("static", IoxideFiles.From(staticDir));
}

return app;
}

}
42 changes: 42 additions & 0 deletions frameworks/genhttp-11-ioxide/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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 / 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

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

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.
Loading