Search code examples
c#multithreadingasp.net-web-apiowinncrunch

Get free port for WebApi OWIN self host when running tests in parallel


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?


Solution

  • 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:

    1. Thread A: check for the 3369: it isn't in excludedPorts, and _portNumber is equal to 3368, so check is passed. But stop, I'll think a while...
    2. Thread B: check for the 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.
    3. Thread A: OK, so where were we? Oh, yes, 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;