diff --git a/frameworks/genhttp-11-ioxide/Dockerfile b/frameworks/genhttp-11-ioxide/Dockerfile new file mode 100644 index 000000000..ee6e8bf1d --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Dockerfile @@ -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"] diff --git a/frameworks/genhttp-11-ioxide/Infrastructure/Postgres.cs b/frameworks/genhttp-11-ioxide/Infrastructure/Postgres.cs new file mode 100644 index 000000000..2280cf176 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Infrastructure/Postgres.cs @@ -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; + +/// +/// 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 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. +/// +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(); + + 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>(row.Field(6)), + Rating = new RatingInfo { Score = ParseInt(row.Field(7)), Count = ParseInt(row.Field(8)) }, + }; + + public static int ParseInt(ReadOnlySpan s) => Utf8Parser.TryParse(s, out int v, out _) ? v : 0; + + public 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/Infrastructure/TlsTransport.cs b/frameworks/genhttp-11-ioxide/Infrastructure/TlsTransport.cs new file mode 100644 index 000000000..575af089e --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Infrastructure/TlsTransport.cs @@ -0,0 +1,50 @@ +using System.IO.Pipelines; + +using GenHTTP.Engine.Ioxide; + +using ioxide; +using ioxide.tls; + +namespace genhttp.Infrastructure; + +/// +/// 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 ( / TlsDuplexPipe); this just supplies the +/// cert/key and wires the per-reactor service + the connection factory's port decision. +/// +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 CreatePipe(Connection conn) + => conn.ListenerPort == Port + ? IoxideTls.AcceptAsync(conn) + : new ValueTask(new ConnectionDualPipe(conn)); +} diff --git a/frameworks/genhttp-11-ioxide/Model.cs b/frameworks/genhttp-11-ioxide/Model.cs new file mode 100644 index 000000000..a98572311 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Model.cs @@ -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? 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? 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(List items) +{ + + public List Items => items; + + public int Count => items.Count; + +} + + +public sealed class CrudListResponse +{ + public List 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; } +} diff --git a/frameworks/genhttp-11-ioxide/Program.cs b/frameworks/genhttp-11-ioxide/Program.cs new file mode 100644 index 000000000..454ba3139 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Program.cs @@ -0,0 +1,70 @@ +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? 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>? 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, + BufferRingEntries = 256 + }, + onReactorStart, + connectionFactory) + .Handler(Project.Create()) + .Compression(CompressedContent.Default()); + +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..7f1ef09ca --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Project.cs @@ -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("baseline11") + .AddService("baseline2") + .AddService("upload") + .AddService("json"); + + // async-db and crud require a configured Postgres (DATABASE_URL). + if (Postgres.Enabled) + { + var crud = Layout.Create() + .AddService("items"); + + app = app.AddService("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; + } + +} diff --git a/frameworks/genhttp-11-ioxide/README.md b/frameworks/genhttp-11-ioxide/README.md new file mode 100644 index 000000000..97dbf1b33 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/README.md @@ -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. diff --git a/frameworks/genhttp-11-ioxide/Tests/AsyncDatabase.cs b/frameworks/genhttp-11-ioxide/Tests/AsyncDatabase.cs new file mode 100644 index 000000000..e631f2e7e --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Tests/AsyncDatabase.cs @@ -0,0 +1,39 @@ +using System.Text; +using System.Text.Json; + +using genhttp.Infrastructure; + +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class AsyncDatabase +{ + + // 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 objects GenHTTP serializes ({ items, count }). + [ResourceMethod] + public async Task> Compute(int min = 10, int max = 50, int limit = 50) + { + var items = new List(limit); + + // 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 {Postgres.Columns} FROM items WHERE price BETWEEN {min} AND {max} LIMIT {limit}"; + + await Postgres.Pool.QueryAsync(sql, default, row => items.Add(new + { + id = Postgres.ParseInt(row.Field(0)), + name = Encoding.UTF8.GetString(row.Field(1)), + category = Encoding.UTF8.GetString(row.Field(2)), + price = Postgres.ParseInt(row.Field(3)), + quantity = Postgres.ParseInt(row.Field(4)), + active = row.Field(5).SequenceEqual("t"u8), + tags = JsonSerializer.Deserialize>(row.Field(6)), + rating = new { score = Postgres.ParseInt(row.Field(7)), count = Postgres.ParseInt(row.Field(8)) }, + })); + + return new ListWithCount(items); + } + +} 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/Tests/Crud.cs b/frameworks/genhttp-11-ioxide/Tests/Crud.cs new file mode 100644 index 000000000..890070ddf --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Tests/Crud.cs @@ -0,0 +1,141 @@ +using System.Text.Json; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using genhttp.Infrastructure; + +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +using ioxide.pg; + +using Microsoft.Extensions.Caching.Memory; +using StringContent = GenHTTP.Modules.IO.Strings.StringContent; + +namespace genhttp.Tests; + +// Mounted at /crud/items (Crud sublayout): +// GET /crud/items?category=&page=&limit= -> paginated list (load-more semantics) +// GET /crud/items/{id} -> single item, cache-aside (X-Cache: MISS|HIT, 200ms TTL) +// POST /crud/items -> upsert (201) +// PUT /crud/items/{id} -> update (200), invalidates the cache entry +public class Crud +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private static readonly IMemoryCache ItemCache = new MemoryCache(new MemoryCacheOptions()); + + private static readonly MemoryCacheEntryOptions ItemCacheOptions = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(200) }; + + [ResourceMethod] + public async Task List(string category = "electronics", int page = 1, int limit = 10) + { + if (page < 1) page = 1; + if (limit is < 1 or > 50) limit = 10; + + var offset = (page - 1) * limit; + + var items = new List(); + + PgParam[] args = [PgParam.Text(category), PgParam.Int(limit), PgParam.Int(offset)]; + await Postgres.Pool.QueryAsync( + "SELECT " + Postgres.Columns + " FROM items WHERE category = $1 ORDER BY id LIMIT $2 OFFSET $3", + args, + row => items.Add(Postgres.MapItem(row))); + + return new CrudListResponse + { + Items = items, + Total = items.Count, + Page = page, + Limit = limit + }; + } + + [ResourceMethod(":id")] + public async ValueTask Get(int id, IRequest request) + { + if (ItemCache.TryGetValue(id, out string? cached) && cached is not null) + { + return request.Respond() + .Content(new StringContent(cached, ContentType.ApplicationJson)) + .Header("X-Cache", "HIT") + .Build(); + } + + var item = await FetchItemByIdAsync(id); + + if (item is null) + { + throw new ProviderException(ResponseStatus.NotFound, $"Item with ID {id} does not exist"); + } + + // Serialize once and cache the string, so a HIT skips both the DB round-trip and re-serialization. + var json = JsonSerializer.Serialize(item, JsonOptions); + + ItemCache.Set(id, json, ItemCacheOptions); + + return request.Respond() + .Content(new StringContent(json, ContentType.ApplicationJson)) + .Header("X-Cache", "MISS") + .Build(); + } + + [ResourceMethod(Method.Post)] + public async Task> Create(CrudItem item) + { + PgParam[] args = + [ + PgParam.Int(item.Id ?? 0), + PgParam.Text(item.Name ?? "New Product"), + PgParam.Text(item.Category ?? "test"), + PgParam.Int(item.Price), + PgParam.Int(item.Quantity) + ]; + + await Postgres.Pool.QueryAsync( + "INSERT INTO items (id, name, category, price, quantity, active, tags, rating_score, rating_count) " + + "VALUES ($1, $2, $3, $4, $5, true, '[\"bench\"]', 0, 0) " + + "ON CONFLICT (id) DO UPDATE SET name = $2, price = $4, quantity = $5", + args); + + ItemCache.Remove(item.Id ?? 0); + + return new Result(item).Status(ResponseStatus.Created); + } + + [ResourceMethod(Method.Put, ":id")] + public async Task Update(int id, CrudItem item) + { + PgParam[] args = + [ + PgParam.Text(item.Name ?? "Updated"), + PgParam.Int(item.Price), + PgParam.Int(item.Quantity), + PgParam.Int(id) + ]; + + await Postgres.Pool.QueryAsync( + "UPDATE items SET name = $1, price = $2, quantity = $3 WHERE id = $4", + args); + + ItemCache.Remove(id); + + return item; + } + + private static async Task FetchItemByIdAsync(int id) + { + ProcessedItem? item = null; + + PgParam[] args = [PgParam.Int(id)]; + await Postgres.Pool.QueryAsync( + "SELECT " + Postgres.Columns + " FROM items WHERE id = $1 LIMIT 1", + args, + row => item = Postgres.MapItem(row)); + + return item; + } + +} diff --git a/frameworks/genhttp-11-ioxide/Tests/Json.cs b/frameworks/genhttp-11-ioxide/Tests/Json.cs new file mode 100644 index 000000000..7ed96d0a2 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Tests/Json.cs @@ -0,0 +1,60 @@ +using System.Text.Json; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Json +{ + private static readonly List? DatasetItems = LoadItems(); + + private static List? LoadItems() + { + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var datasetPath = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json"; + + if (File.Exists(datasetPath)) + { + return JsonSerializer.Deserialize>(File.ReadAllText(datasetPath), jsonOptions); + } + + return null; + } + + [ResourceMethod(":count")] + public ListWithCount Compute(int count, int m = 1) + { + if (DatasetItems == null) + { + throw new ProviderException(ResponseStatus.InternalServerError, "No dataset"); + } + + if (count > DatasetItems.Count) count = DatasetItems.Count; + if (count < 0) count = 0; + + var processed = new List(count); + + for (var i = 0; i < count; i++) + { + var d = DatasetItems[i]; + + processed.Add(new ProcessedItem + { + Id = d.Id, Name = d.Name, Category = d.Category, + Price = d.Price, Quantity = d.Quantity, Active = d.Active, + Tags = d.Tags, Rating = d.Rating, + Total = d.Price * d.Quantity * m + }); + } + + return new(processed); + } + +} diff --git a/frameworks/genhttp-11-ioxide/Tests/Upload.cs b/frameworks/genhttp-11-ioxide/Tests/Upload.cs new file mode 100644 index 000000000..ba8cf45fa --- /dev/null +++ b/frameworks/genhttp-11-ioxide/Tests/Upload.cs @@ -0,0 +1,37 @@ +using System.Buffers; + +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +namespace genhttp.Tests; + +public class Upload +{ + + [ResourceMethod(Method.Post)] + public async ValueTask Compute(Stream input) + { + var pool = ArrayPool.Shared; + + var buffer = pool.Rent(16384); + + try + { + long total = 0; + + var read = 0; + + while ((read = await input.ReadAsync(buffer)) > 0) + { + total += read; + } + + return total; + } + finally + { + pool.Return(buffer); + } + } + +} diff --git a/frameworks/genhttp-11-ioxide/genhttp.csproj b/frameworks/genhttp-11-ioxide/genhttp.csproj new file mode 100644 index 000000000..c08f2b18b --- /dev/null +++ b/frameworks/genhttp-11-ioxide/genhttp.csproj @@ -0,0 +1,32 @@ + + + + Exe + net11.0 + enable + enable + true + runtime-async=on + 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..7ecfae511 --- /dev/null +++ b/frameworks/genhttp-11-ioxide/meta.json @@ -0,0 +1,27 @@ +{ + "display_name": "genhttp-11-ioxide", + "language": "C#", + "type": "emerging", + "mode": "tuned", + "engine": "io_uring", + "description": "GenHTTP 11 on .NET 11 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). Responses go through GenHTTP's own routing + serialization; Postgres rides ioxide.pg (per-reactor ring-native pool) and json-tls rides ioxide.tls (kTLS TX) on :8081.", + "repo": "https://github.com/Kaliumhexacyanoferrat/GenHTTP/pull/860", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "json-comp", + "json-tls", + "static", + "upload", + "async-db", + "crud", + "api-4", + "api-16" + ], + "maintainers": [ + "MDA2AV" + ] +} diff --git a/frameworks/ioxide/nuget.config b/frameworks/ioxide/nuget.config new file mode 100644 index 000000000..efd109950 --- /dev/null +++ b/frameworks/ioxide/nuget.config @@ -0,0 +1,9 @@ + + + + + + + +