Skip to content

ioxide.Kestrel: on-reactor services — run ioxide.pg / ioxide.file on the reactor from endpoints#88

Merged
MDA2AV merged 1 commit into
mainfrom
feat/reactor-services
Jun 21, 2026
Merged

ioxide.Kestrel: on-reactor services — run ioxide.pg / ioxide.file on the reactor from endpoints#88
MDA2AV merged 1 commit into
mainfrom
feat/reactor-services

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Lets a Kestrel endpoint run ring-native I/O (ioxide.pg, ioxide.file) on the connection's reactor, so DB and file I/O stay thread-per-core. Without it, Npgsql / blocking file I/O hop to the ThreadPool (Npgsql opens its own connections and the request continuation resumes off-reactor), losing the io_uring advantage for DB-backed endpoints.

Bumps packages to 0.0.16. No changes to ioxide / ioxide.pg / ioxide.file / ioxide.tls — only their existing public APIs are used, so GenHTTP and other consumers are unaffected.

API

  • IoxideTransportOptions.OnReactorStart: per-reactor startup hook — open ring-native clients here (PgPool.Start(r, …), AssetReader.CreatePool(r, …)).
  • HttpContext.OnReactor(work): resolves the connection's reactor (IReactorFeature) and runs work on it.
builder.WebHost.UseIoxide(o => o.OnReactorStart = r => PgPool.Start(r, pgOptions));

app.MapGet("/db", (HttpContext ctx) => ctx.OnReactor(async r => {
    var pool = r.GetService<PgPool>();
    var rows = new List<Item>();
    await pool.QueryAsync(sql, args, row => rows.Add(Map(row)));   // runs on the reactor
    return rows;
}));

How it stays on-reactor

  • Warm / keep-alive requests: the endpoint already runs on the reactor → OnReactor runs work inline (ioxide's inline resume preserved).
  • Cold (first request of a connection): Kestrel dispatches new connections to the ThreadPool, so the endpoint may run there → OnReactor marshals work onto the reactor via ScheduleOnReactor. The query/read always runs on the reactor.
  • ReactorPinReader additionally pins the first read of each connection onto the reactor, so header parse stays thread-per-core too.

Verified

20/20 fresh connections ran a Postgres query — query thread = reactor every time (marshaled when cold, inline when warm), zero errors.

…rel endpoints

Lets an endpoint run ioxide.pg / ioxide.file on the connection's reactor, so DB and file I/O stay thread-per-core (Npgsql / blocking file I/O would hop to the ThreadPool instead).

- IoxideTransportOptions.OnReactorStart: per-reactor startup hook (open ring-native clients here: PgPool.Start, AssetReader.CreatePool, ...).
- IReactorFeature on the ConnectionContext + HttpContext.OnReactor(work) helper: resolves the connection's reactor and runs the work on it — inline when the endpoint is already on that reactor (warm/keep-alive), marshaled via ScheduleOnReactor when not (the first request of a connection, which Kestrel dispatches to the ThreadPool). The ring I/O always runs on the reactor.
- IoxideReactor current-reactor seam (ThreadStatic, bound in OnStart) + ReactorPinReader: pins the first read of each connection onto the reactor.
- No changes to ioxide / ioxide.pg / ioxide.file / ioxide.tls — only their existing public APIs are used.
- Bump packages to 0.0.16.
@MDA2AV MDA2AV merged commit 7823599 into main Jun 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant