Reduce probability of startup port collisions (#1136)

- GetTickCount() is limited to the resolution of the system timer, which is typically 10-16 ms.  If two apps in separate app pools are started within this time window, it's possible GetTickCount() will return the same value, which causes the apps to try the same random port(s).
- Addresses #1124
This commit is contained in:
Mike Harder 2018-08-06 14:41:11 -07:00 committed by GitHub
parent 8f3513b9ec
commit 60a559719f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 11 deletions

View File

@ -3,6 +3,8 @@
#pragma once
#include <random>
#define MIN_PORT 1025
#define MAX_PORT 48000
#define MAX_RETRY 10
@ -303,6 +305,8 @@ private:
volatile BOOL m_fReady;
mutable LONG m_cRefs;
std::mt19937 m_randomGenerator;
DWORD m_dwPort;
DWORD m_dwStartupTimeLimitInMS;
DWORD m_dwShutdownTimeLimitInMS;
@ -331,4 +335,4 @@ private:
PROCESS_MANAGER *m_pProcessManager;
ENVIRONMENT_VAR_HASH *m_pEnvironmentVarTable ;
};
};

View File

@ -89,13 +89,15 @@ SERVER_PROCESS::GetRandomPort
BOOL fPortInUse = FALSE;
DWORD dwActualProcessId = 0;
std::uniform_int_distribution<> dist(MIN_PORT, MAX_PORT);
if (g_fNsiApiNotSupported)
{
//
// the default value for optional parameter dwExcludedPort is 0 which is reserved
// a random number between MIN_PORT and MAX_PORT
//
while ((*pdwPickedPort = (rand() % (MAX_PORT - MIN_PORT)) + MIN_PORT + 1) == dwExcludedPort);
while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort);
}
else
{
@ -107,7 +109,7 @@ SERVER_PROCESS::GetRandomPort
// determing whether the randomly generated port is
// in use by any other process.
//
while ((*pdwPickedPort = (rand() % (MAX_PORT - MIN_PORT)) + MIN_PORT + 1) == dwExcludedPort);
while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort);
hr = CheckIfServerIsUp(*pdwPickedPort, &dwActualProcessId, &fPortInUse);
} while (fPortInUse && ++cRetry < MAX_RETRY);
@ -1909,10 +1911,10 @@ SERVER_PROCESS::SERVER_PROCESS() :
m_pForwarderConnection( NULL ),
m_dwListeningProcessId( 0 ),
m_hListeningProcessHandle( NULL ),
m_hShutdownHandle( NULL )
m_hShutdownHandle( NULL ),
m_randomGenerator( std::random_device()() )
{
InterlockedIncrement(&g_dwActiveServerProcesses);
srand(GetTickCount());
for(INT i=0;i<MAX_ACTIVE_CHILD_PROCESSES; ++i)
{

View File

@ -107,13 +107,15 @@ SERVER_PROCESS::GetRandomPort
BOOL fPortInUse = FALSE;
DWORD dwActualProcessId = 0;
std::uniform_int_distribution<> dist(MIN_PORT, MAX_PORT);
if (g_fNsiApiNotSupported)
{
//
// the default value for optional parameter dwExcludedPort is 0 which is reserved
// a random number between MIN_PORT and MAX_PORT
//
while ((*pdwPickedPort = (rand() % (MAX_PORT - MIN_PORT)) + MIN_PORT + 1) == dwExcludedPort);
while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort);
}
else
{
@ -125,7 +127,7 @@ SERVER_PROCESS::GetRandomPort
// determing whether the randomly generated port is
// in use by any other process.
//
while ((*pdwPickedPort = (rand() % (MAX_PORT - MIN_PORT)) + MIN_PORT + 1) == dwExcludedPort);
while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort);
hr = CheckIfServerIsUp(*pdwPickedPort, &dwActualProcessId, &fPortInUse);
} while (fPortInUse && ++cRetry < MAX_RETRY);
@ -1705,10 +1707,10 @@ SERVER_PROCESS::SERVER_PROCESS() :
m_pForwarderConnection(NULL),
m_dwListeningProcessId(0),
m_hListeningProcessHandle(NULL),
m_hShutdownHandle(NULL)
m_hShutdownHandle(NULL),
m_randomGenerator(std::random_device()())
{
//InterlockedIncrement(&g_dwActiveServerProcesses);
srand(GetTickCount());
for (INT i=0; i<MAX_ACTIVE_CHILD_PROCESSES; ++i)
{

View File

@ -3,6 +3,8 @@
#pragma once
#include <random>
#define MIN_PORT 1025
#define MAX_PORT 48000
#define MAX_RETRY 10
@ -257,6 +259,8 @@ private:
volatile BOOL m_fReady;
mutable LONG m_cRefs;
std::mt19937 m_randomGenerator;
DWORD m_dwPort;
DWORD m_dwStartupTimeLimitInMS;
DWORD m_dwShutdownTimeLimitInMS;

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
protected ApplicationDeployer _deployer;
protected virtual async Task<IISDeploymentResult> DeployAsync(IISDeploymentParameters parameters)
protected ApplicationDeployer CreateDeployer(IISDeploymentParameters parameters)
{
if (!parameters.EnvironmentVariables.ContainsKey(DebugEnvironmentVariable))
{
@ -37,8 +37,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
throw new InvalidOperationException("All tests should use ApplicationPublisher");
}
_deployer = IISApplicationDeployerFactory.Create(parameters, LoggerFactory);
return IISApplicationDeployerFactory.Create(parameters, LoggerFactory);
}
protected virtual async Task<IISDeploymentResult> DeployAsync(IISDeploymentParameters parameters)
{
_deployer = CreateDeployer(parameters);
return (IISDeploymentResult)await _deployer.DeployAsync();
}

View File

@ -0,0 +1,25 @@
// 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.IntegrationTesting
{
public class DisposableList<T> : List<T>, IDisposable where T : IDisposable
{
public DisposableList() : base() { }
public DisposableList(IEnumerable<T> collection) : base(collection) { }
public DisposableList(int capacity) : base(capacity) { }
public void Dispose()
{
foreach (var item in this)
{
item?.Dispose();
}
}
}
}

View File

@ -0,0 +1,66 @@
// 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.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
{
[Collection(PublishedSitesCollection.Name)]
public class MultipleAppTests : IISFunctionalTestBase
{
private readonly PublishedSitesFixture _fixture;
public MultipleAppTests(PublishedSitesFixture fixture)
{
_fixture = fixture;
}
[ConditionalTheory]
[InlineData(AncmVersion.AspNetCoreModule)]
[InlineData(AncmVersion.AspNetCoreModuleV2)]
public async Task Startup(AncmVersion ancmVersion)
{
const int numApps = 10;
using (var deployers = new DisposableList<ApplicationDeployer>())
{
var deploymentResults = new List<DeploymentResult>();
// Deploy all apps
for (var i = 0; i < numApps; i++)
{
var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: IntegrationTesting.HostingModel.OutOfProcess);
deploymentParameters.AncmVersion = ancmVersion;
var deployer = CreateDeployer(deploymentParameters);
deployers.Add(deployer);
deploymentResults.Add(await deployer.DeployAsync());
}
// Start all requests as quickly as possible, so apps are started as quickly as possible,
// to test possible race conditions when multiple apps start at the same time.
var requestTasks = new List<Task<HttpResponseMessage>>();
foreach (var deploymentResult in deploymentResults)
{
requestTasks.Add(deploymentResult.HttpClient.GetAsync("/HelloWorld"));
}
// Verify all apps started and return expected response
foreach (var requestTask in requestTasks)
{
var response = await requestTask;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World", responseText);
}
}
}
}
}