2

I have a server (asp.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:
So, 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.Action`2<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/<ConnectAsync>d__3:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/<StartAsync>d__40:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/<StartAsyncCore>d__41:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/<SelectAndStartTransport>d__44:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<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/<NegotiateAsync>d__45:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<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/<SendAsync>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;
}
Perazim
  • 1,501
  • 3
  • 19
  • 42
  • 1
    If you already have `.WithAutomaticReconnect(...)` configured you don't need to invoke the `StartConnection` method again, since the `WithAutomaticReconnect` method will try to reconnect 5 seconds (in your case) after each failed attempt. – PhobosFerro Mar 06 '23 at 10:56
  • 1
    Thanks for the answer. I understand it differently. You can customize the number of retry attempts and delays by passing the desired values as an array in the `WithAutomaticReconnect`. In this case it will retry exactly once after the five seconds have elapsed. After that, however, the connection is closed and the `Closed` event is called. – Perazim Mar 06 '23 at 11:30
  • 1
    You're right, sorry. I was talking about a RetryPolicy which I mistakingly thought you were using. Did you try calling your reconnection method in a new thread? – PhobosFerro Mar 06 '23 at 11:38
  • 1
    No I didn't. I could try that. I just looked up what I can do with a RetryPolicy and saw that I can also make an [infinite connection attempt process](https://stackoverflow.com/a/73579021/5818081). I'm gonna try that first :) – Perazim Mar 06 '23 at 11:48
  • The reconnection could be blocking the UI thread, so it's worth giving it a try! – PhobosFerro Mar 06 '23 at 11:54
  • 1
    @PhobosFerro you can write an answer on this post so I can mark it as the right answer that that led me to the solution. I tried the referenced RetryPolicy and that worked for me :) – Perazim Mar 06 '23 at 13:14

1 Answers1

1

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.

PhobosFerro
  • 737
  • 6
  • 18