Skip to content

Commit 28dc9bf

Browse files
authored
HTTP2: Write data and trailers under one lock (#24822)
1 parent c92eaa2 commit 28dc9bf

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame
198198
}
199199
}
200200

201-
public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
201+
public ValueTask<FlushResult> WriteResponseTrailersAsync(int streamId, HttpResponseTrailers headers)
202202
{
203203
lock (_writeLock)
204204
{
@@ -256,6 +256,9 @@ private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
256256

257257
public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool endStream, bool firstWrite, bool forceFlush)
258258
{
259+
// Logic in this method is replicated in WriteDataAndTrailersAsync.
260+
// Changes here may need to be mirrored in WriteDataAndTrailersAsync.
261+
259262
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
260263
var dataLength = data.Length;
261264

@@ -286,6 +289,43 @@ public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowContr
286289
}
287290
}
288291

292+
public ValueTask<FlushResult> WriteDataAndTrailersAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool firstWrite, HttpResponseTrailers headers)
293+
{
294+
// This method combines WriteDataAsync and WriteResponseTrailers.
295+
// Changes here may need to be mirrored in WriteDataAsync.
296+
297+
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
298+
var dataLength = data.Length;
299+
300+
lock (_writeLock)
301+
{
302+
if (_completed || flowControl.IsAborted)
303+
{
304+
return default;
305+
}
306+
307+
// Zero-length data frames are allowed to be sent immediately even if there is no space available in the flow control window.
308+
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
309+
if (dataLength != 0 && dataLength > flowControl.Available)
310+
{
311+
return WriteDataAndTrailersAsyncCore(this, streamId, flowControl, data, dataLength, firstWrite, headers);
312+
}
313+
314+
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
315+
flowControl.Advance((int)dataLength);
316+
WriteDataUnsynchronized(streamId, data, dataLength, endStream: false);
317+
318+
return WriteResponseTrailersAsync(streamId, headers);
319+
}
320+
321+
static async ValueTask<FlushResult> WriteDataAndTrailersAsyncCore(Http2FrameWriter writer, int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool firstWrite, HttpResponseTrailers headers)
322+
{
323+
await writer.WriteDataAsync(streamId, flowControl, data, dataLength, endStream: false, firstWrite);
324+
325+
return await writer.WriteResponseTrailersAsync(streamId, headers);
326+
}
327+
}
328+
289329
/* Padding is not implemented
290330
+---------------+
291331
|Pad Length? (8)|

src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -423,16 +423,19 @@ private async ValueTask ProcessDataWrites()
423423
{
424424
// Output is ending and there are trailers to write
425425
// Write any remaining content then write trailers
426-
if (readResult.Buffer.Length > 0)
427-
{
428-
// Only flush if required (i.e. content length exceeds flow control availability)
429-
// Writing remaining content without flushing allows content and trailers to be sent in the same packet
430-
await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream: false, firstWrite, forceFlush: false);
431-
}
432426

433427
_stream.ResponseTrailers.SetReadOnly();
434428
_stream.DecrementActiveClientStreamCount();
435-
flushResult = await _frameWriter.WriteResponseTrailers(StreamId, _stream.ResponseTrailers);
429+
430+
if (readResult.Buffer.Length > 0)
431+
{
432+
// It is faster to write data and trailers together. Locking once reduces lock contention.
433+
flushResult = await _frameWriter.WriteDataAndTrailersAsync(StreamId, _flowControl, readResult.Buffer, firstWrite, _stream.ResponseTrailers);
434+
}
435+
else
436+
{
437+
flushResult = await _frameWriter.WriteResponseTrailersAsync(StreamId, _stream.ResponseTrailers);
438+
}
436439
}
437440
else if (readResult.IsCompleted && _streamEnded)
438441
{

0 commit comments

Comments
 (0)