@@ -25,57 +25,73 @@ package testsuite
25
25
import (
26
26
"fmt"
27
27
"net"
28
+ "runtime"
28
29
)
29
30
30
- // Modified from Temporalite which itself modified from
31
- // https://github.com/phayes/freeport /blob/95f893ade6f232a5f1511d61735d89b1ae2df543 /freeport.go
31
+ // Copied and adapted from
32
+ // https://github.com/temporalio/cli /blob/350cb2f9dca55e5063b39ffbdaa2739fdeab4399/temporalcli/devserver /freeport.go
32
33
33
- func newPortProvider () * portProvider {
34
- return & portProvider {}
35
- }
36
-
37
- type portProvider struct {
38
- listeners []* net.TCPListener
39
- }
40
-
41
- // GetFreePort asks the kernel for a free open port that is ready to use.
42
- // Returns the interface's IP and the free port.
43
- func (p * portProvider ) GetFreePort () (string , int , error ) {
44
- addr , err := net .ResolveTCPAddr ("tcp" , "127.0.0.1:0" )
45
- if err != nil {
46
- if addr , err = net .ResolveTCPAddr ("tcp6" , "[::1]:0" ); err != nil {
47
- return "" , 0 , fmt .Errorf ("failed to get free port: %w" , err )
48
- }
49
- }
50
-
51
- l , err := net .ListenTCP ("tcp" , addr )
34
+ // Returns a TCP port that is available to listen on, for the given (local) host.
35
+ //
36
+ // This works by binding a new TCP socket on port 0, which requests the OS to
37
+ // allocate a free port. There is no strict guarantee that the port will remain
38
+ // available after this function returns, but it should be safe to assume that
39
+ // a given port will not be allocated again to any process on this machine
40
+ // within a few seconds.
41
+ //
42
+ // On Unix-based systems, binding to the port returned by this function requires
43
+ // setting the `SO_REUSEADDR` socket option (Go already does that by default,
44
+ // but other languages may not); otherwise, the OS may fail with a message such
45
+ // as "address already in use". Windows default behavior is already appropriate
46
+ // in this regard; on that platform, `SO_REUSEADDR` has a different meaning and
47
+ // should not be set (setting it may have unpredictable consequences).
48
+ func getFreePort (host string ) (string , int , error ) {
49
+ l , err := net .Listen ("tcp" , host + ":0" )
52
50
if err != nil {
53
- return "" , 0 , err
51
+ return "" , 0 , fmt . Errorf ( "failed to assign a free port: %w" , err )
54
52
}
53
+ defer func () { _ = l .Close () }()
54
+ port := l .Addr ().(* net.TCPAddr ).Port
55
55
56
- p .listeners = append (p .listeners , l )
57
- tcpAddr := l .Addr ().(* net.TCPAddr )
58
-
59
- return tcpAddr .IP .String (), tcpAddr .Port , nil
60
- }
61
-
62
- func (p * portProvider ) Close () error {
63
- for _ , l := range p .listeners {
64
- if err := l .Close (); err != nil {
65
- return err
56
+ // On Linux and some BSD variants, ephemeral ports are randomized, and may
57
+ // consequently repeat within a short time frame after the listenning end
58
+ // has been closed. To avoid this, we make a connection to the port, then
59
+ // close that connection from the server's side (this is very important),
60
+ // which puts the connection in TIME_WAIT state for some time (by default,
61
+ // 60s on Linux). While it remains in that state, the OS will not reallocate
62
+ // that port number for bind(:0) syscalls, yet we are not prevented from
63
+ // explicitly binding to it (thanks to SO_REUSEADDR).
64
+ //
65
+ // On macOS and Windows, the above technique is not necessary, as the OS
66
+ // allocates ephemeral ports sequentially, meaning a port number will only
67
+ // be reused after the entire range has been exhausted. Quite the opposite,
68
+ // given that these OSes use a significantly smaller range for ephemeral
69
+ // ports, making an extra connection just to reserve a port might actually
70
+ // be harmful (by hastening ephemeral port exhaustion).
71
+ if runtime .GOOS != "darwin" && runtime .GOOS != "windows" {
72
+ r , err := net .DialTCP ("tcp" , nil , l .Addr ().(* net.TCPAddr ))
73
+ if err != nil {
74
+ return "" , 0 , fmt .Errorf ("failed to assign a free port: %w" , err )
75
+ }
76
+ c , err := l .Accept ()
77
+ if err != nil {
78
+ return "" , 0 , fmt .Errorf ("failed to assign a free port: %w" , err )
66
79
}
80
+ // Closing the socket from the server side
81
+ _ = c .Close ()
82
+ defer func () { _ = r .Close () }()
67
83
}
68
- return nil
84
+
85
+ return host , port , nil
69
86
}
70
87
71
88
func getFreeHostPort () (string , error ) {
72
- pp := newPortProvider ()
73
- host , port , err := pp .GetFreePort ()
74
- closeErr := pp .Close ()
89
+ host , port , err := getFreePort ("127.0.0.1" )
75
90
if err != nil {
76
- return "" , err
77
- } else if closeErr != nil {
78
- return "" , fmt .Errorf ("failed to close TCP listener: %w" , closeErr )
91
+ host , port , err = getFreePort ("[::1]" )
92
+ if err != nil {
93
+ return "" , err
94
+ }
79
95
}
80
96
return fmt .Sprintf ("%v:%v" , host , port ), nil
81
97
}
0 commit comments