github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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 }