@@ -22,9 +22,18 @@ internal class LocalHttpListener : IDisposable
2222 {
2323 private const int DefaultPort = 17071 ;
2424
25+ // Pick a large, fixed range of ports that are going to be valid in all environment.
26+ // Avoiding ports below 1024 as those are blocked by app service sandbox.
27+ // Ephemeral ports for most OS start well above 32768. See https://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html
28+ private const int MinPort = 30000 ;
29+ private const int MaxPort = 31000 ;
30+
2531 private readonly Func < HttpRequestMessage , Task < HttpResponseMessage > > handler ;
2632 private readonly EndToEndTraceHelper traceHelper ;
2733 private readonly DurableTaskOptions durableTaskOptions ;
34+ private readonly Random portGenerator ;
35+ private readonly HashSet < int > attemptedPorts ;
36+
2837 private IWebHost localWebHost ;
2938
3039 public LocalHttpListener (
@@ -39,6 +48,8 @@ public LocalHttpListener(
3948 // Set to a non null value
4049 this . InternalRpcUri = new Uri ( $ "http://uninitialized") ;
4150 this . localWebHost = new NoOpWebHost ( ) ;
51+ this . portGenerator = new Random ( ) ;
52+ this . attemptedPorts = new HashSet < int > ( ) ;
4253 }
4354
4455 public Uri InternalRpcUri { get ; private set ; }
@@ -59,10 +70,12 @@ public async Task StartAsync()
5970 int numAttempts = 1 ;
6071 do
6172 {
62- int availablePort = this . GetAvailablePort ( ) ;
73+ int listeningPort = numAttempts == 1
74+ ? DefaultPort
75+ : this . GetRandomPort ( ) ;
6376 try
6477 {
65- this . InternalRpcUri = new Uri ( $ "http://127.0.0.1:{ availablePort } /durabletask/") ;
78+ this . InternalRpcUri = new Uri ( $ "http://127.0.0.1:{ listeningPort } /durabletask/") ;
6679 var listenUri = new Uri ( this . InternalRpcUri . GetLeftPart ( UriPartial . Authority ) ) ;
6780 this . localWebHost = new WebHostBuilder ( )
6881 . UseKestrel ( )
@@ -80,11 +93,9 @@ public async Task StartAsync()
8093 this . durableTaskOptions . HubName ,
8194 functionName : string . Empty ,
8295 instanceId : string . Empty ,
83- message : $ "Failed to open local socket { availablePort } . This was attempt #{ numAttempts } to open a local port.") ;
96+ message : $ "Failed to open local port { listeningPort } . This was attempt #{ numAttempts } to open a local port.") ;
97+ this . attemptedPorts . Add ( listeningPort ) ;
8498 numAttempts ++ ;
85- var random = new Random ( ) ;
86- var millisecondsToWait = ( int ) Math . Round ( random . NextDouble ( ) * 1000 ) ;
87- await Task . Delay ( millisecondsToWait ) ;
8899 }
89100 }
90101 while ( numAttempts <= maxAttempts ) ;
@@ -99,6 +110,20 @@ public async Task StartAsync()
99110#endif
100111 }
101112
113+ private int GetRandomPort ( )
114+ {
115+ // Get a random port that has not already been attempted so we don't waste time trying
116+ // to listen to a port we know is not free.
117+ int randomPort ;
118+ do
119+ {
120+ randomPort = this . portGenerator . Next ( MinPort , MaxPort ) ;
121+ }
122+ while ( this . attemptedPorts . Contains ( randomPort ) ) ;
123+
124+ return randomPort ;
125+ }
126+
102127 public async Task StopAsync ( )
103128 {
104129#if ! FUNCTIONS_V1
@@ -110,29 +135,6 @@ public async Task StopAsync()
110135 this . IsListening = false ;
111136 }
112137
113- private int GetAvailablePort ( )
114- {
115- // If we are able to successfully start a listener looking on the default port without
116- // an exception, we can use the default port. Otherwise, let the TcpListener class decide for us.
117- try
118- {
119- var listener = new TcpListener ( IPAddress . Loopback , DefaultPort ) ;
120- listener . Start ( ) ;
121- listener . Stop ( ) ;
122- return DefaultPort ;
123- }
124- catch ( SocketException )
125- {
126- // Following guidance of this stack overflow answer
127- // to find available port: https://stackoverflow.com/a/150974/9035640
128- var listener = new TcpListener ( IPAddress . Loopback , 0 ) ;
129- listener . Start ( ) ;
130- int availablePort = ( ( IPEndPoint ) listener . LocalEndpoint ) . Port ;
131- listener . Stop ( ) ;
132- return availablePort ;
133- }
134- }
135-
136138 private async Task HandleRequestAsync ( HttpContext context )
137139 {
138140 try
0 commit comments