github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/helper/freeport/freeport.go (about)

     1  // Copied from github.com/hashicorp/consul/sdk/freeport
     2  //
     3  // and tweaked for use by Nomad.
     4  package freeport
     5  
     6  import (
     7  	"container/list"
     8  	"fmt"
     9  	"math/rand"
    10  	"net"
    11  	"os"
    12  	"runtime"
    13  	"sync"
    14  	"time"
    15  )
    16  
    17  // todo(shoenig)
    18  //  There is a conflict between this copy of the updated sdk/freeport package
    19  //  and the lib/freeport package that is vendored as of nomad v0.10.x, which
    20  //  means we need to be careful to avoid the ports that transitive dependency
    21  //  is going to use (i.e. 10,000+). For now, we use the 9XXX port range with
    22  //  small blocks which means some tests will have to wait, and we need to be
    23  //  very careful not to leak ports.
    24  
    25  const (
    26  	// blockSize is the size of the allocated port block. ports are given out
    27  	// consecutively from that block and after that point in a LRU fashion.
    28  	// blockSize = 1500
    29  	blockSize = 100 // todo(shoenig) revert once consul dependency is updated
    30  
    31  	// maxBlocks is the number of available port blocks before exclusions.
    32  	// maxBlocks = 30
    33  	maxBlocks = 10 // todo(shoenig) revert once consul dependency is updated
    34  
    35  	// lowPort is the lowest port number that should be used.
    36  	// lowPort = 10000
    37  	lowPort = 9000 // todo(shoenig) revert once consul dependency is updated
    38  
    39  	// attempts is how often we try to allocate a port block
    40  	// before giving up.
    41  	attempts = 10
    42  )
    43  
    44  var (
    45  	// effectiveMaxBlocks is the number of available port blocks.
    46  	// lowPort + effectiveMaxBlocks * blockSize must be less than 65535.
    47  	effectiveMaxBlocks int
    48  
    49  	// firstPort is the first port of the allocated block.
    50  	firstPort int
    51  
    52  	// lockLn is the system-wide mutex for the port block.
    53  	lockLn net.Listener
    54  
    55  	// mu guards:
    56  	// - pendingPorts
    57  	// - freePorts
    58  	// - total
    59  	mu sync.Mutex
    60  
    61  	// once is used to do the initialization on the first call to retrieve free
    62  	// ports
    63  	once sync.Once
    64  
    65  	// condNotEmpty is a condition variable to wait for freePorts to be not
    66  	// empty. Linked to 'mu'
    67  	condNotEmpty *sync.Cond
    68  
    69  	// freePorts is a FIFO of all currently free ports. Take from the front,
    70  	// and return to the back.
    71  	freePorts *list.List
    72  
    73  	// pendingPorts is a FIFO of recently freed ports that have not yet passed
    74  	// the not-in-use check.
    75  	pendingPorts *list.List
    76  
    77  	// total is the total number of available ports in the block for use.
    78  	total int
    79  )
    80  
    81  // initialize is used to initialize freeport.
    82  func initialize() {
    83  	var err error
    84  	effectiveMaxBlocks, err = adjustMaxBlocks()
    85  	if err != nil {
    86  		panic("freeport: ephemeral port range detection failed: " + err.Error())
    87  	}
    88  	if effectiveMaxBlocks < 0 {
    89  		panic("freeport: no blocks of ports available outside of ephemeral range")
    90  	}
    91  	if lowPort+effectiveMaxBlocks*blockSize > 65535 {
    92  		panic("freeport: block size too big or too many blocks requested")
    93  	}
    94  
    95  	rand.Seed(time.Now().UnixNano())
    96  	firstPort, lockLn = alloc()
    97  
    98  	condNotEmpty = sync.NewCond(&mu)
    99  	freePorts = list.New()
   100  	pendingPorts = list.New()
   101  
   102  	// fill with all available free ports
   103  	for port := firstPort + 1; port < firstPort+blockSize; port++ {
   104  		if used := isPortInUse(port); !used {
   105  			freePorts.PushBack(port)
   106  		}
   107  	}
   108  	total = freePorts.Len()
   109  
   110  	go checkFreedPorts()
   111  }
   112  
   113  func checkFreedPorts() {
   114  	ticker := time.NewTicker(250 * time.Millisecond)
   115  	for {
   116  		<-ticker.C
   117  		checkFreedPortsOnce()
   118  	}
   119  }
   120  
   121  func checkFreedPortsOnce() {
   122  	mu.Lock()
   123  	defer mu.Unlock()
   124  
   125  	pending := pendingPorts.Len()
   126  	remove := make([]*list.Element, 0, pending)
   127  	for elem := pendingPorts.Front(); elem != nil; elem = elem.Next() {
   128  		port := elem.Value.(int)
   129  		if used := isPortInUse(port); !used {
   130  			freePorts.PushBack(port)
   131  			remove = append(remove, elem)
   132  		}
   133  	}
   134  
   135  	retained := pending - len(remove)
   136  
   137  	if retained > 0 {
   138  		logf("WARN", "%d out of %d pending ports are still in use; something probably didn't wait around for the port to be closed!", retained, pending)
   139  	}
   140  
   141  	if len(remove) == 0 {
   142  		return
   143  	}
   144  
   145  	for _, elem := range remove {
   146  		pendingPorts.Remove(elem)
   147  	}
   148  
   149  	condNotEmpty.Broadcast()
   150  }
   151  
   152  // adjustMaxBlocks avoids having the allocation ranges overlap the ephemeral
   153  // port range.
   154  func adjustMaxBlocks() (int, error) {
   155  	ephemeralPortMin, ephemeralPortMax, err := getEphemeralPortRange()
   156  	if err != nil {
   157  		return 0, err
   158  	}
   159  
   160  	if ephemeralPortMin <= 0 || ephemeralPortMax <= 0 {
   161  		logf("INFO", "ephemeral port range detection not configured for GOOS=%q", runtime.GOOS)
   162  		return maxBlocks, nil
   163  	}
   164  
   165  	logf("INFO", "detected ephemeral port range of [%d, %d]", ephemeralPortMin, ephemeralPortMax)
   166  	for block := 0; block < maxBlocks; block++ {
   167  		min := lowPort + block*blockSize
   168  		max := min + blockSize
   169  		overlap := intervalOverlap(min, max-1, ephemeralPortMin, ephemeralPortMax)
   170  		if overlap {
   171  			logf("INFO", "reducing max blocks from %d to %d to avoid the ephemeral port range", maxBlocks, block)
   172  			return block, nil
   173  		}
   174  	}
   175  	return maxBlocks, nil
   176  }
   177  
   178  // alloc reserves a port block for exclusive use for the lifetime of the
   179  // application. lockLn serves as a system-wide mutex for the port block and is
   180  // implemented as a TCP listener which is bound to the firstPort and which will
   181  // be automatically released when the application terminates.
   182  func alloc() (int, net.Listener) {
   183  	for i := 0; i < attempts; i++ {
   184  		block := int(rand.Int31n(int32(effectiveMaxBlocks)))
   185  		firstPort := lowPort + block*blockSize
   186  		ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", firstPort))
   187  		if err != nil {
   188  			continue
   189  		}
   190  		// logf("DEBUG", "allocated port block %d (%d-%d)", block, firstPort, firstPort+blockSize-1)
   191  		return firstPort, ln
   192  	}
   193  	panic("freeport: cannot allocate port block")
   194  }
   195  
   196  // MustTake is the same as Take except it panics on error.
   197  func MustTake(n int) (ports []int) {
   198  	ports, err := Take(n)
   199  	if err != nil {
   200  		panic(err)
   201  	}
   202  	return ports
   203  }
   204  
   205  // Take returns a list of free ports from the allocated port block. It is safe
   206  // to call this method concurrently. Ports have been tested to be available on
   207  // 127.0.0.1 TCP but there is no guarantee that they will remain free in the
   208  // future.
   209  func Take(n int) (ports []int, err error) {
   210  	if n <= 0 {
   211  		return nil, fmt.Errorf("freeport: cannot take %d ports", n)
   212  	}
   213  
   214  	mu.Lock()
   215  	defer mu.Unlock()
   216  
   217  	// Reserve a port block
   218  	once.Do(initialize)
   219  
   220  	if n > total {
   221  		return nil, fmt.Errorf("freeport: block size too small")
   222  	}
   223  
   224  	for len(ports) < n {
   225  		for freePorts.Len() == 0 {
   226  			if total == 0 {
   227  				return nil, fmt.Errorf("freeport: impossible to satisfy request; there are no actual free ports in the block anymore")
   228  			}
   229  			condNotEmpty.Wait()
   230  		}
   231  
   232  		elem := freePorts.Front()
   233  		freePorts.Remove(elem)
   234  		port := elem.Value.(int)
   235  
   236  		if used := isPortInUse(port); used {
   237  			// Something outside of the test suite has stolen this port, possibly
   238  			// due to assignment to an ephemeral port, remove it completely.
   239  			logf("WARN", "leaked port %d due to theft; removing from circulation", port)
   240  			total--
   241  			continue
   242  		}
   243  
   244  		ports = append(ports, port)
   245  	}
   246  
   247  	// logf("DEBUG", "free ports: %v", ports)
   248  	return ports, nil
   249  }
   250  
   251  // Return returns a block of ports back to the general pool. These ports should
   252  // have been returned from a call to Take().
   253  func Return(ports []int) {
   254  	if len(ports) == 0 {
   255  		return // convenience short circuit for test ergonomics
   256  	}
   257  
   258  	mu.Lock()
   259  	defer mu.Unlock()
   260  
   261  	for _, port := range ports {
   262  		if port > firstPort && port < firstPort+blockSize {
   263  			pendingPorts.PushBack(port)
   264  		}
   265  	}
   266  }
   267  
   268  func isPortInUse(port int) bool {
   269  	ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", port))
   270  	if err != nil {
   271  		return true
   272  	}
   273  	_ = ln.Close()
   274  	return false
   275  }
   276  
   277  func tcpAddr(ip string, port int) *net.TCPAddr {
   278  	return &net.TCPAddr{IP: net.ParseIP(ip), Port: port}
   279  }
   280  
   281  // intervalOverlap returns true if the doubly-inclusive integer intervals
   282  // represented by [min1, max1] and [min2, max2] overlap.
   283  func intervalOverlap(min1, max1, min2, max2 int) bool {
   284  	if min1 > max1 {
   285  		logf("WARN", "interval1 is not ordered [%d, %d]", min1, max1)
   286  		return false
   287  	}
   288  	if min2 > max2 {
   289  		logf("WARN", "interval2 is not ordered [%d, %d]", min2, max2)
   290  		return false
   291  	}
   292  	return min1 <= max2 && min2 <= max1
   293  }
   294  
   295  func logf(severity string, format string, a ...interface{}) {
   296  	_, _ = fmt.Fprintf(os.Stderr, "["+severity+"] freeport: "+format+"\n", a...)
   297  }