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
25 changes: 18 additions & 7 deletions ioxide.Kestrel/HopDuplexPipe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ internal sealed class HopDuplexPipe : IDuplexPipe, IAsyncDisposable
private Task _sendPump = Task.CompletedTask;
private int _started;

[System.Runtime.InteropServices.DllImport("libc", EntryPoint = "shutdown")]
private static extern int Shutdown(int sockfd, int how);
private const int ShutWr = 1; // SHUT_WR

public PipeReader Input => _inbound.Reader;
public PipeWriter Output => _outbound.Writer;

Expand Down Expand Up @@ -139,14 +143,21 @@ private async Task SendPumpAsync()

public async ValueTask DisposeAsync()
{
// Kestrel has completed its ends; wake the pumps and unwind. MarkClosed wakes a recv parked in
// conn.ReadAsync — schedule it ON the reactor so the recv continuation (which touches reactor-owned
// recv state) runs there, not on Kestrel's dispose thread. The pipe cancels resume via the pipes'
// reactor reader/writer schedulers, so they're reactor-safe too.
// Quiesce the send side BEFORE closing. Wake the send pump if it's parked on the output reader and
// await it, so any in-flight SEND completes normally and the full response is flushed. Draining here
// — rather than concurrently with MarkClosed — is what makes the response reliably reach the client
// and avoids completing a live flush twice (MarkClosed racing the SEND CQE's CompleteFlush).
_outbound.Reader.CancelPendingRead();
try { await _sendPump.ConfigureAwait(false); } catch { }

// Half-close the write side so EOF-delimited clients (Connection: close / upgrade) see the end of
// the response — ioxide's refcounted teardown does not FIN a server-initiated close on its own.
Shutdown(_conn.ClientFd, ShutWr);

// Now wake and unwind the recv side. MarkClosed wakes a recv parked in conn.ReadAsync — schedule it
// on the reactor so the recv continuation (reactor-owned state) runs there, not the dispose thread.
_reactor.ScheduleOnReactor(static c => ((Connection)c!).MarkClosed(), _conn);
_inbound.Writer.CancelPendingFlush();
_outbound.Reader.CancelPendingRead();
try { await _recvPump; } catch { }
try { await _sendPump; } catch { }
try { await _recvPump.ConfigureAwait(false); } catch { }
}
}
2 changes: 1 addition & 1 deletion 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.0.13</Version>
<Version>0.0.14</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
2 changes: 1 addition & 1 deletion 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.0.13</Version>
<Version>0.0.14</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 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.0.13</Version>
<Version>0.0.14</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 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.0.13</Version>
<Version>0.0.14</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 ioxide.tls/ioxide.tls.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.tls</RootNamespace>

<PackageId>ioxide.tls</PackageId>
<Version>0.0.13</Version>
<Version>0.0.14</Version>
<Authors>MDA2AV</Authors>
<Description>TLS for the ioxide io_uring runtime: OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload - handlers keep writing plaintext through the same connection API. Requires Linux kTLS (tls module) and OpenSSL 3.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
11 changes: 9 additions & 2 deletions ioxide/Connection/Connection.Write.Flush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,16 @@ internal void CompleteFlush()
WriteInFlight = 0;
ZcNotifPending = 0;
Volatile.Write(ref _flushInProgress, 0);
Interlocked.Exchange(ref _flushArmed, 0);

_flushSignal.SetResult(true);
// Guard against a double completion. During teardown MarkClosed() may have already disarmed and
// completed this flush (e.g. a Connection: close response whose SEND CQE lands after the close),
// which Resets/invalidates the value-task source. Only the call that actually disarms the flush
// signals — mirroring MarkClosed and the recv path's _armed check. Without this, the late CQE's
// SetResult throws InvalidOperationException on the reactor thread and crashes the process.
if (Interlocked.Exchange(ref _flushArmed, 0) == 1)
{
_flushSignal.SetResult(true);
}
}

#region IValueTaskSource
Expand Down
2 changes: 1 addition & 1 deletion 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.0.13</Version>
<Version>0.0.14</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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading