From f1901516c6682aea05cd0f2140fb3864a32bff62 Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 5 Jun 2017 02:01:54 -0700 Subject: [PATCH] #519 Expose a connection limit option --- .../HttpSysListener.cs | 4 +- .../HttpSysOptions.cs | 37 +++++- .../NativeInterop/HttpApi.cs | 21 ++++ .../NativeInterop/UrlGroup.cs | 17 +++ .../ServerTests.cs | 106 ++++++++++++++++++ 5 files changed, 181 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs index c2bea54052..56d1e4356d 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs @@ -146,9 +146,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys return; } - Options.Authentication.SetUrlGroupSecurity(UrlGroup); - Options.Timeouts.SetUrlGroupTimeouts(UrlGroup); - Options.SetRequestQueueLimit(RequestQueue); + Options.Apply(UrlGroup, RequestQueue); _requestQueue.AttachToUrlGroup(); diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs index 85c8dcc2e1..7fc7089a15 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs @@ -12,7 +12,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys // The native request queue private long _requestQueueLength = DefaultRequestQueueLength; + private long? _maxConnections; private RequestQueue _requestQueue; + private UrlGroup _urlGroup; public HttpSysOptions() { @@ -54,6 +56,29 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// public bool ThrowWriteExceptions { get; set; } + /// + /// Gets or sets the maximum number of concurrent connections to accept, -1 for infinite, or null to + /// use the machine wide setting from the registry. The default value is null. + /// + public long? MaxConnections + { + get => _maxConnections; + set + { + if (value.HasValue && value < -1) + { + throw new ArgumentOutOfRangeException(nameof(value), value, string.Empty); + } + + if (value.HasValue && _urlGroup != null) + { + _urlGroup.SetMaxConnections(value.Value); + } + + _maxConnections = value; + } + } + /// /// Gets or sets the maximum number of requests that will be queued up in Http.Sys. /// @@ -79,13 +104,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } - internal void SetRequestQueueLimit(RequestQueue requestQueue) + internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue) { + _urlGroup = urlGroup; _requestQueue = requestQueue; + + if (_maxConnections.HasValue) + { + _urlGroup.SetMaxConnections(_maxConnections.Value); + } + if (_requestQueueLength != DefaultRequestQueueLength) { _requestQueue.SetLengthLimit(_requestQueueLength); } + + Authentication.SetUrlGroupSecurity(urlGroup); + Timeouts.SetUrlGroupTimeouts(urlGroup); } } } diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs index fbea3b2fbc..94bc558af5 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs @@ -544,6 +544,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys HttpRequestAuthTypeKerberos } + internal enum HTTP_QOS_SETTING_TYPE + { + HttpQosSettingTypeBandwidth, + HttpQosSettingTypeConnectionLimit, + HttpQosSettingTypeFlowRate + } + [StructLayout(LayoutKind.Sequential)] internal struct HTTP_SERVER_AUTHENTICATION_INFO { @@ -604,6 +611,20 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal IntPtr RequestQueueHandle; } + [StructLayout(LayoutKind.Sequential)] + internal struct HTTP_CONNECTION_LIMIT_INFO + { + internal HTTP_FLAGS Flags; + internal uint MaxConnections; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct HTTP_QOS_SETTING_INFO + { + internal HTTP_QOS_SETTING_TYPE QosType; + internal IntPtr QosSetting; + } + // see http.w for definitions [Flags] internal enum HTTP_FLAGS : uint diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs index 2e69c0bcb4..cdeaae7800 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs @@ -3,12 +3,16 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys { internal class UrlGroup : IDisposable { + private static readonly int QosInfoSize = + Marshal.SizeOf(); + private ServerSession _serverSession; private ILogger _logger; private bool _disposed; @@ -33,6 +37,19 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal ulong Id { get; private set; } + internal unsafe void SetMaxConnections(long maxConnections) + { + var connectionLimit = new HttpApi.HTTP_CONNECTION_LIMIT_INFO(); + connectionLimit.Flags = HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; + connectionLimit.MaxConnections = (uint)maxConnections; + + var qosSettings = new HttpApi.HTTP_QOS_SETTING_INFO(); + qosSettings.QosType = HttpApi.HTTP_QOS_SETTING_TYPE.HttpQosSettingTypeConnectionLimit; + qosSettings.QosSetting = new IntPtr(&connectionLimit); + + SetProperty(HttpApi.HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize); + } + internal void SetProperty(HttpApi.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true) { Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer"); diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs index 28ec77a4fd..4d8129d7f4 100644 --- a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs +++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs @@ -300,6 +300,112 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + [ConditionalFact] + public void Server_SetConnectionLimitArgumentValidation_Success() + { + var server = Utilities.CreatePump(); + + Assert.Null(server.Listener.Options.MaxConnections); + Assert.Throws(() => server.Listener.Options.MaxConnections = -2); + Assert.Null(server.Listener.Options.MaxConnections); + server.Listener.Options.MaxConnections = null; + server.Listener.Options.MaxConnections = 3; + } + + [ConditionalFact] + public async Task Server_SetConnectionLimit_Success() + { + // This is just to get a dynamic port + string address; + using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { } + + var server = Utilities.CreatePump(); + server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address)); + Assert.Null(server.Listener.Options.MaxConnections); + server.Listener.Options.MaxConnections = 3; + + using (server) + { + await server.StartAsync(new DummyApplication(), CancellationToken.None); + + using (var client1 = await SendHungRequestAsync("GET", address)) + using (var client2 = await SendHungRequestAsync("GET", address)) + { + using (var client3 = await SendHungRequestAsync("GET", address)) + { + // Maxed out, refuses connection and throws + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + } + + // A connection has been closed, try again. + string responseText = await SendRequestAsync(address); + Assert.Equal(string.Empty, responseText); + } + } + } + + [ConditionalFact] + public async Task Server_SetConnectionLimitChangeAfterStarted_Success() + { + // This is just to get a dynamic port + string address; + using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { } + + var server = Utilities.CreatePump(); + server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address)); + Assert.Null(server.Listener.Options.MaxConnections); + server.Listener.Options.MaxConnections = 3; + + using (server) + { + await server.StartAsync(new DummyApplication(), CancellationToken.None); + + using (var client1 = await SendHungRequestAsync("GET", address)) + using (var client2 = await SendHungRequestAsync("GET", address)) + using (var client3 = await SendHungRequestAsync("GET", address)) + { + // Maxed out, refuses connection and throws + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + + server.Listener.Options.MaxConnections = 4; + + string responseText = await SendRequestAsync(address); + Assert.Equal(string.Empty, responseText); + + server.Listener.Options.MaxConnections = 2; + + // Maxed out, refuses connection and throws + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + } + } + } + + [ConditionalFact] + public async Task Server_SetConnectionLimitInfinite_Success() + { + // This is just to get a dynamic port + string address; + using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { } + + var server = Utilities.CreatePump(); + server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address)); + server.Listener.Options.MaxConnections = -1; // infinite + + using (server) + { + await server.StartAsync(new DummyApplication(), CancellationToken.None); + + using (var client1 = await SendHungRequestAsync("GET", address)) + using (var client2 = await SendHungRequestAsync("GET", address)) + using (var client3 = await SendHungRequestAsync("GET", address)) + { + // Doesn't max out + string responseText = await SendRequestAsync(address); + Assert.Equal(string.Empty, responseText); + } + } + } + private async Task SendRequestAsync(string uri) { using (HttpClient client = new HttpClient())