I have a server (ASP.NET Core Web API project) and a client (Unity/HoloLens). The connection works fine under normal circumstances. However, I wanted to see what happens in case of a disconnect and what I can do to reconnect.
The workflow
I start the server, connect the client to it and close the server.
What happens in the code
After disconnecting, the defined behaviour in WithAutomaticReconnect
gets executed and then the connection gets closed.
After the closure I want to establish a new connection. Therefore I restart the connection from the method Closed
by invoking my action. StartAsync
results in a HttpRequestException
and is working as expected, because the server is still offline. The strange thing here is that as soon as this happens, the FPS of my application (on the HoloLens) drops from 60FPS to 10FPS. It stays maybe 20 seconds that low and then rises up again to 60FPS. What is happening here?
Error
Default: ⨯[SignalRClient]: System.Net.Http.HttpRequestException: An error occurred while sending the request
System.Net.WebException: Error: ConnectFailure (No connection could be established because the target computer refused to connect.UnityEngine.DebugLogHandler:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[])
MyLogHandler:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[]) (at Assets/Scripts/Logging/MyLogHandler.cs:29)
UnityEngine.Logger:LogError (string,object)
MyApp.Logging.MyLogger:DoLog (MyApp.Logging.DebugMode,System.Action2<string, object>,string,object,object[]) (at Assets/Scripts/Logging/MyLogger.cs:66) MyApp.Logging.MyLogger:LogError (MyApp.Logging.DebugMode,object,object[]) (at Assets/Scripts/Logging/MyLogger.cs:33) SignalRClient/<StartConnection>d__4:MoveNext () (at Assets/Scripts/Clients/SignalRClient.cs:78) System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception) Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsync>d__48:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception) Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsyncInner>d__49:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception) Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsyncCore>d__58:MoveNext () System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder
1<Microsoft.AspNetCore.Connections.ConnectionContext>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionFactory/d__3:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/ d__40:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__41:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception) Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__44:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder1<Microsoft.AspNetCore.Http.Connections.NegotiationResponse>:SetException (System.Exception) Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/<GetNegotiationResponseAsync>d__52:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder
1<Microsoft.AspNetCore.Http.Connections.NegotiationResponse>:SetException (System.Exception) Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__45:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder1<System.Net.Http.HttpResponseMessage>:SetException (System.Exception) Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler/<SendAsync>d__2:MoveNext () System.Runtime.CompilerServices.AsyncTaskMethodBuilder
1<System.Net.Http.HttpResponseMessage>:SetException (System.Exception) Microsoft.AspNetCore.Http.Connections.Client.Internal.AccessTokenHttpMessageHandler/d__3:MoveNext () System.Threading.Tasks.TaskFactory`1<System.Net.WebResponse>:FromAsyncCoreLogic (System.IAsyncR
Code:
public class SignalRClient
{
private Action _connectionClosed;
public SignalRClient()
{
_connectionClosed += Restart;
_hubConnection = new HubConnectionBuilder()
.WithUrl(ConfigModel.Config.MyUri + "/hmdHub")
.WithAutomaticReconnect(new[]{ TimeSpan.FromSeconds(5) })
.Build();
_hubConnection.Reconnecting += Reconnecting;
_hubConnection.Reconnected += Reconnected;
_hubConnection.Closed += Closed;
}
public async void Start()
{
ConfigureConnection();
_ = await StartConnection();
}
public async void Restart()
=> _ = await StartConnection();
public async Task Dispose()
{
_connectionClosed -= Restart;
_hubConnection.Reconnecting -= Reconnecting;
_hubConnection.Reconnected -= Reconnected;
_hubConnection.Closed -= Closed;
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
}
private void ConfigureConnection()
{
_hubConnection.On<Alarm>("CreateAlarm", (alarmObject) =>
{
MyLogger.Log(DebugMode.Default, "SignalRClient>CreateAlarm", $"Got new alarm with id: {alarmObject.Id} with message: {alarmObject.Message}");
});
}
private async Task<bool> StartConnection()
{
_tokenSource = new CancellationTokenSource();
while (true)
{
try
{
await _hubConnection.StartAsync(_tokenSource.Token);
MyLogger.LogSuccess(DebugMode.Default, this, "Connected to SignalR-Hub");
return true;
}
catch when (_tokenSource.IsCancellationRequested)
{
return false;
}
catch(Exception ex)
{
MyLogger.LogError(DebugMode.Default, this, ex.ToString());
await Task.Delay(5000);
}
}
}
private Task Reconnecting(Exception arg)
{
MyLogger.Log(DebugMode.Default, this, "Attempting to reconnect...");
return Task.CompletedTask;
}
private Task Reconnected(string arg)
{
MyLogger.LogSuccess(DebugMode.Default, this, "Connection restored.");
return Task.CompletedTask;
}
private Task Closed(Exception arg)
{
MyLogger.LogWarning(DebugMode.Default, this, "Connection closed.");
_connectionClosed?.Invoke();
return Task.CompletedTask;
}
private HubConnection _hubConnection;
private CancellationTokenSource _tokenSource;
}
The method WithAutomaticReconnect
could either take a TimeSpan
array, or a RetryPolicy
.
If you use WithAutomaticReconnect
, there's no point in manually calling the StartConnection
method. SignalR will take care of that reconnection for you, trying as many times as objects you have in your array (if you don't want an infinite loop), or infinitely by using a custom RetryPolicy.
You can create a custom IRetryPolicy in order to achieve that.