| File: Internal\Http2\FlowControl\InputFlowControl.cs | Web Access |
| Project: src\aspnetcore\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; /// <summary> /// Represents the in-bound flow control state of a stream or connection. /// </summary> /// <remarks> /// Owns a <see cref="FlowControl"/> that it uses to track the present window size. /// <para/> /// <see cref="Http2Connection"/> owns an instance for the connection-level flow control. /// <see cref="StreamInputFlowControl"/> owns an instance for the stream-level flow control. /// <para/> /// Reusable after calling <see cref="Reset"/>. /// </remarks> /// <seealso href="https://datatracker.ietf.org/doc/html/rfc9113#name-flow-control"/> internal sealed class InputFlowControl { private readonly int _initialWindowSize; private readonly int _minWindowSizeIncrement; private FlowControl _flow; private int _pendingUpdateSize; private bool _windowUpdatesDisabled; private readonly Lock _flowLock = new(); public InputFlowControl(uint initialWindowSize, uint minWindowSizeIncrement) { Debug.Assert(initialWindowSize >= minWindowSizeIncrement, "minWindowSizeIncrement is greater than the window size."); _flow = new FlowControl(initialWindowSize); _initialWindowSize = (int)initialWindowSize; _minWindowSizeIncrement = (int)minWindowSizeIncrement; } public bool IsAvailabilityLow => _flow.Available < _minWindowSizeIncrement; public void Reset() { _flow = new FlowControl((uint)_initialWindowSize); _pendingUpdateSize = 0; _windowUpdatesDisabled = false; } public bool TryAdvance(int bytes) { lock (_flowLock) { // Even if the stream is aborted, the client should never send more data than was available in the // flow-control window at the time of the abort. if (bytes > _flow.Available) { throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorFlowControlWindowExceeded, Http2ErrorCode.FLOW_CONTROL_ERROR, ConnectionEndReason.FlowControlWindowExceeded); } if (_flow.IsAborted) { // This data won't be read by the app, so tell the caller to count the data as already consumed. return false; } _flow.Advance(bytes); return true; } } public bool TryUpdateWindow(int bytes, out int updateSize) { lock (_flowLock) { updateSize = 0; if (_flow.IsAborted) { // All data received by stream has already been returned to the connection window. return false; } if (!_flow.TryUpdateWindow(bytes)) { // We only try to update the window back to its initial size after the app consumes data. // It shouldn't be possible for the window size to ever exceed Http2PeerSettings.MaxWindowSize. Debug.Assert(false, $"{nameof(TryUpdateWindow)} attempted to grow window past max size."); } if (_windowUpdatesDisabled) { // Continue returning space to the connection window. The end of the stream has already // been received, so don't send window updates for the stream window. return true; } var potentialUpdateSize = _pendingUpdateSize + bytes; if (potentialUpdateSize > _minWindowSizeIncrement) { _pendingUpdateSize = 0; updateSize = potentialUpdateSize; } else { _pendingUpdateSize = potentialUpdateSize; } return true; } } public void StopWindowUpdates() { _windowUpdatesDisabled = true; } public int Abort() { lock (_flowLock) { if (_flow.IsAborted) { return 0; } _flow.Abort(); // Tell caller to return connection window space consumed by this stream. Even if window updates have // been disabled at the stream level, connection-level window updates may still be necessary. return _initialWindowSize - _flow.Available; } } }