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
1 change: 1 addition & 0 deletions src/protocols/ioxide.http3/Http3Connection.Streamed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ private async Task ServeAsync(Func<Http3Request, Http3ResponseWriter, ValueTask>
{
_writers.Remove(writer.StreamId);
_writerPool.Push(writer);
Recycle(rs);
}
}

Expand Down
41 changes: 39 additions & 2 deletions src/protocols/ioxide.http3/Http3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public sealed partial class Http3Connection
// client uni (id % 4 == 2) = control / QPACK / push streams.
private readonly Dictionary<long, ReqStream> _requests = new();
private readonly Dictionary<long, UniStream> _unis = new();

// Request streams whose body arrived whole, kept for the next stream; capped, so a burst leaves little behind.
private readonly Stack<ReqStream> _reqPool = new();
private const int MaxPooledRequests = 32;
private readonly List<long> _ready = [];
private readonly List<Http3BodyReader> _bodyWakes = [];

Expand Down Expand Up @@ -93,6 +97,20 @@ private sealed class ReqStream
// RetireBody unpaced the stream, because bytes delivered while it was still paced can be
// in the ring then - over-crediting the ones that arrived after is harmless.
public bool Paced;

/// <summary>Fresh for the next stream, after <see cref="ReleaseParseBuffers"/>.</summary>
public void Reset()
{
Request.Reset();
Sink = null;
State = ParseState.FrameHeader;
CarryLen = 0;
Remaining = 0;
HeadersLen = 0;
HeadersDone = false;
Finished = false;
Paced = false;
}
}

private sealed class UniStream
Expand Down Expand Up @@ -294,7 +312,7 @@ private void Feed(in QuicRecvRing.Delivery item)
_quicConnection.ResetStream(item.StreamId, H3RequestIncomplete);
return;
}
rs = new ReqStream();
rs = _reqPool.TryPop(out ReqStream? pooled) ? pooled : new ReqStream();
rs.Request.StreamId = item.StreamId;
_requests[item.StreamId] = rs;
}
Expand Down Expand Up @@ -657,6 +675,7 @@ private void DispatchReady(Func<Http3Request, Http3Response> handler)
}

Submit(rs.Request.StreamId, resp);
Recycle(rs);
}
_ready.Clear();
}
Expand Down Expand Up @@ -687,13 +706,16 @@ private void DispatchReadyAsync(Func<Http3Request, ValueTask<Http3Response>> han
Console.Error.WriteLine($"[ioxide.http3] request handler faulted: {e.GetBaseException().Message}");
RetireBody(rs);
Submit(sid, new Http3Response { Status = 500 });
Recycle(rs);
continue;
}

if (pending.IsCompletedSuccessfully)
{
RetireBody(rs);
Submit(sid, pending.Result);
Http3Response response = pending.Result; // once: a ValueTask is consumed by reading it
Submit(sid, response);
Recycle(rs);
}
else
{
Expand Down Expand Up @@ -723,6 +745,21 @@ private async Task CompleteBufferedAsync(ValueTask<Http3Response> pending, ReqSt
return;
}
Submit(rs.Request.StreamId, resp);
Recycle(rs);
}

/// <summary>Back for the next stream once the handler is done and the response submitted.</summary>
private void Recycle(ReqStream rs)
{
// A streamed body's request stays in _requests until its stream ends; only the reactor rents.
if (_streamBodies || _reqPool.Count >= MaxPooledRequests || OffReactor || !rs.Request.IsSmall)
{
return;
}

ReleaseParseBuffers(rs);
rs.Reset();
_reqPool.Push(rs);
}

// The handler is done with the body: credit what it left unread and let the engine credit the
Expand Down
29 changes: 28 additions & 1 deletion src/protocols/ioxide.http3/Http3Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace ioxide.http3;
/// <summary>
/// One HTTP/3 request - post-QPACK BYTES throughout, mirroring the ioxide.nghttp3 surface: route
/// by byte compare (<c>req.Path.Span.SequenceEqual("/x"u8)</c>) or decode at the edge. Every
/// memory slices a per-request buffer valid only until the handler returns.
/// memory slices a per-request buffer valid only until the handler returns, and a request whose
/// body arrived whole is itself reused for a later stream on the same connection after that.
/// </summary>
public sealed class Http3Request
{
Expand Down Expand Up @@ -103,11 +104,37 @@ internal void Freeze()
}

private ReadOnlyMemory<byte> Slice((int Off, int Len) r) => r.Len < 0 ? default : Arena.AsMemory(r.Off, r.Len);

// Not above the inline-body limit, so a response body that waits for send capacity never points into a kept arena.
internal const int MaxPooledArena = 4 * 1024;

// Grew nothing a pooled request should keep: lists past this were a peer's thousands of header lines.
internal bool IsSmall => Headers.Capacity <= 64 && HeaderRanges.Capacity <= 64;

/// <summary>Fresh for the next stream, keeping the arena and both lists' capacity.</summary>
internal void Reset()
{
StreamId = 0;
Method = Path = Scheme = Authority = Body = default;
Headers.Clear();
BodyReader = null;

if (Arena.Length > MaxPooledArena)
{
Arena = [];
}
ArenaUsed = 0;
MethodR = PathR = SchemeR = AuthorityR = (0, -1);
HeaderRanges.Clear();
BodyBuffer = null;
ContentLengthLeft = -1;
}
}

/// <summary>
/// One HTTP/3 response: status, headers, in-memory body - bytes throughout. Everything is encoded
/// and handed to the QUIC engine synchronously at submit, so the memories can be pooled or static.
/// A response kept past its handler has to own them: a request's are reused for a later stream.
/// </summary>
public sealed class Http3Response
{
Expand Down
1 change: 1 addition & 0 deletions tests/Ioxide.Tests.E2E/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private static int Main()
StreamedPeerGoneTests.Register(runner);
QuicStreamingCreditTests.Register(runner);
Http3BodyTests.Register(runner);
Http3RequestReuseTests.Register(runner);
H3BodyModeTests.Register(runner);
H3BufferedFaultTests.Register(runner);
H3BodyLimitTests.Register(runner);
Expand Down
Loading
Loading