Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/clients/ioxide.file/ioxide.file.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.file</RootNamespace>

<PackageId>ioxide.file</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.httpclient/ioxide.httpclient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.httpclient</RootNamespace>

<PackageId>ioxide.httpclient</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>The ring-native HTTP client for the ioxide io_uring runtime: HTTP/1.1, HTTP/2 and HTTP/3 behind one API, with the protocol chosen per origin via Alt-Svc. One package - the h1 parser, a pure-C# HTTP/2 client on ioxide.http2's framing, the nghttp3 bridge, client-side TLS (SNI, ALPN and certificate verification) for https:// origins, and the negotiating client - sharing one set of message types. Every response resumes the awaiting handler inline on its own reactor thread.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.pg/ioxide.pg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.pg</RootNamespace>

<PackageId>ioxide.pg</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.redis/ioxide.redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.redis</RootNamespace>

<PackageId>ioxide.redis</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>Redis client for the ioxide io_uring runtime: pooled ring-native connections per reactor, full RESP2 protocol, a generic command API plus typed helpers (strings, keys, hashes, lists, sets, sorted sets, pub/sub, transactions, scripting), and pipelining. Inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/ioxide/ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide</RootNamespace>

<PackageId>ioxide</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>A shared-nothing io_uring runtime for .NET: one ring per reactor thread, inline completions, zero native dependencies. The engine - reactor, connection, and the IRingHost client seam. Includes TLS termination: the OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload, so handlers keep writing plaintext. TLS needs OpenSSL 3 and the Linux tls module; nothing else does, and neither is loaded unless you use it.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http2/ioxide.http2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http2</RootNamespace>

<PackageId>ioxide.http2</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>Pure-C# HTTP/2 for the ioxide io_uring runtime: framing, HPACK (static and dynamic tables, Huffman) and flow control, with zero native code. Serves h2c with prior knowledge and h2 over TLS by ALPN, buffered or streamed in either direction, and the same framing drives ioxide.httpclient's HTTP/2 client.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http3/ioxide.http3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http3</RootNamespace>

<PackageId>ioxide.http3</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>Pure C# HTTP/3 for the ioxide io_uring runtime: frame parsing, QPACK (static table + Huffman) and request dispatch with zero native dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, drop-in alternative to ioxide.nghttp3.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp2</RootNamespace>

<PackageId>ioxide.nghttp2</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/2 for the ioxide io_uring runtime: framing, HPACK and flow control from nghttp2, statically linked behind a small shim with no external dependencies beyond libc. Serves HTTP/2 over any TcpConnection - h2c with prior knowledge, or h2 over TLS via ALPN - and backs the HTTP/2 client in ioxide.httpclient from the same session code. nghttp2 is sans-I/O, so ioxide keeps the ring and the loop.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,44 @@ public async Task RunStreamedResponseAsync(Func<Nghttp3Request, Nghttp3ResponseW
/// back to ReadAsync between chunks would wait for a packet that only our own output will
/// provoke, so the response would stall after its first chunk.
/// </summary>
// True while DrainStreamed is on the stack. A writer that flushes from inside it may park and
// be resumed by the loop; one that flushes from anywhere else has to drive the drain itself,
// because no loop is running to do it.
private bool _inStreamedDrain;

private void DrainStreamed()
{
if (_inStreamedDrain)
{
return; // already draining; the loop below picks up whatever was just staged
}

_inStreamedDrain = true;
try
{
DrainStreamedCore();
}
finally
{
_inStreamedDrain = false;
}
}

/// <summary>
/// Drive egress for a writer that resumed OUTSIDE the read pass - after a file read, a database
/// call, an upstream response. Inside the pass this does nothing, because the drain loop is
/// already running and will pump what was just staged; that path stays exactly as it was, which
/// is what keeps a one-chunk response from paying a reactor round trip it does not need.
/// </summary>
internal void PumpIfOutsidePass()
{
if (!_inStreamedDrain && !_protocolFailed)
{
DrainStreamed();
}
}

private void DrainStreamedCore()
{
while (!_protocolFailed)
{
Expand Down Expand Up @@ -282,6 +319,16 @@ internal Task PumpAsync()
return Task.CompletedTask;
}

// Outside the drain loop there is nobody to release a pass waiter: the loop only runs while
// the peer is sending, and a peer waiting for our response sends nothing. Parking here is
// what made a handler that awaits anything - a file read, a query, an upstream - stall on
// its first chunk and hang on its second. Drive the drain instead and carry on.
if (!_inStreamedDrain)
{
DrainStreamed();
return Task.CompletedTask;
}

// NOT RunContinuationsAsynchronously: everything on a reactor resumes inline on the
// reactor thread, and a parked writer is no different. Completing this runs the handler
// to its next park right there, so the drain loop needs no scheduler round trip to let it
Expand Down
2 changes: 2 additions & 0 deletions src/protocols/ioxide.nghttp3/Http/Nghttp3ResponseWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public async ValueTask FlushAsync()
PromoteStagedChunk();

_connection.ResumeStreamedResponse(_streamId);
_connection.PumpIfOutsidePass();
}

/// <summary>
Expand Down Expand Up @@ -182,6 +183,7 @@ public async ValueTask CompleteAsync()
// nothing to keep the handler around for.
_completed = true;
_connection.ResumeStreamedResponse(_streamId);
_connection.PumpIfOutsidePass();
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp3</RootNamespace>

<PackageId>ioxide.nghttp3</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/3 layer for the ioxide io_uring runtime: nghttp3 (H3 + QPACK) bundled as a single self-contained native library with no external dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, no ioxide.ngtcp2 dependency.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.ngtcp2</RootNamespace>

<PackageId>ioxide.ngtcp2</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>QUIC engine for the ioxide io_uring runtime: ngtcp2 + picotls bundled as a single self-contained native library (only system dependency: libcrypto.so.3 / OpenSSL 3.x). Plugs into the reactor's QUIC transport via QuicConnection. Server side; engine bindings in progress.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.Kestrel</RootNamespace>

<PackageId>ioxide.Kestrel</PackageId>
<Version>0.4.177</Version>
<Version>0.4.178</Version>
<Authors>MDA2AV</Authors>
<Description>ASP.NET Core Kestrel transport backed by the ioxide io_uring runtime: one reactor (ring) per core, SO_REUSEPORT load-balanced, with Kestrel's HTTP request loop pinned to the reactor thread. Drop-in via UseIoxide().</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
42 changes: 42 additions & 0 deletions tests/Ioxide.Tests.E2E/Protocols/H3Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,48 @@ public static void Register(Runner runner)
Assert.Equal("got 600000", text);
});

runner.Test("h3: a streamed response whose handler awaits between chunks still arrives", () =>
{
// Every other streamed test writes its chunks in a tight loop, so the handler never
// leaves the pass that dispatched it and DrainStreamed resumes it inline. A REAL
// handler awaits something first - a file read, a query, an upstream - and comes back
// outside that pass. There the flush used to park on a pass waiter that only inbound
// packets release, while the peer sat waiting for the response that would have
// provoked them: first chunk stalled, second hung outright.
(string certPath, string keyPath) = TestCert.Ensure();
using var engine = new QuicEngine(certPath, keyPath, cidLength: 8);

(_, int udpPort) = TestServer.StartDatagram(
onDatagram: null,
quicFactory: engine.CreateFactory(),
quicHandle: static (_, conn) => new Nghttp3Connection(conn).RunStreamedResponseAsync(
static async (_, writer) =>
{
writer.WriteHeaders(new Nghttp3Response { Status = 200 });

for (int i = 0; i < 4; i++)
{
// The point of the test. Task.Yield is the cheapest way to leave the
// dispatch pass; a file read or a database call lands in the same place.
await Task.Yield();

"chunk"u8.CopyTo(writer.GetSpan(5));
writer.Advance(5);
await writer.FlushAsync();
}

await writer.CompleteAsync();
}));

using var client = new H3TestClient("127.0.0.1", udpPort);
client.Connect();
Assert.True(client.CompleteHandshake(timeoutMs: 5000), "handshake did not complete");

(int status, string body) = client.Get("/streamed", timeoutMs: 10_000);
Assert.Equal(200, status);
Assert.Equal("chunkchunkchunkchunk", body);
});

runner.Test("h3: buffered-async handler (whole body in req.Body, handler may await)", () =>
{
(string certPath, string keyPath) = TestCert.Ensure();
Expand Down
Loading