【问题标题】:Get free port for WebApi OWIN self host when running tests in parallel并行运行测试时为 WebApi OWIN 自主机获取免费端口
【发布时间】:2015-11-25 00:32:55
【问题描述】:

我正在使用 OWIN 来自托管 Web API,同时使用 NCrunch 在 并行 中运行我的测试,我在 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;
    }
}

有谁知道如何确保我总是获得可用端口?

【问题讨论】:

    标签: c# multithreading asp.net-web-api owin ncrunch


    【解决方案1】:

    你的代码有问题:

    if (!excludedPorts.Contains((ushort)port) && Interlocked.Read(ref _portNumber) < port)
    {
        Interlocked.Increment(ref _portNumber);
        return port;
    }
    

    首先,您可以在每次测试开始时计算一次excludedPorts,并将它们存储在某个静态字段中。

    其次,问题是由错误的逻辑定义端口是否可用引起的:在Interlocked.ReadInterlocked.Increment 之间,其他线程可以进行相同的检查并返回相同的端口!例如:

    1. 线程A:检查3369:不在excludedPorts中,且_portNumber等于3368,所以检查通过。不过停下来,我再想想……
    2. 线程B:检查3369:它不在excludedPorts,并且_portNumber等于3368,所以检查也通过了!哇,好激动,让我们Increment吧,然后回3369
    3. 线程 A:好的,那我们在哪里?哦,是的,Increment然后返回3369

    典型的竞争条件。您可以通过两种方式解决它:

    • 使用CAS-operationCompareExchange 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;
      

    【讨论】:

    • 感谢您的回复,但我不明白为什么在 Interlocked.CompareExchange 中您将第一个参数与递增的第三个参数进行比较?你不应该比较第一个和第三个参数并增加第二个吗?我在这里粘贴了一些带有 cmets 的代码:cspad.com/w4rb 请你看一下并纠正我的错误,因为测试仍然失败,我不明白出了什么问题。
    • 测试失败的原因是两个不同的测试获得了相同的端口号。是的,这是一个错字
    • 尝试在parallel.for 中运行此代码并查看结果。此外,您的 _portNumber 应该是 volatile,并且不,您不能简单地使用 ++ 运算符,因为它不是线程安全的
    • 不得不稍微重构我的 specflow 测试,但最终使它们工作。采用 ConcurrentDictionary 的第二种方法。非常感谢!
    • 这不是您问题的直接答案,但如果是用于测试,请查看 Microsoft.Owin.Testing nuget 包。您可以使用内存中的 TestServer 实例,而不是启动自托管服务(网络层过载)。重构很简单:只需使用 TestServer.Create 而不是 WebApp.Start 并使用已创建实例的 HttpClient 属性。
    猜你喜欢
    • 2023-04-06
    • 2015-03-18
    • 1970-01-01
    • 2014-08-24
    • 2021-01-17
    • 1970-01-01
    • 2018-12-24
    • 2019-01-30
    相关资源
    最近更新 更多