Skip to content
代码片段 群组 项目
提交 3f7ee338 编辑于 作者: Mike Harder's avatar Mike Harder 提交者: Pranav K
浏览文件

Change SelfHostDeployer to use dynamic ports by default (#1383)

* Should significantly reduce flaky test failures due to AddressInUse exceptions
* Addresses https://github.com/aspnet/Hosting/issues/1296
上级 958d41f7
No related branches found
No related tags found
无相关合并请求
...@@ -11,39 +11,72 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common ...@@ -11,39 +11,72 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
{ {
public static Uri BuildTestUri() public static Uri BuildTestUri()
{ {
return new UriBuilder("http", "localhost", GetNextPort()).Uri; return BuildTestUri(null);
} }
public static Uri BuildTestUri(string hint) public static Uri BuildTestUri(string hint)
{
// If this method is called directly, there is no way to know the server type or whether status messages
// are enabled. It's safest to assume the server is WebListener (which doesn't support binding to dynamic
// port "0") and status messages are not enabled (so the assigned port cannot be scraped from console output).
return BuildTestUri(hint, serverType: ServerType.WebListener, statusMessagesEnabled: false);
}
internal static Uri BuildTestUri(string hint, ServerType serverType, bool statusMessagesEnabled)
{ {
if (string.IsNullOrEmpty(hint)) if (string.IsNullOrEmpty(hint))
{ {
return BuildTestUri(); if (serverType == ServerType.Kestrel && statusMessagesEnabled)
{
// Most functional tests use this codepath and should directly bind to dynamic port "0" and scrape
// the assigned port from the status message, which should be 100% reliable since the port is bound
// once and never released. Binding to dynamic port "0" on "localhost" (both IPv4 and IPv6) is not
// supported, so the port is only bound on "127.0.0.1" (IPv4). If a test explicitly requires IPv6,
// it should provide a hint URL with "localhost" (IPv4 and IPv6) or "[::1]" (IPv6-only).
return new UriBuilder("http", "127.0.0.1", 0).Uri;
}
else
{
// If the server type is not Kestrel, or status messages are disabled, there is no status message
// from which to scrape the assigned port, so the less reliable GetNextPort() must be used. The
// port is bound on "localhost" (both IPv4 and IPv6), since this is supported when using a specific
// (non-zero) port.
return new UriBuilder("http", "localhost", GetNextPort()).Uri;
}
} }
else else
{ {
var uriHint = new Uri(hint); var uriHint = new Uri(hint);
if (uriHint.Port == 0) if (uriHint.Port == 0)
{ {
// Only a few tests use this codepath, so it's fine to use the less reliable GetNextPort() for simplicity.
// The tests using this codepath will be reviewed to see if they can be changed to directly bind to dynamic
// port "0" on "127.0.0.1" and scrape the assigned port from the status message (the default codepath).
return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri; return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri;
} }
else else
{ {
// If the hint contains a specific port, return it unchanged.
return uriHint; return uriHint;
} }
} }
} }
// Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508
//
// This method is an attempt to safely get a free port from the OS. Most of the time,
// when binding to dynamic port "0" the OS increments the assigned port, so it's safe
// to re-use the assigned port in another process. However, occasionally the OS will reuse
// a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse
// exceptions. This method should only be used when the application itself cannot use
// dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel
// (with status messages enabled) should directly bind to dynamic port "0" and scrape
// the assigned port from the status message, which should be 100% reliable since the port
// is bound once and never released.
public static int GetNextPort() public static int GetNextPort()
{ {
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{ {
// Let the OS assign the next available port. Unless we cycle through all ports
// on a test run, the OS will always increment the port number when making these calls.
// This prevents races in parallel test runs where a test is already bound to
// a given port, and a new test is able to bind to the same port due to port
// reuse being enabled by default by the OS.
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint).Port; return ((IPEndPoint)socket.LocalEndPoint).Port;
} }
......
...@@ -41,7 +41,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting ...@@ -41,7 +41,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
DotnetPublish(); DotnetPublish();
} }
var hintUrl = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); var hintUrl = TestUriHelper.BuildTestUri(
DeploymentParameters.ApplicationBaseUriHint,
DeploymentParameters.ServerType,
DeploymentParameters.StatusMessagesEnabled);
// Launch the host process. // Launch the host process.
var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl); var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl);
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册