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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private static unsafe void CallbackBeginHeaders(void* user, int streamId)
// the request we have rather than replacing it and losing everything accumulated.
if (!connection._pending.ContainsKey(streamId))
{
connection._pending[streamId] = new PendingRequest { StreamId = streamId };
connection._pending[streamId] = connection.RentPending(streamId);
}
}
catch (Exception e)
Expand Down Expand Up @@ -108,7 +108,7 @@ private static unsafe void CallbackEndHeaders(void* user, int streamId)
connection._pending.TryGetValue(streamId, out PendingRequest? pending) && pending.BodyReader is null)
{
pending.BodyReader = new Nghttp2BodyReader(connection, streamId);
connection._readyThisPass.Add(pending);
connection._readyThisPass.Enqueue(pending);
}
}
catch (Exception e)
Expand Down Expand Up @@ -141,7 +141,7 @@ private static unsafe void CallbackData(void* user, int streamId, byte* data, nu
{
// Left pending, so a trailer later in this read cannot start a fresh request on the stream.
pending.Overflowed = true;
connection._readyThisPass.Add(pending);
connection._readyThisPass.Enqueue(pending);
return;
}
pending.AppendBody(new ReadOnlySpan<byte>(data, (int)dataLength));
Expand Down Expand Up @@ -170,7 +170,7 @@ private static unsafe void CallbackEndStream(void* user, int streamId)
}
else if (!pending.Overflowed)
{
connection._readyThisPass.Add(pending);
connection._readyThisPass.Enqueue(pending);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,6 @@ private unsafe void Feed(in ReadOnlySequence<byte> buffer)
}
}

// Snapshot and clear first: a handler that awaits can let another read pass land more
// requests here, and the list must not be mutated while it is walked.
private PendingRequest[] TakeReady()
{
if (_readyThisPass.Count == 0)
{
return [];
}

PendingRequest[] ready = _readyThisPass.ToArray();
_readyThisPass.Clear();
return ready;
}

// A request that outgrew MaxRequestBytes is reset instead of dispatched, whichever the response mode.
private bool RefuseOverflowed(PendingRequest pending)
{
Expand All @@ -160,7 +146,7 @@ private bool RefuseOverflowed(PendingRequest pending)

private void DispatchBuffered(Func<Nghttp2Request, ValueTask<Nghttp2Response>> handler)
{
foreach (PendingRequest pending in TakeReady())
while (_readyThisPass.TryDequeue(out PendingRequest? pending))
{
if (RefuseOverflowed(pending))
{
Expand Down Expand Up @@ -194,7 +180,7 @@ private void DispatchBuffered(Func<Nghttp2Request, ValueTask<Nghttp2Response>> h
{
// The arena backs the request's memories, so it can only go back once the
// handler has returned and the response is submitted (which copies).
pending.Dispose();
Retire(pending);
}
continue;
}
Expand Down Expand Up @@ -234,7 +220,7 @@ private async Task CompleteAsync(ValueTask<Nghttp2Response> inFlight, PendingReq
}
finally
{
pending.Dispose();
Retire(pending);
await FlushEgressAsync();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public Task RunAsync(Func<Nghttp2Request, Nghttp2BodyReader, Nghttp2ResponseWrit
/// </summary>
private void DispatchStreamed(Func<Nghttp2Request, Nghttp2ResponseWriter, ValueTask> handler)
{
foreach (PendingRequest pending in TakeReady())
while (_readyThisPass.TryDequeue(out PendingRequest? pending))
{
if (RefuseOverflowed(pending))
{
Expand Down Expand Up @@ -92,7 +92,7 @@ private async Task ServeStreamedAsync(Func<Nghttp2Request, Nghttp2ResponseWriter
// Nothing awaits this task, so the retirement the dispatch loop would have done has to
// happen here - the arena backs the request's memories and the writer holds a pooled
// staging buffer.
pending.Dispose();
Retire(pending);
_writers.Remove(writer.StreamId);
writer.Release();
_writerPool.Push(writer);
Expand Down
62 changes: 50 additions & 12 deletions src/protocols/ioxide.nghttp2/Connection/Nghttp2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,31 @@ public sealed partial class Nghttp2Connection : IDisposable

// Requests whose streams ended during the current ih2_read. The callbacks only record them;
// the loop dispatches after mem_recv returns.
private readonly List<PendingRequest> _readyThisPass = [];
private readonly Queue<PendingRequest> _readyThisPass = new();

// Retired buffered requests, kept for the next stream; capped, so a burst leaves little on an idle connection.
private readonly Stack<PendingRequest> _pendingPool = new();
private const int MaxPooledRequests = 32;

private PendingRequest RentPending(int streamId)
{
PendingRequest pending = _pendingPool.TryPop(out PendingRequest? pooled) ? pooled : new PendingRequest();
pending.StreamId = streamId;
return pending;
}

/// <summary>Done with a dispatched request once its handler has returned; a streamed body's stays in _pending.</summary>
private void Retire(PendingRequest pending)
{
pending.Dispose();

// Only the reactor rents, and a field list a peer's header lines grew would stay that size.
if (pending.BodyReader is null && _pendingPool.Count < MaxPooledRequests && !OffReactor && pending.IsSmall)
{
pending.Reset();
_pendingPool.Push(pending);
}
}

/// <summary>
/// Serve over an already-chosen transport. The pipe is the caller's to dispose - it may be a
Expand Down Expand Up @@ -164,6 +188,7 @@ private sealed class PendingRequest : IDisposable
private int _used;
private readonly List<(int NameOffset, int NameLength, int ValueOffset, int ValueLength)> _fields = [];
private (int Offset, int Length) _body = (0, 0);
private readonly Nghttp2Request _request = new();

public int StreamId;

Expand Down Expand Up @@ -229,19 +254,17 @@ public void AppendBody(ReadOnlySpan<byte> data)
}
}

/// <summary>Materialize the public request. Valid until <see cref="Dispose"/>.</summary>
/// <summary>Materialize the public request, this one's own and reused. Valid until <see cref="Dispose"/>.</summary>
public Nghttp2Request Freeze()
{
var request = new Nghttp2Request
{
StreamId = StreamId,
Method = Slice(Method),
Path = Slice(Path),
Scheme = Slice(Scheme),
Authority = Slice(Authority),
Body = Slice(_body),
BodyReader = BodyReader,
};
Nghttp2Request request = _request;
request.StreamId = StreamId;
request.Method = Slice(Method);
request.Path = Slice(Path);
request.Scheme = Slice(Scheme);
request.Authority = Slice(Authority);
request.Body = Slice(_body);
request.BodyReader = BodyReader;

foreach ((int nameOffset, int nameLength, int valueOffset, int valueLength) in _fields)
{
Expand All @@ -267,5 +290,20 @@ public void Dispose()
}
_used = 0;
}

/// <summary>Fresh for the next stream that rents it, after <see cref="Dispose"/>.</summary>
public void Reset()
{
StreamId = 0;
Overflowed = false;
Method = Path = Scheme = Authority = _body = default;

// Its request still points into the arena Dispose returned, which the pool must not pin.
_request.Headers.Clear();
_request.Method = _request.Path = _request.Scheme = _request.Authority = _request.Body = default;
_request.BodyReader = null;
}

public bool IsSmall => _fields.Capacity <= 64;
}
}
3 changes: 2 additions & 1 deletion src/protocols/ioxide.nghttp2/Http/Nghttp2Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace ioxide.nghttp2;
/// most handlers only ever compare a path or read a header.
///
/// The memories point into an arena owned by the connection and are valid for the duration of the
/// handler call. Copy anything you keep past it.
/// handler call. Copy anything you keep past it: a request whose body arrived whole is itself reused
/// for a later stream on the same connection once its handler is done.
/// </summary>
public sealed class Nghttp2Request
{
Expand Down
Loading
Loading