Implement max connection limits

- Added new options to allow configuring the maximum number of concurrent connections and upgraded connections.
- `KestrelServerLimits.MaxConcurrentConnections` defaults unlimited.
- `KestrelServerLimits.MaxConcurrentUpgradedConnections` defaults to unlimited.
- Calls to IHttpUpgradeFeature.UpgradeAsync() will throw when the MaxConcurrentUpgradedConnections limit has been reached.
- Kestrel will close new connections without response when MaxConcurrentConnections is reached.
This commit is contained in:
Nate McMaster 2017-05-26 12:26:05 -07:00
parent 009759c7f6
commit c343628926
33 changed files with 888 additions and 122 deletions

8
.vscode/launch.json vendored
View File

@ -7,6 +7,12 @@
"request": "attach",
"processId": "${command:pickProcess}"
},
{
"name": "Attach: .NET Framework",
"type": "clr",
"request": "attach",
"processId": "${command:pickProcess}"
},
{
"name": "Debug: SampleApp",
"type": "coreclr",
@ -62,7 +68,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "Compile: CodeGenerator",
"program": "${workspaceRoot}/tools/CodeGenerator/bin/Debug/netcoreapp1.1/CodeGenerator.dll",
"program": "${workspaceRoot}/tools/CodeGenerator/bin/Debug/netcoreapp2.0/CodeGenerator.dll",
"args": [],
"cwd": "${workspaceRoot}",
"console": "internalConsole",

17
.vscode/tasks.json vendored
View File

@ -76,6 +76,23 @@
"cwd": "${workspaceRoot}/tools/CodeGenerator/"
}
},
{
"taskName": "Run: resx generation",
"suppressTaskName": true,
"command": "build.cmd",
"args": [
"/t:resx"
],
"options": {
"cwd": "${workspaceRoot}"
},
"osx": {
"command": "./build.sh"
},
"linux": {
"command": "./build.sh"
}
},
{
"taskName": "Run: Benchmarks",
"args": [

View File

@ -228,14 +228,17 @@
<data name="InvalidContentLength_InvalidNumber" xml:space="preserve">
<value>Invalid Content-Length: "{value}". Value must be a positive integral number.</value>
</data>
<data name="NonNegativeNullableIntRequired" xml:space="preserve">
<value>Value must be null or a non-negative integer.</value>
<data name="NonNegativeNumberOrNullRequired" xml:space="preserve">
<value>Value must be null or a non-negative number.</value>
</data>
<data name="PositiveIntRequired" xml:space="preserve">
<value>Value must be a positive integer.</value>
<data name="NonNegativeNumberRequired" xml:space="preserve">
<value>Value must be a non-negative number.</value>
</data>
<data name="PositiveNullableIntRequired" xml:space="preserve">
<value>Value must be null or a positive integer.</value>
<data name="PositiveNumberRequired" xml:space="preserve">
<value>Value must be a positive number.</value>
</data>
<data name="PositiveNumberOrNullRequired" xml:space="preserve">
<value>Value must be null or a positive number.</value>
</data>
<data name="UnixSocketPathMustBeAbsolute" xml:space="preserve">
<value>Unix socket path must be absolute.</value>
@ -309,4 +312,10 @@
<data name="CannotUpgradeNonUpgradableRequest" xml:space="preserve">
<value>Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded.</value>
</data>
</root>
<data name="UpgradedConnectionLimitReached" xml:space="preserve">
<value>Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.</value>
</data>
<data name="UpgradeCannotBeCalledMultipleTimes" xml:space="preserve">
<value>IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.</value>
</data>
</root>

View File

@ -33,6 +33,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
var connectionId = CorrelationIdGenerator.GetNextId();
var frameConnectionId = Interlocked.Increment(ref _lastFrameConnectionId);
if (!_serviceContext.ConnectionManager.NormalConnectionCount.TryLockOne())
{
var goAway = new RejectionConnection(inputPipe, outputPipe, connectionId, _serviceContext);
goAway.Reject();
return goAway;
}
var connection = new FrameConnection(new FrameConnectionContext
{
ConnectionId = connectionId,

View File

@ -126,6 +126,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context.ServiceContext.ConnectionManager.RemoveConnection(_context.FrameConnectionId);
DisposeAdaptedConnections();
if (_frame.WasUpgraded)
{
_context.ServiceContext.ConnectionManager.UpgradedConnectionCount.ReleaseOne();
}
else
{
_context.ServiceContext.ConnectionManager.NormalConnectionCount.ReleaseOne();
}
Log.ConnectionStop(ConnectionId);
KestrelEventSource.Log.ConnectionStop(this);
}

View File

@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
bool IHttpResponseFeature.HasStarted => HasResponseStarted;
bool IHttpUpgradeFeature.IsUpgradableRequest => _upgrade;
bool IHttpUpgradeFeature.IsUpgradableRequest => _upgradeAvailable;
bool IFeatureCollection.IsReadOnly => false;
@ -235,6 +235,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest);
}
if (_wasUpgraded)
{
throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes);
}
if (!ServiceContext.ConnectionManager.UpgradedConnectionCount.TryLockOne())
{
throw new InvalidOperationException(CoreStrings.UpgradedConnectionLimitReached);
}
_wasUpgraded = true;
ServiceContext.ConnectionManager.NormalConnectionCount.ReleaseOne();
StatusCode = StatusCodes.Status101SwitchingProtocols;
ReasonPhrase = "Switching Protocols";
ResponseHeaders["Connection"] = "Upgrade";

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
private const string EmptyPath = "/";
private const string Asterisk = "*";
@ -61,7 +61,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
protected bool _upgrade;
protected bool _upgradeAvailable;
private volatile bool _wasUpgraded;
private bool _canHaveBody;
private bool _autoChunk;
protected Exception _applicationException;
@ -138,6 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public bool WasUpgraded => _wasUpgraded;
public IPAddress RemoteIpAddress { get; set; }
public int RemotePort { get; set; }
public IPAddress LocalIpAddress { get; set; }
@ -208,10 +210,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private int _statusCode;
public int StatusCode
{
get
{
return _statusCode;
}
get => _statusCode;
set
{
if (HasResponseStarted)
@ -227,10 +226,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public string ReasonPhrase
{
get
{
return _reasonPhrase;
}
get => _reasonPhrase;
set
{
if (HasResponseStarted)
@ -1038,6 +1035,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
responseHeaders.ContentLength = 0;
if (ServerOptions.AddServerHeader)

View File

@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this);
_keepAlive = messageBody.RequestKeepAlive;
_upgrade = messageBody.RequestUpgrade;
_upgradeAvailable = messageBody.RequestUpgrade;
InitializeStreams(messageBody);
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// An upgraded request has no defined request body length.
// Cancel any pending read so the read loop ends.
if (_upgrade)
if (_upgradeAvailable)
{
Input.CancelPendingRead();
}

View File

@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
if (bufferSize <= 0)
{
throw new ArgumentException(CoreStrings.PositiveIntRequired, nameof(bufferSize));
throw new ArgumentException(CoreStrings.PositiveNumberRequired, nameof(bufferSize));
}
var task = ValidateState(cancellationToken);

View File

@ -11,11 +11,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private readonly ConcurrentDictionary<long, FrameConnectionReference> _connectionReferences = new ConcurrentDictionary<long, FrameConnectionReference>();
private readonly IKestrelTrace _trace;
public FrameConnectionManager(IKestrelTrace trace)
public FrameConnectionManager(IKestrelTrace trace, long? normalConnectionLimit, long? upgradedConnectionLimit)
: this(trace, GetCounter(normalConnectionLimit), GetCounter(upgradedConnectionLimit))
{
}
public FrameConnectionManager(IKestrelTrace trace, ResourceCounter normalConnections, ResourceCounter upgradedConnections)
{
NormalConnectionCount = normalConnections;
UpgradedConnectionCount = upgradedConnections;
_trace = trace;
}
/// <summary>
/// TCP connections processed by Kestrel.
/// </summary>
public ResourceCounter NormalConnectionCount { get; }
/// <summary>
/// Connections that have been switched to a different protocol.
/// </summary>
public ResourceCounter UpgradedConnectionCount { get; }
public void AddConnection(long id, FrameConnection connection)
{
if (!_connectionReferences.TryAdd(id, new FrameConnectionReference(connection)))
@ -52,5 +69,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// If both conditions are false, the connection was removed during the heartbeat.
}
}
private static ResourceCounter GetCounter(long? number)
=> number.HasValue
? ResourceCounter.Quota(number.Value)
: ResourceCounter.Unlimited;
}
}

View File

@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void ConnectionResume(string connectionId);
void ConnectionRejected(string connectionId);
void ConnectionKeepAlive(string connectionId);
void ConnectionDisconnect(string connectionId);

View File

@ -9,4 +9,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void ResetTimeout(long ticks, TimeoutAction timeoutAction);
void CancelTimeout();
}
}
}

View File

@ -68,6 +68,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
WriteEvent(2, connectionId);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Event(5, Level = EventLevel.Verbose)]
public void ConnectionRejected(string connectionId)
{
if (IsEnabled())
{
WriteEvent(5, connectionId);
}
}
[NonEvent]
public void RequestStart(Frame frame)
{

View File

@ -54,6 +54,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action<ILogger, string, Exception> _applicationNeverCompleted =
LoggerMessage.Define<string>(LogLevel.Critical, 23, @"Connection id ""{ConnectionId}"" application never completed");
private static readonly Action<ILogger, string, Exception> _connectionRejected =
LoggerMessage.Define<string>(LogLevel.Warning, 24, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.");
protected readonly ILogger _logger;
public KestrelTrace(ILogger logger)
@ -86,6 +89,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_connectionKeepAlive(_logger, connectionId, null);
}
public void ConnectionRejected(string connectionId)
{
_connectionRejected(_logger, connectionId, null);
}
public virtual void ConnectionDisconnect(string connectionId)
{
_connectionDisconnect(_logger, connectionId, null);
@ -138,4 +146,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public virtual IDisposable BeginScope<TState>(TState state) => _logger.BeginScope(state);
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public abstract class ResourceCounter
{
public abstract bool TryLockOne();
public abstract void ReleaseOne();
public static ResourceCounter Unlimited { get; } = new UnlimitedCounter();
public static ResourceCounter Quota(long amount) => new FiniteCounter(amount);
private class UnlimitedCounter : ResourceCounter
{
public override bool TryLockOne() => true;
public override void ReleaseOne()
{
}
}
internal class FiniteCounter : ResourceCounter
{
private readonly long _max;
private long _count;
public FiniteCounter(long max)
{
if (max < 0)
{
throw new ArgumentOutOfRangeException(CoreStrings.NonNegativeNumberRequired);
}
_max = max;
}
public override bool TryLockOne()
{
var count = _count;
// Exit if count == MaxValue as incrementing would overflow.
while (count < _max && count != long.MaxValue)
{
var prev = Interlocked.CompareExchange(ref _count, count + 1, count);
if (prev == count)
{
return true;
}
// Another thread changed the count before us. Try again with the new counter value.
count = prev;
}
return false;
}
public override void ReleaseOne()
{
Interlocked.Decrement(ref _count);
Debug.Assert(_count >= 0, "Resource count is negative. More resources were released than were locked.");
}
// for testing
internal long Count
{
get => _count;
set => _count = value;
}
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
public class RejectionConnection : IConnectionContext
{
private readonly IKestrelTrace _log;
private readonly IPipe _input;
private readonly IPipe _output;
public RejectionConnection(IPipe input, IPipe output, string connectionId, ServiceContext serviceContext)
{
ConnectionId = connectionId;
_log = serviceContext.Log;
_input = input;
_output = output;
}
public string ConnectionId { get; }
public IPipeWriter Input => _input.Writer;
public IPipeReader Output => _output.Reader;
public void Reject()
{
KestrelEventSource.Log.ConnectionRejected(ConnectionId);
_log.ConnectionRejected(ConnectionId);
_input.Reader.Complete();
_output.Writer.Complete();
}
// TODO: Remove these (https://github.com/aspnet/KestrelHttpServer/issues/1772)
void IConnectionContext.OnConnectionClosed(Exception ex)
{
}
void IConnectionContext.Abort(Exception ex)
{
}
}
}

View File

@ -67,7 +67,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var serverOptions = options.Value ?? new KestrelServerOptions();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel");
var trace = new KestrelTrace(logger);
var connectionManager = new FrameConnectionManager(trace);
var connectionManager = new FrameConnectionManager(
trace,
serverOptions.Limits.MaxConcurrentConnections,
serverOptions.Limits.MaxConcurrentUpgradedConnections);
var systemClock = new SystemClock();
var dateHeaderValueManager = new DateHeaderValueManager(systemClock);

View File

@ -28,6 +28,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
// default to unlimited
private long? _maxConcurrentConnections = null;
private long? _maxConcurrentUpgradedConnections = null;
/// <summary>
/// Gets or sets the maximum size of the response buffer before write
/// calls begin to block or return tasks that don't complete until the
@ -41,15 +45,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public long? MaxResponseBufferSize
{
get
{
return _maxResponseBufferSize;
}
get => _maxResponseBufferSize;
set
{
if (value.HasValue && value.Value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNullableIntRequired);
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
}
_maxResponseBufferSize = value;
}
@ -64,15 +65,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public long? MaxRequestBufferSize
{
get
{
return _maxRequestBufferSize;
}
get => _maxRequestBufferSize;
set
{
if (value.HasValue && value.Value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNullableIntRequired);
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberOrNullRequired);
}
_maxRequestBufferSize = value;
}
@ -86,15 +84,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public int MaxRequestLineSize
{
get
{
return _maxRequestLineSize;
}
get => _maxRequestLineSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired);
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
}
_maxRequestLineSize = value;
}
@ -108,15 +103,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public int MaxRequestHeadersTotalSize
{
get
{
return _maxRequestHeadersTotalSize;
}
get => _maxRequestHeadersTotalSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired);
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
}
_maxRequestHeadersTotalSize = value;
}
@ -130,15 +122,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public int MaxRequestHeaderCount
{
get
{
return _maxRequestHeaderCount;
}
get => _maxRequestHeaderCount;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired);
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
}
_maxRequestHeaderCount = value;
}
@ -152,14 +141,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public TimeSpan KeepAliveTimeout
{
get
{
return _keepAliveTimeout;
}
set
{
_keepAliveTimeout = value;
}
get => _keepAliveTimeout;
set => _keepAliveTimeout = value;
}
/// <summary>
@ -170,13 +153,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks>
public TimeSpan RequestHeadersTimeout
{
get
{
return _requestHeadersTimeout;
}
get => _requestHeadersTimeout;
set => _requestHeadersTimeout = value;
}
/// <summary>
/// Gets or sets the maximum number of open HTTP/S connections. When set to null, the number of connections is unlimited.
/// </summary>
/// <remarks>
/// <para>
/// Defaults to null.
/// </para>
/// <para>
/// When a connection is upgraded to another protocol, such as WebSockets, its connection is counted against the
/// <see cref="MaxConcurrentUpgradedConnections" /> limit instead of <see cref="MaxConcurrentConnections" />.
/// </para>
/// </remarks>
public long? MaxConcurrentConnections
{
get => _maxConcurrentConnections;
set
{
_requestHeadersTimeout = value;
if (value.HasValue && value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberOrNullRequired);
}
_maxConcurrentConnections = value;
}
}
/// <summary>
/// Gets or sets the maximum number of open, upgraded connections. When set to null, the number of upgraded connections is unlimited.
/// An upgraded connection is one that has been switched from HTTP to another protocol, such as WebSockets.
/// </summary>
/// <remarks>
/// <para>
/// Defaults to null.
/// </para>
/// <para>
/// When a connection is upgraded to another protocol, such as WebSockets, its connection is counted against the
/// <see cref="MaxConcurrentUpgradedConnections" /> limit instead of <see cref="MaxConcurrentConnections" />.
/// </para>
/// </remarks>
public long? MaxConcurrentUpgradedConnections
{
get => _maxConcurrentUpgradedConnections;
set
{
if (value.HasValue && value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
}
_maxConcurrentUpgradedConnections = value;
}
}
}

View File

@ -529,46 +529,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidContentLength_InvalidNumber", "value"), value);
/// <summary>
/// Value must be null or a non-negative integer.
/// Value must be null or a non-negative number.
/// </summary>
internal static string NonNegativeNullableIntRequired
internal static string NonNegativeNumberOrNullRequired
{
get => GetString("NonNegativeNullableIntRequired");
get => GetString("NonNegativeNumberOrNullRequired");
}
/// <summary>
/// Value must be null or a non-negative integer.
/// Value must be null or a non-negative number.
/// </summary>
internal static string FormatNonNegativeNullableIntRequired()
=> GetString("NonNegativeNullableIntRequired");
internal static string FormatNonNegativeNumberOrNullRequired()
=> GetString("NonNegativeNumberOrNullRequired");
/// <summary>
/// Value must be a positive integer.
/// Value must be a non-negative number.
/// </summary>
internal static string PositiveIntRequired
internal static string NonNegativeNumberRequired
{
get => GetString("PositiveIntRequired");
get => GetString("NonNegativeNumberRequired");
}
/// <summary>
/// Value must be a positive integer.
/// Value must be a non-negative number.
/// </summary>
internal static string FormatPositiveIntRequired()
=> GetString("PositiveIntRequired");
internal static string FormatNonNegativeNumberRequired()
=> GetString("NonNegativeNumberRequired");
/// <summary>
/// Value must be null or a positive integer.
/// Value must be a positive number.
/// </summary>
internal static string PositiveNullableIntRequired
internal static string PositiveNumberRequired
{
get => GetString("PositiveNullableIntRequired");
get => GetString("PositiveNumberRequired");
}
/// <summary>
/// Value must be null or a positive integer.
/// Value must be a positive number.
/// </summary>
internal static string FormatPositiveNullableIntRequired()
=> GetString("PositiveNullableIntRequired");
internal static string FormatPositiveNumberRequired()
=> GetString("PositiveNumberRequired");
/// <summary>
/// Value must be null or a positive number.
/// </summary>
internal static string PositiveNumberOrNullRequired
{
get => GetString("PositiveNumberOrNullRequired");
}
/// <summary>
/// Value must be null or a positive number.
/// </summary>
internal static string FormatPositiveNumberOrNullRequired()
=> GetString("PositiveNumberOrNullRequired");
/// <summary>
/// Unix socket path must be absolute.
@ -906,6 +920,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatCannotUpgradeNonUpgradableRequest()
=> GetString("CannotUpgradeNonUpgradableRequest");
/// <summary>
/// Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.
/// </summary>
internal static string UpgradedConnectionLimitReached
{
get => GetString("UpgradedConnectionLimitReached");
}
/// <summary>
/// Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.
/// </summary>
internal static string FormatUpgradedConnectionLimitReached()
=> GetString("UpgradedConnectionLimitReached");
/// <summary>
/// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.
/// </summary>
internal static string UpgradeCannotBeCalledMultipleTimes
{
get => GetString("UpgradeCannotBeCalledMultipleTimes");
}
/// <summary>
/// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.
/// </summary>
internal static string FormatUpgradeCannotBeCalledMultipleTimes()
=> GetString("UpgradeCannotBeCalledMultipleTimes");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var connectionId = "0";
var trace = new Mock<IKestrelTrace>();
var frameConnectionManager = new FrameConnectionManager(trace.Object);
var frameConnectionManager = new FrameConnectionManager(trace.Object, ResourceCounter.Unlimited, ResourceCounter.Unlimited);
// Create FrameConnection in inner scope so it doesn't get rooted by the current frame.
UnrootedConnectionsGetRemovedFromHeartbeatInnerScope(connectionId, frameConnectionManager, trace);

View File

@ -105,10 +105,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData(0)]
public void MaxRequestHeadersTotalSizeInvalid(int value)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
(new KestrelServerLimits()).MaxRequestHeadersTotalSize = value;
});
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxRequestHeadersTotalSize = value);
Assert.StartsWith(CoreStrings.PositiveNumberRequired, ex.Message);
}
[Theory]
@ -187,5 +185,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
o.RequestHeadersTimeout = TimeSpan.FromSeconds(seconds);
Assert.Equal(seconds, o.RequestHeadersTimeout.TotalSeconds);
}
[Fact]
public void MaxConnectionsDefault()
{
Assert.Null(new KestrelServerLimits().MaxConcurrentConnections);
Assert.Null(new KestrelServerLimits().MaxConcurrentUpgradedConnections);
}
[Theory]
[InlineData(null)]
[InlineData(1u)]
[InlineData(long.MaxValue)]
public void MaxConnectionsValid(long? value)
{
var limits = new KestrelServerLimits
{
MaxConcurrentConnections = value
};
Assert.Equal(value, limits.MaxConcurrentConnections);
}
[Theory]
[InlineData(long.MinValue)]
[InlineData(-1)]
[InlineData(0)]
public void MaxConnectionsInvalid(long value)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxConcurrentConnections = value);
Assert.StartsWith(CoreStrings.PositiveNumberOrNullRequired, ex.Message);
}
[Theory]
[InlineData(null)]
[InlineData(0)]
[InlineData(1)]
[InlineData(long.MaxValue)]
public void MaxUpgradedConnectionsValid(long? value)
{
var limits = new KestrelServerLimits
{
MaxConcurrentUpgradedConnections = value
};
Assert.Equal(value, limits.MaxConcurrentUpgradedConnections);
}
[Theory]
[InlineData(long.MinValue)]
[InlineData(-1)]
public void MaxUpgradedConnectionsInvalid(long value)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxConcurrentUpgradedConnections = value);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class ResourceCounterTests
{
[Theory]
[InlineData(-1)]
[InlineData(long.MinValue)]
public void QuotaInvalid(long max)
{
Assert.Throws<ArgumentOutOfRangeException>(() => ResourceCounter.Quota(max));
}
[Fact]
public void QuotaAcceptsUpToButNotMoreThanMax()
{
var counter = ResourceCounter.Quota(1);
Assert.True(counter.TryLockOne());
Assert.False(counter.TryLockOne());
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(10)]
[InlineData(100)]
public void QuotaValid(long max)
{
var counter = ResourceCounter.Quota(max);
Parallel.For(0, max, i =>
{
Assert.True(counter.TryLockOne());
});
Parallel.For(0, 10, i =>
{
Assert.False(counter.TryLockOne());
});
}
[Fact]
public void QuotaDoesNotWrapAround()
{
var counter = new ResourceCounter.FiniteCounter(long.MaxValue);
counter.Count = long.MaxValue;
Assert.False(counter.TryLockOne());
}
}
}

View File

@ -0,0 +1,198 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Tests;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class ConnectionLimitTests
{
[Fact]
public async Task ResetsCountWhenConnectionClosed()
{
var requestTcs = new TaskCompletionSource<object>();
var releasedTcs = new TaskCompletionSource<object>();
var lockedTcs = new TaskCompletionSource<bool>();
var (serviceContext, counter) = SetupMaxConnections(max: 1);
counter.OnLock += (s, e) => lockedTcs.TrySetResult(e);
counter.OnRelease += (s, e) => releasedTcs.TrySetResult(null);
using (var server = new TestServer(async context =>
{
await context.Response.WriteAsync("Hello");
await requestTcs.Task;
}, serviceContext))
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetAsKeepAlive(); ;
await connection.Receive("HTTP/1.1 200 OK");
Assert.True(await lockedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10)));
requestTcs.TrySetResult(null);
}
await releasedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
}
[Fact]
public async Task UpgradedConnectionsCountsAgainstDifferentLimit()
{
var (serviceContext, _) = SetupMaxConnections(max: 1);
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
if (feature.IsUpgradableRequest)
{
var stream = await feature.UpgradeAsync();
// keep it running until aborted
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}
}, serviceContext))
using (var disposables = new DisposableStack<TestConnection>())
{
var upgraded = server.CreateConnection();
disposables.Push(upgraded);
await upgraded.SendEmptyGetWithUpgrade();
await upgraded.Receive("HTTP/1.1 101");
// once upgraded, normal connection limit is decreased to allow room for more "normal" connections
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200 OK");
using (var rejected = server.CreateConnection())
{
try
{
// this may throw IOException, depending on how fast Kestrel closes the socket
await rejected.SendEmptyGetAsKeepAlive();
} catch { }
// connection should close without sending any data
await rejected.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
}
}
[Fact]
public async Task RejectsConnectionsWhenLimitReached()
{
const int max = 10;
var (serviceContext, _) = SetupMaxConnections(max);
var requestTcs = new TaskCompletionSource<object>();
using (var server = new TestServer(async context =>
{
await context.Response.WriteAsync("Hello");
await requestTcs.Task;
}, serviceContext))
using (var disposables = new DisposableStack<TestConnection>())
{
for (var i = 0; i < max; i++)
{
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200 OK");
}
// limit has been reached
for (var i = 0; i < 10; i++)
{
using (var connection = server.CreateConnection())
{
try
{
// this may throw IOException, depending on how fast Kestrel closes the socket
await connection.SendEmptyGetAsKeepAlive();
} catch { }
// connection should close without sending any data
await connection.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
}
requestTcs.TrySetResult(null);
}
}
[Fact]
public async Task ConnectionCountingReturnsToZero()
{
const int count = 500;
var opened = 0;
var closed = 0;
var openedTcs = new TaskCompletionSource<object>();
var closedTcs = new TaskCompletionSource<object>();
var (serviceContext, counter) = SetupMaxConnections(uint.MaxValue);
counter.OnLock += (o, e) =>
{
if (e && Interlocked.Increment(ref opened) >= count)
{
openedTcs.TrySetResult(null);
}
};
counter.OnRelease += (o, e) =>
{
if (Interlocked.Increment(ref closed) >= count)
{
closedTcs.TrySetResult(null);
}
};
using (var server = new TestServer(_ => Task.CompletedTask, serviceContext))
{
// open a bunch of connections in parallel
Parallel.For(0, count, async i =>
{
try
{
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200");
}
}
catch (Exception ex)
{
closedTcs.TrySetException(ex);
}
});
// wait until resource counter has called lock for each connection
await openedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60));
// wait until resource counter has released all normal connections
await closedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60));
Assert.Equal(count, opened);
Assert.Equal(count, closed);
}
}
private (TestServiceContext serviceContext, EventRaisingResourceCounter counter) SetupMaxConnections(long max)
{
var counter = new EventRaisingResourceCounter(ResourceCounter.Quota(max));
var serviceContext = new TestServiceContext();
serviceContext.ConnectionManager = new FrameConnectionManager(serviceContext.Log, counter, ResourceCounter.Unlimited);
return (serviceContext, counter);
}
}
}

View File

@ -46,10 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"",
"");
await connection.SendEmptyGet();
Assert.True(await appStartedWh.WaitAsync(TimeSpan.FromSeconds(10)));

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Tuple.Create((long?)_dataLength - 1, false),
// Buffer is exactly the same size as data. Exposed race condition where
// IConnectionControl.Resume() was called after socket was disconnected.
// the connection was resumed after socket was disconnected.
Tuple.Create((long?)_dataLength, false),
// Largest possible buffer, should never trigger backpressure.
@ -244,9 +244,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
private static IWebHost StartWebHost(long? maxRequestBufferSize,
byte[] expectedBody,
bool useConnectionAdapter,
private static IWebHost StartWebHost(long? maxRequestBufferSize,
byte[] expectedBody,
bool useConnectionAdapter,
TaskCompletionSource<object> startReadingRequestBody,
TaskCompletionSource<object> clientFinishedSendingRequestBody)
{

View File

@ -662,10 +662,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var buffer = new char[identifierLength];
for (var i = 0; i < iterations; i++)
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"",
"");
await connection.SendEmptyGet();
await connection.Receive($"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",

View File

@ -6,14 +6,24 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Tests;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class UpgradeTests
{
private readonly ITestOutputHelper _output;
public UpgradeTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task ResponseThrowsAfterUpgrade()
{
@ -36,11 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.SendEmptyGetWithUpgrade();
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
@ -90,11 +96,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.SendEmptyGetWithUpgrade();
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
@ -110,6 +112,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
[Fact]
public async Task UpgradeCannotBeCalledMultipleTimes()
{
var upgradeTcs = new TaskCompletionSource<object>();
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
await feature.UpgradeAsync();
try
{
await feature.UpgradeAsync();
}
catch (Exception e)
{
upgradeTcs.TrySetException(e);
throw;
}
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}))
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetWithUpgrade();
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
await connection.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(15)));
Assert.Equal(CoreStrings.UpgradeCannotBeCalledMultipleTimes, ex.Message);
}
[Fact]
public async Task RejectsRequestWithContentLengthAndUpgrade()
{
@ -151,11 +192,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.SendEmptyGetWithUpgrade();
await connection.Receive("HTTP/1.1 200 OK");
}
}
@ -207,10 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"",
"");
await connection.SendEmptyGet();
await connection.Receive("HTTP/1.1 200 OK");
}
}
@ -218,5 +252,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task).TimeoutAfter(TimeSpan.FromSeconds(15));
Assert.Equal(CoreStrings.CannotUpgradeNonUpgradableRequest, ex.Message);
}
[Fact]
public async Task RejectsUpgradeWhenLimitReached()
{
const int limit = 10;
var upgradeTcs = new TaskCompletionSource<object>();
var serviceContext = new TestServiceContext();
serviceContext.ConnectionManager = new FrameConnectionManager(serviceContext.Log, ResourceCounter.Unlimited, ResourceCounter.Quota(limit));
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
if (feature.IsUpgradableRequest)
{
try
{
var stream = await feature.UpgradeAsync();
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}
catch (InvalidOperationException ex)
{
upgradeTcs.TrySetException(ex);
}
}
}, serviceContext))
{
using (var disposables = new DisposableStack<TestConnection>())
{
for (var i = 0; i < limit; i++)
{
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetWithUpgradeAndKeepAlive();
await connection.Receive("HTTP/1.1 101");
}
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetWithUpgradeAndKeepAlive();
await connection.Receive("HTTP/1.1 200");
}
}
}
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60)));
Assert.Equal(CoreStrings.UpgradedConnectionLimitReached, exception.Message);
}
}
}

View File

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public void ConnectionReadFin(string connectionId) { }
public void ConnectionReset(string connectionId) { }
public void ConnectionResume(string connectionId) { }
public void ConnectionRejected(string connectionId) { }
public void ConnectionStart(string connectionId) { }
public void ConnectionStop(string connectionId) { }
public void ConnectionWrite(string connectionId, int count) { }

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Server.Kestrel.Tests
{
public class DisposableStack<T> : Stack<T>, IDisposable
where T : IDisposable
{
public void Dispose()
{
while (Count > 0)
{
Pop()?.Dispose();
}
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Tests
{
public class EventRaisingResourceCounter : ResourceCounter
{
private readonly ResourceCounter _wrapped;
public EventRaisingResourceCounter(ResourceCounter wrapped)
{
_wrapped = wrapped;
}
public event EventHandler OnRelease;
public event EventHandler<bool> OnLock;
public override void ReleaseOne()
{
_wrapped.ReleaseOne();
OnRelease?.Invoke(this, EventArgs.Empty);
}
public override bool TryLockOne()
{
var retVal = _wrapped.TryLockOne();
OnLock?.Invoke(this, retVal);
return retVal;
}
}
}

View File

@ -61,6 +61,32 @@ namespace Microsoft.AspNetCore.Testing
}
}
public Task SendEmptyGet()
{
return Send("GET / HTTP/1.1",
"Host:",
"",
"");
}
public Task SendEmptyGetWithUpgradeAndKeepAlive()
=> SendEmptyGetWithConnection("Upgrade, keep-alive");
public Task SendEmptyGetWithUpgrade()
=> SendEmptyGetWithConnection("Upgrade");
public Task SendEmptyGetAsKeepAlive()
=> SendEmptyGetWithConnection("keep-alive");
private Task SendEmptyGetWithConnection(string connection)
{
return Send("GET / HTTP/1.1",
"Host:",
"Connection: " + connection,
"",
"");
}
public async Task SendAll(params string[] lines)
{
var text = string.Join("\r\n", lines);

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Testing
ThreadPool = new LoggingThreadPool(Log);
SystemClock = new MockSystemClock();
DateHeaderValueManager = new DateHeaderValueManager(SystemClock);
ConnectionManager = new FrameConnectionManager(Log);
ConnectionManager = new FrameConnectionManager(Log, ResourceCounter.Unlimited, ResourceCounter.Unlimited);
DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String;
HttpParserFactory = frameAdapter => new HttpParser<FrameAdapter>(frameAdapter.Frame.ServiceContext.Log.IsEnabled(LogLevel.Information));
ServerOptions = new KestrelServerOptions