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:
parent
009759c7f6
commit
c343628926
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
void ResetTimeout(long ticks, TimeoutAction timeoutAction);
|
||||
void CancelTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue