我在使用NCrunch在 parallel 中运行我的测试时使用OWIN自我主机Web API,我在BeforeEach中启动它并在AfterEach方法中停止。
在每次测试之前我都试图获得可用的端口,但是85中的5-10个测试通常会失败,但有以下异常:
System.Net.HttpListenerException : Failed to listen on prefix
'http://localhost:3369/' because it conflicts with an existing registration on the machine.
所以看来,有时候我没有可用的端口。我尝试使用Interlocked类来共享多个线程之间的最后使用的端口,但它没有帮助。
这是我的测试基类:
public class BaseSteps
{
private const int PortRangeStart = 3368;
private const int PortRangeEnd = 8968;
private static long _portNumber = PortRangeStart;
private IDisposable _webServer;
//.....
[BeforeScenario]
public void Before()
{
Url = GetFullUrl();
_webServer = WebApp.Start<TestStartup>(Url);
}
[AfterScenario]
public void After()
{
_webServer.Dispose();
}
private static string GetFullUrl()
{
var ipAddress = IPAddress.Loopback;
var portAvailable = GetAvailablePort(PortRangeStart, PortRangeEnd, ipAddress);
return String.Format("http://{0}:{1}/", "localhost", portAvailable);
}
private static int GetAvailablePort(int rangeStart, int rangeEnd, IPAddress ip, bool includeIdlePorts = false)
{
IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();
// if the ip we want a port on is an 'any' or loopback port we need to exclude all ports that are active on any IP
Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
IPAddress.IPv6Any.Equals(i) ||
IPAddress.Loopback.Equals(i) ||
IPAddress.IPv6Loopback.
Equals(i);
// get all active ports on specified IP.
List<ushort> excludedPorts = new List<ushort>();
// if a port is open on an 'any' or 'loopback' interface then include it in the excludedPorts
excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
where
n.LocalEndPoint.Port >= rangeStart &&
n.LocalEndPoint.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) || n.LocalEndPoint.Address.Equals(ip) ||
isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
(!includeIdlePorts || n.State != TcpState.TimeWait)
select (ushort)n.LocalEndPoint.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.Sort();
for (int port = rangeStart; port <= rangeEnd; port++)
{
if (!excludedPorts.Contains((ushort)port) && Interlocked.Read(ref _portNumber) < port)
{
Interlocked.Increment(ref _portNumber);
return port;
}
}
return 0;
}
}
有谁知道如何确保我始终获得可用端口?
答案 0 :(得分:1)
您的代码中存在问题:
if (!excludedPorts.Contains((ushort)port) && Interlocked.Read(ref _portNumber) < port)
{
Interlocked.Increment(ref _portNumber);
return port;
}
首先,您可以在每个测试开始时计算excludedPorts
一次,并将它们存储在某个静态字段中。
其次,问题是由错误的逻辑定义端口是否可用:Interlocked.Read
和Interlocked.Increment
之间的其他线程可以执行相同的检查并返回相同的端口! EG:
3369
:它不在excludedPorts
,_portNumber
等于3368,因此检查通过。但是停下来,我会想一会儿...... 3369
:它不在excludedPorts
,_portNumber
等于3368,所以检查也通过了!哇,我很兴奋,让它Increment
,然后返回3369
。 Increment
,并返回3369
! 典型的比赛条件。您可以通过两种方式解决它:
使用CAS-operation CompareExchange
from Interlocked
class(您可以删除port
变量,如下所示(请自行测试此代码):
var portNumber = _portNumber;
if (excludedPorts.Contains((ushort)portNumber))
{
// if port already taken
continue;
}
if (Interlocked.CompareExchange(ref _portNumber, portNumber + 1, portNumber) != portNumber))
{
// if exchange operation failed, other thread passed through
continue;
}
// only one thread can succeed
return portNumber;
使用静态ConcurrentDictionary
端口,并为它们添加新端口,如下所示(您可以选择其他集合):
// static field in your class
// value item isn't useful
static ConcurrentDictionary<int, bool>() ports = new ConcurrentDictionary<int, bool>();
foreach (var p in excludedPorts)
// you may check here is the adding the port succeed
ports.TryAdd(p, true);
var portNumber = _portNumber;
if (!ports.TryAdd(portNumber, true))
{
continue;
}
return portNumber;