You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by sh...@apache.org on 2020/06/11 23:00:11 UTC

[trafficserver] branch master updated: AuTest: port selection improvements. (#6888)

This is an automated email from the ASF dual-hosted git repository.

shinrich pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new ca031ab  AuTest: port selection improvements. (#6888)
ca031ab is described below

commit ca031ab036465a2e17454c0cd69bb7b8a69f479a
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Thu Jun 11 18:00:00 2020 -0500

    AuTest: port selection improvements. (#6888)
    
    This is an attempt to address AuTest port selection issues. This:
    
    - Checks whether the port being delivered via the port queue is still
    available. If not, the next port in the queue checked. This repeats
    until an available port is found.
    
    - Changes the port from a LIFO to a FIFO. As a LIFO, the port that was
    used in a previous test and just released would be attempted to be used
    again in the very next test. It could be that the port is still in a
    WAIT state. I'm wondering whether this caused the address in use issues.
    Changing to a FIFO should address this.
    
    - Adds debug and verbose logging so we can get a view into the logic
    being used. There's two methods of port selection, one that is preferred
    and a fallback method using a bound socket with SO_REUSEADDR. If we are
    still having issues after this change, I suggest we turn on verbose
    logging in our CI which would reveal which method is used. Note that
    verbose logging shouldn't be nearly as noisy as debug logging.
    
    Co-authored-by: bneradt <bn...@verizonmedia.com>
---
 tests/gold_tests/autest-site/ports.py | 166 +++++++++++++++++++++++++++++-----
 1 file changed, 144 insertions(+), 22 deletions(-)

diff --git a/tests/gold_tests/autest-site/ports.py b/tests/gold_tests/autest-site/ports.py
index 7932be8..4f26871 100644
--- a/tests/gold_tests/autest-site/ports.py
+++ b/tests/gold_tests/autest-site/ports.py
@@ -31,8 +31,25 @@ except ImportError:
 g_ports = None  # ports we can use
 
 
+class PortQueueSelectionError(Exception):
+    """
+    An exception for when there are problems selecting a port from the port
+    queue.
+    """
+    pass
+
+
 def PortOpen(port, address=None):
+    """
+    Detect whether the port is open, that is a socket is currently using that port.
+
+    Open ports are currently in use by an open socket and are therefore not available
+    for a server to listen on them.
 
+    Returns:
+        True if there is a connection currently listening on the port, False if
+        there is no server listening on the port currently.
+    """
     ret = False
     if address is None:
         address = "localhost"
@@ -40,25 +57,84 @@ def PortOpen(port, address=None):
     address = (address, port)
 
     try:
+        # Try to connect on that port. If we can connect on it, then someone is
+        # listening on that port and therefore the port is open.
         s = socket.create_connection(address, timeout=.5)
         s.close()
         ret = True
+        host.WriteDebug(
+            'PortOpen',
+            "Connection to port {} succeeded, the port is open, "
+            "and a future connection cannot use it".format(port))
     except socket.error:
         s = None
-        ret = False
+        host.WriteDebug(
+            'PortOpen',
+            "socket error for port {0}, port is closed, "
+            "and therefore a future connection can use it".format(port))
     except socket.timeout:
         s = None
+        host.WriteDebug(
+            'PortOpen',
+            "Timeout error for port {0}, port is closed, "
+            "and therefore a future connection can use it".format(port))
 
     return ret
 
 
-def setup_port_queue(amount=1000):
+def _get_available_port(queue):
+    """
+    Get the next available port from the port queue and return it.
+
+    Since there can be a delay between when the queue is populated and when the
+    port is requested, this checks the next port in the queue to see whether it
+    is still not open. If it isn't, it is dropped from the queue and the next
+    one is inspected. This process continues until either an available port is
+    found, or, if the queue is exhausted, PortQueueSelectionError is raised.
+
+    Returns:
+        An available (i.e., non-open) port.
+
+    Throws:
+        PortQueueSelectionError if the port queue is exhausted.
+    """
+
+    if queue.qsize() == 0:
+        host.WriteWarning("Port queue is empty.")
+        raise PortQueueSelectionError(
+                "Could not get a valid port because the queue is empty")
+
+    port = queue.get()
+    while PortOpen(port):
+        host.WriteDebug(
+            '_get_available_port'
+            "Port was open but now is used: {}".format(port))
+        if queue.qsize() == 0:
+            host.WriteWarning("Port queue is empty.")
+            raise PortQueueSelectionError(
+                    "Could not get a valid port because the queue is empty")
+        port = queue.get()
+    return port
+
+
+def _setup_port_queue(amount=1000):
+    """
+    Build up the set of ports that the OS in theory will not use.
+    """
     global g_ports
     if g_ports is None:
-        g_ports = Queue.LifoQueue()
+        host.WriteDebug(
+            '_setup_port_queue',
+            "Populating the port queue.")
+        g_ports = Queue.Queue()
     else:
+        # The queue has already been populated.
+        host.WriteDebug(
+            '_setup_port_queue',
+            "Queue was previously populated. Queue size: {}".format(g_ports.qsize()))
         return
     try:
+        # Use sysctl to find the range of ports that the OS publishes it uses.
         # some docker setups don't have sbin setup correctly
         new_env = os.environ.copy()
         new_env['PATH'] = "/sbin:/usr/sbin:" + new_env['PATH']
@@ -86,43 +162,89 @@ def setup_port_queue(amount=1000):
     rmax = 65536 - dmax
 
     if rmax > amount:
-        # fill in ports
+        # Fill in ports, starting above the upper OS-usable port range.
         port = dmax + 1
         while port < 65536 and g_ports.qsize() < amount:
-            # if port good:
             if not PortOpen(port):
+                host.WriteDebug(
+                    '_setup_port_queue',
+                    "Adding a possible port to connect to: {0}".format(port))
                 g_ports.put(port)
+            else:
+                host.WriteDebug(
+                    '_setup_port_queue',
+                    "Rejecting a possible port to connect to: {0}".format(port))
             port += 1
     if rmin > amount and g_ports.qsize() < amount:
         port = 2001
+        # Fill in more ports, starting at 2001, well above well known ports,
+        # and going up until the minimum port range used by the OS.
         while port < dmin and g_ports.qsize() < amount:
-            # if port good:
             if not PortOpen(port):
+                host.WriteDebug(
+                    '_setup_port_queue',
+                    "Adding a possible port to connect to: {0}".format(port))
                 g_ports.put(port)
+            else:
+                host.WriteDebug(
+                    '_setup_port_queue',
+                    "Rejecting a possible port to connect to: {0}".format(port))
             port += 1
 
 
-def get_port(obj, name):
-    '''
-    Get a port and set it to a variable on the object
+def _get_port_by_bind():
+    """
+    Create a socket, bind with REUSEADDR, and return the bound port.
 
-    '''
+    Because SO_REUSEADDR is applied as an option to the socket, the future
+    server should be able to use this port. This method is considered
+    sub-optimal in comparison to the OS-unused port range method used above
+    because another process might bind to this port in the meantime.
 
-    setup_port_queue()
-    if g_ports.qsize():
-        # get port
-        port = g_ports.get()
-        # assign to variable
-        obj.Variables[name] = port
-        # setup clean up step to recycle the port
-        obj.Setup.Lambda(func_cleanup=lambda: g_ports.put(
-            port), description="recycling port")
-        return port
-
-    # use old code
+    Returns:
+        A port value that can be used for a connection.
+    """
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     sock.bind(('', 0))  # bind to all interfaces on an ephemeral port
     port = sock.getsockname()[1]
+    return port
+
+
+def get_port(obj, name):
+    '''
+    Get a port and set it to the specified variable on the object.
+
+    Args:
+        obj: The object upon which to set the port variable.
+        name: The name of the variable that receives the port value.
+
+    Returns:
+        The port value.
+    '''
+    _setup_port_queue()
+    port = 0
+    if g_ports.qsize() > 0:
+        try:
+            port = _get_available_port(g_ports)
+            host.WriteVerbose(
+                    "get_port",
+                    "Using port from port queue: {}".format(port))
+            # setup clean up step to recycle the port
+            obj.Setup.Lambda(func_cleanup=lambda: g_ports.put(
+                port), description="recycling port")
+        except PortQueueSelectionError:
+            port = _get_port_by_bind()
+            host.WriteVerbose(
+                    "get_port",
+                    "Queue was drained. Using port from a bound socket: {}".format(port))
+    else:
+        # Since the queue could not be populated, use a port via bind.
+        port = _get_port_by_bind()
+        host.WriteVerbose(
+                "get_port",
+                "Queue is empty. Using port from a bound socket: {}".format(port))
+
+    # Assign to the named variable.
     obj.Variables[name] = port
     return port