3

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?

VMAtm
  • 27,943
  • 17
  • 79
  • 125
Andrew
  • 412
  • 5
  • 16

1 Answers1

1

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;
    
VMAtm
  • 27,943
  • 17
  • 79
  • 125
  • Thanks for the response, but I didn't get why in Interlocked.CompareExchange you're comparing 1st parameter to incremented 3rd parameter? Shoudn't you compare 1st to 3rd parameter and increase 2nd? I pasted some code with comments here: http://cspad.com/w4rb Could you please take a look and correct me where I'm wrong, because tests still fails and I don't understand what's wrong. – Andrew Nov 25 '15 at 21:17
  • Tests fails cause of two different tests get the same port number. Yes, this was a typo – VMAtm Nov 26 '15 at 09:29
  • Try to run this code in `parallel.for` and see the results. Also your `_portNumber` should be `volatile`, and no< you can't simply use `++` operator as it isn't thread safe – VMAtm Nov 26 '15 at 09:40
  • Had to refactor my specflow tests a little bit, but finally made them work. Took your second approach with ConcurrentDictionary. Thanks a lot! – Andrew Nov 27 '15 at 00:11
  • Yep, I think that this is the best option. Good luck with your projects! – VMAtm Nov 27 '15 at 09:51
  • 1
    It's not a direct answer to your question, but if it's for testing, look at the Microsoft.Owin.Testing nuget package. Instead of starting a self hosted service (with network layer overload), you can use an in-memory TestServer instance. Refactoring is easy: just use TestServer.Create instead of WebApp.Start and use the HttpClient property of the created instance. – Nullius May 18 '17 at 15:11