Skip to content

Commit 51ae61b

Browse files
bkatmsanalogrelay
authored andcommitted
Support attaching to an existing request queue in HTTP.SYS (#14182)
1 parent 5df258a commit 51ae61b

21 files changed

+837
-49
lines changed

src/Servers/HttpSys/HttpSysServer.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Hostin
7070
EndProject
7171
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Connections.Abstractions", "..\Connections.Abstractions\src\Microsoft.AspNetCore.Connections.Abstractions.csproj", "{00A88B8D-D539-45DD-B071-1E955AF89A4A}"
7272
EndProject
73+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueueSharing", "samples\QueueSharing\QueueSharing.csproj", "{9B58DF76-DC6D-4728-86B7-40087BDDC897}"
74+
EndProject
7375
Global
7476
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7577
Debug|Any CPU = Debug|Any CPU
@@ -304,6 +306,18 @@ Global
304306
{00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
305307
{00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|x86.ActiveCfg = Release|Any CPU
306308
{00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|x86.Build.0 = Release|Any CPU
309+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
310+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Any CPU.Build.0 = Debug|Any CPU
311+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
312+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
313+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|x86.ActiveCfg = Debug|Any CPU
314+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|x86.Build.0 = Debug|Any CPU
315+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Any CPU.ActiveCfg = Release|Any CPU
316+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Any CPU.Build.0 = Release|Any CPU
317+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
318+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Mixed Platforms.Build.0 = Release|Any CPU
319+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|x86.ActiveCfg = Release|Any CPU
320+
{9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|x86.Build.0 = Release|Any CPU
307321
EndGlobalSection
308322
GlobalSection(SolutionProperties) = preSolution
309323
HideSolutionNode = FALSE
@@ -329,6 +343,7 @@ Global
329343
{19DC60DE-C413-43A2-985E-0D0F20AD2302} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
330344
{D93575B3-BFA3-4523-B060-D268D6A0A66B} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
331345
{00A88B8D-D539-45DD-B071-1E955AF89A4A} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
346+
{9B58DF76-DC6D-4728-86B7-40087BDDC897} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514}
332347
EndGlobalSection
333348
GlobalSection(ExtensibilityGlobals) = postSolution
334349
SolutionGuid = {34B42B42-FA09-41AB-9216-14073990C504}

src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public HttpSysOptions() { }
5252
public long? MaxConnections { get { throw null; } set { } }
5353
public long? MaxRequestBodySize { get { throw null; } set { } }
5454
public long RequestQueueLimit { get { throw null; } set { } }
55+
public Microsoft.AspNetCore.Server.HttpSys.RequestQueueMode RequestQueueMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5556
public string RequestQueueName { get { throw null; } set { } }
5657
public bool ThrowWriteExceptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5758
public Microsoft.AspNetCore.Server.HttpSys.TimeoutManager Timeouts { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
@@ -61,6 +62,12 @@ public partial interface IHttpSysRequestInfoFeature
6162
{
6263
System.Collections.Generic.IReadOnlyDictionary<int, System.ReadOnlyMemory<byte>> RequestInfo { get; }
6364
}
65+
public enum RequestQueueMode
66+
{
67+
Create = 0,
68+
Attach = 1,
69+
CreateOrAttach = 2,
70+
}
6471
public sealed partial class TimeoutManager
6572
{
6673
internal TimeoutManager() { }
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Xml.Schema;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Server.HttpSys;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Hosting;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace QueueSharing
12+
{
13+
public static class Program
14+
{
15+
public static void Main(string[] args)
16+
{
17+
Console.Write("Create and (c)reate, (a)ttach to existing, or attach (o)r create? ");
18+
var key = Console.ReadKey();
19+
Console.WriteLine();
20+
var mode = RequestQueueMode.Create;
21+
switch (key.KeyChar)
22+
{
23+
case 'a':
24+
mode = RequestQueueMode.Attach;
25+
break;
26+
case 'o':
27+
mode = RequestQueueMode.CreateOrAttach;
28+
break;
29+
case 'c':
30+
mode = RequestQueueMode.Create;
31+
break;
32+
default:
33+
Console.WriteLine("Unknown option, defaulting to (c)create.");
34+
break;
35+
}
36+
37+
var host = new HostBuilder()
38+
.ConfigureLogging(factory => factory.AddConsole())
39+
.ConfigureWebHost(webHost =>
40+
{
41+
webHost.UseHttpSys(options =>
42+
{
43+
// Skipping this to ensure the server works without any prefixes in attach mode.
44+
if (mode != RequestQueueMode.Attach)
45+
{
46+
options.UrlPrefixes.Add("http://localhost:5002");
47+
}
48+
options.RequestQueueName = "QueueName";
49+
options.RequestQueueMode = mode;
50+
options.MaxAccepts = 1; // Better load rotation between instances.
51+
}).ConfigureServices(services =>
52+
{
53+
54+
}).Configure(app =>
55+
{
56+
app.Run(async context =>
57+
{
58+
context.Response.ContentType = "text/plain";
59+
// There's a strong connection affinity between processes. Close the connection so the next request can be dispatched to a random instance.
60+
// It appears to be round robin based and switch instances roughly every 30 requests when using new connections.
61+
// This seems related to the default MaxAccepts (5 * processor count).
62+
// I'm told this connection affinity does not apply to HTTP/2.
63+
context.Response.Headers["Connection"] = "close";
64+
await context.Response.WriteAsync("Hello world from " + context.Request.Host + " at " + DateTime.Now);
65+
});
66+
});
67+
68+
})
69+
.Build();
70+
71+
host.Run();
72+
}
73+
}
74+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
<ServerGarbageCollection>true</ServerGarbageCollection>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Reference Include="Microsoft.AspNetCore.Server.HttpSys" />
11+
<Reference Include="Microsoft.Extensions.Hosting" />
12+
<Reference Include="Microsoft.Extensions.Logging.Console" />
13+
</ItemGroup>
14+
</Project>

src/Servers/HttpSys/src/AuthenticationManager.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ internal AuthenticationManager()
3232
{
3333
}
3434

35+
/// <summary>
36+
/// When attaching to an existing queue this setting must match the one used to create the queue.
37+
/// </summary>
3538
public AuthenticationSchemes Schemes
3639
{
3740
get { return _authSchemes; }

src/Servers/HttpSys/src/HttpSysListener.cs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
6969
// V2 initialization sequence:
7070
// 1. Create server session
7171
// 2. Create url group
72-
// 3. Create request queue - Done in Start()
72+
// 3. Create request queue
7373
// 4. Add urls to url group - Done in Start()
7474
// 5. Attach request queue to url group - Done in Start()
7575

@@ -79,7 +79,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
7979

8080
_urlGroup = new UrlGroup(_serverSession, Logger);
8181

82-
_requestQueue = new RequestQueue(_urlGroup, options.RequestQueueName, Logger);
82+
_requestQueue = new RequestQueue(_urlGroup, options.RequestQueueName, options.RequestQueueMode, Logger);
8383

8484
_disconnectListener = new DisconnectListener(_requestQueue, Logger);
8585
}
@@ -147,20 +147,24 @@ public void Start()
147147
return;
148148
}
149149

150-
Options.Apply(UrlGroup, RequestQueue);
150+
// If this instance created the queue then configure it.
151+
if (_requestQueue.Created)
152+
{
153+
Options.Apply(UrlGroup, RequestQueue);
151154

152-
_requestQueue.AttachToUrlGroup();
155+
_requestQueue.AttachToUrlGroup();
153156

154-
// All resources are set up correctly. Now add all prefixes.
155-
try
156-
{
157-
Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup);
158-
}
159-
catch (HttpSysException)
160-
{
161-
// If an error occurred while adding prefixes, free all resources allocated by previous steps.
162-
_requestQueue.DetachFromUrlGroup();
163-
throw;
157+
// All resources are set up correctly. Now add all prefixes.
158+
try
159+
{
160+
Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup);
161+
}
162+
catch (HttpSysException)
163+
{
164+
// If an error occurred while adding prefixes, free all resources allocated by previous steps.
165+
_requestQueue.DetachFromUrlGroup();
166+
throw;
167+
}
164168
}
165169

166170
_state = State.Started;
@@ -188,11 +192,15 @@ private void Stop()
188192
return;
189193
}
190194

191-
Options.UrlPrefixes.UnregisterAllPrefixes();
195+
// If this instance created the queue then remove the URL prefixes before shutting down.
196+
if (_requestQueue.Created)
197+
{
198+
Options.UrlPrefixes.UnregisterAllPrefixes();
199+
_requestQueue.DetachFromUrlGroup();
200+
}
192201

193202
_state = State.Stopped;
194203

195-
_requestQueue.DetachFromUrlGroup();
196204
}
197205
}
198206
catch (Exception exception)

src/Servers/HttpSys/src/HttpSysOptions.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public string RequestQueueName
3838
get => _requestQueueName;
3939
set
4040
{
41-
if (value.Length > MaximumRequestQueueNameLength)
41+
if (value?.Length > MaximumRequestQueueNameLength)
4242
{
4343
throw new ArgumentOutOfRangeException(nameof(value),
4444
value,
@@ -48,6 +48,12 @@ public string RequestQueueName
4848
}
4949
}
5050

51+
/// <summary>
52+
/// Indicates if this server instance is responsible for creating and configuring the request queue,
53+
/// of if it should attach to an existing queue. The default is to create.
54+
/// </summary>
55+
public RequestQueueMode RequestQueueMode { get; set; }
56+
5157
/// <summary>
5258
/// The maximum number of concurrent accepts.
5359
/// </summary>
@@ -63,6 +69,7 @@ public string RequestQueueName
6369
/// <summary>
6470
/// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing
6571
/// the listener.
72+
/// When attached to an existing queue the prefixes are only used to compute PathBase for requests.
6673
/// </summary>
6774
public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection();
6875

@@ -75,6 +82,7 @@ public string RequestQueueName
7582
/// <summary>
7683
/// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
7784
/// These may be modified at any time prior to disposing the listener.
85+
/// These settings do not apply when attaching to an existing queue.
7886
/// </summary>
7987
public TimeoutManager Timeouts { get; } = new TimeoutManager();
8088

@@ -87,6 +95,7 @@ public string RequestQueueName
8795
/// <summary>
8896
/// Gets or sets the maximum number of concurrent connections to accept, -1 for infinite, or null to
8997
/// use the machine wide setting from the registry. The default value is null.
98+
/// This settings does not apply when attaching to an existing queue.
9099
/// </summary>
91100
public long? MaxConnections
92101
{
@@ -109,6 +118,7 @@ public long? MaxConnections
109118

110119
/// <summary>
111120
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
121+
/// This settings does not apply when attaching to an existing queue.
112122
/// </summary>
113123
public long RequestQueueLimit
114124
{
@@ -164,6 +174,7 @@ public long? MaxRequestBodySize
164174
/// Gets or sets a value that controls how http.sys reacts when rejecting requests due to throttling conditions - like when the request
165175
/// queue limit is reached. The default in http.sys is "Basic" which means http.sys is just resetting the TCP connection. IIS uses Limited
166176
/// as its default behavior which will result in sending back a 503 - Service Unavailable back to the client.
177+
/// This settings does not apply when attaching to an existing queue.
167178
/// </summary>
168179
public Http503VerbosityLevel Http503Verbosity
169180
{
@@ -192,6 +203,7 @@ public Http503VerbosityLevel Http503Verbosity
192203
}
193204
}
194205

206+
// Not called when attaching to an existing queue.
195207
internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
196208
{
197209
_urlGroup = urlGroup;

src/Servers/HttpSys/src/MessagePump.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,14 @@ public Task StartAsync<TContext>(IHttpApplication<TContext> application, Cancell
112112
Listener.Options.UrlPrefixes.Add(value);
113113
}
114114
}
115-
else
115+
else if (Listener.RequestQueue.Created)
116116
{
117117
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
118118

119119
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
120120
Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
121121
}
122+
// else // Attaching to an existing queue, don't add a default.
122123

123124
// Can't call Start twice
124125
Contract.Assert(_application == null);

src/Servers/HttpSys/src/NativeInterop/HttpApi.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -65,7 +65,7 @@ internal static unsafe class HttpApi
6565

6666
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
6767
internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION version, string pName,
68-
UnsafeNclNativeMethods.SECURITY_ATTRIBUTES pSecurityAttributes, uint flags, out HttpRequestQueueV2Handle pReqQueueHandle);
68+
UnsafeNclNativeMethods.SECURITY_ATTRIBUTES pSecurityAttributes, HTTP_CREATE_REQUEST_QUEUE_FLAG flags, out HttpRequestQueueV2Handle pReqQueueHandle);
6969

7070
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
7171
internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle);

0 commit comments

Comments
 (0)