@@ -22,10 +22,10 @@ internal class LocalHttpListener : IDisposable
2222 {
2323 private const int DefaultPort = 17071 ;
2424
25- private readonly IWebHost localWebHost ;
2625 private readonly Func < HttpRequestMessage , Task < HttpResponseMessage > > handler ;
2726 private readonly EndToEndTraceHelper traceHelper ;
2827 private readonly DurableTaskOptions durableTaskOptions ;
28+ private IWebHost localWebHost ;
2929
3030 public LocalHttpListener (
3131 EndToEndTraceHelper traceHelper ,
@@ -36,23 +36,12 @@ public LocalHttpListener(
3636 this . handler = handler ?? throw new ArgumentNullException ( nameof ( handler ) ) ;
3737 this . durableTaskOptions = durableTaskOptions ?? throw new ArgumentNullException ( nameof ( durableTaskOptions ) ) ;
3838
39- #if ! FUNCTIONS_V1
40- this . InternalRpcUri = new Uri ( $ "http://127.0.0.1:{ this . GetAvailablePort ( ) } /durabletask/") ;
41- var listenUri = new Uri ( this . InternalRpcUri . GetLeftPart ( UriPartial . Authority ) ) ;
42- this . localWebHost = new WebHostBuilder ( )
43- . UseKestrel ( )
44- . UseUrls ( listenUri . OriginalString )
45- . Configure ( a => a . Run ( this . HandleRequestAsync ) )
46- . Build ( ) ;
47- #else
48- // Just use default port for internal Uri. No need to check for port availability since
49- // we won't be listening to this endpoint.
50- this . InternalRpcUri = new Uri ( $ "http://127.0.0.1:{ DefaultPort } /durabletask/") ;
39+ // Set to a non null value
40+ this . InternalRpcUri = new Uri ( $ "http://uninitialized") ;
5141 this . localWebHost = new NoOpWebHost ( ) ;
52- #endif
5342 }
5443
55- public Uri InternalRpcUri { get ; }
44+ public Uri InternalRpcUri { get ; private set ; }
5645
5746 public bool IsListening { get ; private set ; }
5847
@@ -66,8 +55,44 @@ public async Task StartAsync()
6655 }
6756
6857#if ! FUNCTIONS_V1
69- await this . localWebHost . StartAsync ( ) ;
70- this . IsListening = true ;
58+ const int maxAttempts = 10 ;
59+ int numAttempts = 1 ;
60+ do
61+ {
62+ int availablePort = this . GetAvailablePort ( ) ;
63+ try
64+ {
65+ this . InternalRpcUri = new Uri ( $ "http://127.0.0.1:{ availablePort } /durabletask/") ;
66+ var listenUri = new Uri ( this . InternalRpcUri . GetLeftPart ( UriPartial . Authority ) ) ;
67+ this . localWebHost = new WebHostBuilder ( )
68+ . UseKestrel ( )
69+ . UseUrls ( listenUri . OriginalString )
70+ . Configure ( a => a . Run ( this . HandleRequestAsync ) )
71+ . Build ( ) ;
72+
73+ await this . localWebHost . StartAsync ( ) ;
74+ this . IsListening = true ;
75+ break ;
76+ }
77+ catch ( IOException )
78+ {
79+ this . traceHelper . ExtensionWarningEvent (
80+ this . durableTaskOptions . HubName ,
81+ functionName : string . Empty ,
82+ instanceId : string . Empty ,
83+ message : $ "Failed to open local socket { availablePort } . This was attempt #{ numAttempts } to open a local port.") ;
84+ numAttempts ++ ;
85+ var random = new Random ( ) ;
86+ var millisecondsToWait = ( int ) Math . Round ( random . NextDouble ( ) * 1000 ) ;
87+ await Task . Delay ( millisecondsToWait ) ;
88+ }
89+ }
90+ while ( numAttempts <= maxAttempts ) ;
91+
92+ if ( ! this . IsListening )
93+ {
94+ throw new IOException ( $ "Unable to find a port to open an RPC endpoint on after { maxAttempts } attempts") ;
95+ }
7196#else
7297 // no-op: this is dummy code to make build warnings go away
7398 await Task . Yield ( ) ;
@@ -158,7 +183,6 @@ private static async Task SetResponseAsync(HttpContext context, HttpResponseMess
158183 }
159184 }
160185
161- #if FUNCTIONS_V1
162186 private class NoOpWebHost : IWebHost
163187 {
164188 public IFeatureCollection ServerFeatures => throw new NotImplementedException ( ) ;
@@ -173,6 +197,5 @@ public void Start() { }
173197
174198 public Task StopAsync ( CancellationToken cancellationToken = default ( CancellationToken ) ) => Task . CompletedTask ;
175199 }
176- #endif
177200 }
178201}
0 commit comments