I'm using OWIN to Self-Host Web API while running my tests in parallel using NCrunch and I'm starting it in BeforeEach and stoping in AfterEach methods.
Before each test I'm trying to get available free port, but usually 5-10 tests out of 85 fails with the following exception:
System.Net.HttpListenerException : Failed to listen on prefix
'http://localhost:3369/' because it conflicts with an existing registration on the machine.
So it appears, that sometimes I do not get available port. I tried to use Interlocked class in order to share last used port between multiple threads, but it didn't help.
Here's my tests base class:
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;
}
}
Does anyone know how to make sure, that I always get available port?
The problem in your code is here:
if (!excludedPorts.Contains((ushort)port) && Interlocked.Read(ref _portNumber) < port)
{
Interlocked.Increment(ref _portNumber);
return port;
}
First of all, you can compute the excludedPorts
once per test start, and store them in some static field.
Second, the issue is caused by wrong logic to define is port available or not: between Interlocked.Read
and Interlocked.Increment
other thread can do the same check and return the same port! EG:
3369
: it isn't in excludedPorts
, and _portNumber
is equal to 3368, so check is passed. But stop, I'll think a while... 3369
: it isn't in excludedPorts
, and _portNumber
is equal to 3368, so check is passed too! Wow, I'm so excited, let's Increment
it, and return 3369
. Increment
, and return 3369
!Typical race condition. You can resolve it with two ways:
Use CAS-operation CompareExchange
from Interlocked
class (and you can remove port
variable, something like this (test this code by yourself, please):
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;
Use a static ConcurrentDictionary
of the ports, and add new ports to them, something like this (you may choose another collection):
// 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;