dsuryd / dotNetify

Simple, lightweight, yet powerful way to build real-time web apps.
https://dotnetify.net
Other
1.17k stars 164 forks source link

Reconnect issue when connection lost #297

Closed BajakiGabesz closed 2 years ago

BajakiGabesz commented 3 years ago

Hi,

I use dotNetify in a Xamarin Forms application and I find an issue when the connection has been lost. In this case, we cannot communicate with the remote view model anymore, meaning you are not able to dispatch any command and not get any updates from the server. I tried to create my own mechanism that just monitoring the connection and if it's terminated then I try to reconnect:

private void ConnectivityCheck_Tick(object state)
{
    if (ForceReconnectRequested
        || (ServerConnectionState != HubConnectionState.Connected
            && NetworkAccess == NetworkAccess.Internet))
    {
        ((IDisposable)this.dotnetifyHubProxy).Dispose();
        this.dotnetifyHubProxy.StartAsync().ConfigureAwait(false);
        SpinWait.SpinUntil(() => ServerConnectionState != HubConnectionState.Connected, ConstantsSystem.connectionTimeout);
        Reconnected?.Invoke();
        ForceReconnectRequested = false;
    }
}

The page view model looks like this:

public class SomePageViewModel
{
    private IDotNetifyClient? dotnetify;

    public SomePageViewModel()
    {
        ConnectAsync().ConfigureAwait(false);
    }

    ~SomePageViewModel()
    {
        Dispose();
    }

    public async Task ConnectAsync()
    {
        if (dotnetify != null)
            await dotnetify.DisposeAsync();

        this.dotnetify = ViewModelLocator.ResolveService<IDotNetifyClient>();

        await dotnetify.ConnectAsync(
           "something",
           this
        );
    }

    protected virtual void OnReconnected()
    {
        ConnectAsync().ConfigureAwait(false);
    }

    public override void Dispose()
    {
        dotnetify?.DisposeAsync();
        base.Dispose();
    }
}

The backend side looks like this:

public class Something : RemoteVMBase
{
    public Something()
    {
    }

    public override async Task OnCreatedAsync()
    {
        // Getting data from services here...
        await base.OnCreatedAsync();
    }
}

So, the main problem is, even if I trying to reconnect manually or keep everything as it is, the communication between the client and the backend totally lost, and not able to work with the data anymore. When opening a new ViewModel then also not able to get any data from the server. Maybe I just "overlook" something and you have an idea why is it happening.

What is the right way to handle the situation when the client connection to the server is totally terminated? Should it connect back automatically or do I need to add some extra argument to do so or is there a workaround for this?

Thanks for your kind help!

dsuryd commented 3 years ago

I made a PR to address the reconnection issue. Let me know if it works.

BajakiGabesz commented 3 years ago

Thanks for your effort, I will check it tonight. If you don't mind and I find any issues with it that I could recognize, then I could contribute to resolving it.

BajakiGabesz commented 3 years ago

I was tried to test it but I got an error message when I just tried to connect to the server:

System.ArgumentNullException: Value cannot be null. Parameter name: inputType at System.Text.Json.JsonSerializer.Serialize (System.Text.Json.Utf8JsonWriter writer, System.Object value, System.Type inputType, System.Text.Json.JsonSerializerOptions options) [0x00009] in <3e024d1008104aac9fe12198f4c68344>:0 at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.WriteArguments (System.Object[] arguments, System.Text.Json.Utf8JsonWriter writer) [0x00062] in <98d61c04d1b44672b33d354f73c63eae>:0 at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.WriteInvocationMessage (Microsoft.AspNetCore.SignalR.Protocol.InvocationMessage message, System.Text.Json.Utf8JsonWriter writer) [0x0001f] in <98d61c04d1b44672b33d354f73c63eae>:0 at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.WriteMessageCore (Microsoft.AspNetCore.SignalR.Protocol.HubMessage message, System.Buffers.IBufferWriter1[T] stream) [0x00080] in <98d61c04d1b44672b33d354f73c63eae>:0 at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.WriteMessage (Microsoft.AspNetCore.SignalR.Protocol.HubMessage message, System.Buffers.IBufferWriter1[T] output) [0x00000] in <98d61c04d1b44672b33d354f73c63eae>:0 at Microsoft.AspNetCore.SignalR.Client.HubConnection.SendHubMessage (Microsoft.AspNetCore.SignalR.Client.HubConnection+ConnectionState connectionState, Microsoft.AspNetCore.SignalR.Protocol.HubMessage hubMessage, System.Threading.CancellationToken cancellationToken) [0x00035] in <82f2bec582b048f7a648dfe730be82f1>:0 at Microsoft.AspNetCore.SignalR.Client.HubConnection.SendCoreAsyncCore (System.String methodName, System.Object[] args, System.Threading.CancellationToken cancellationToken) [0x00169] in <82f2bec582b048f7a648dfe730be82f1>:0 at System.Threading.Tasks.ForceAsyncAwaiter.GetResult () [0x0000c] in <82f2bec582b048f7a648dfe730be82f1>:0 at Microsoft.AspNetCore.SignalR.Client.HubConnection.SendCoreAsync (System.String methodName, System.Object[] args, System.Threading.CancellationToken cancellationToken) [0x00097] in <82f2bec582b048f7a648dfe730be82f1>:0 at DotNetify.Client.DotNetifyHubProxy.Request_VM (System.String vmId, System.Collections.Generic.Dictionary`2[TKey,TValue] vmArg) [0x00052] in /Users/gabor/Documents/Sources/AppName/Dotnetify/DotNetifyLib.SignalR/Client/DotNetifyHubProxy.cs:162 at DotNetify.Client.DotNetifyClient.ConnectAsync (System.String vmId, DotNetify.Client.IViewState viewState, DotNetify.Client.VMConnectOptions options) [0x00227] in /Users/gabor/Documents/Sources/AppName/Dotnetify/DotNetifyLib.Core/Client/DotNetifyClient.cs:153 at DotNetify.Client.DotNetifyClient.ConnectAsync (System.String vmId, System.ComponentModel.INotifyPropertyChanged view, DotNetify.Client.VMConnectOptions options) [0x00042] in /Users/gabor/Documents/Sources/AppName/Dotnetify/DotNetifyLib.Core/Client/DotNetifyClient.cs:121 at AppName.Models.General.RemoteViewModelBase.ConnectAsync () [0x0012a] in /Users/gabor/Documents/Sources/AppName/AppName/AppName/Models/General/RemoteViewModelBase.cs:91

I also updated the server to work with the same version of dotNetify which is in the PR, but it's not working properly anymore. I was on version 4.1.1 right before I moved to this version. Is it possible that I need to do something to be able to work with this workaround what the PR contains?

Side note: I have a React admin page for the same application and it's working with the current version but Xamarin cannot connect anymore.

dsuryd commented 3 years ago

I'm not seeing the error with the DotNetClient demo. Can you try to reproduce it there? Make sure both client and server are on the same .NET version. You can also try setting the serialization method to Newtonsoft.

BajakiGabesz commented 3 years ago

Actually, I could only use .NET Standard 2.0 or 2.1 in Xamarin. I tried both but I saw the DotNetifyLib.Core project uses 2.0. So, I just created a small application which just could connect to the server and get a list of objects. The same thing happened. I couldn't test the demo client, because I'm on macOS and not able to build those projects because it's incompatible with my OS.

The problem happens when it's trying to call the ConnectAsync method which never returns. It calls in DotNetifyHubProxy.cs:162 the Request_VM method which calls HubConnection.SendCoreAsync with the desired parameters but it's never returned. While the application runs the ConnectAsync looks like never happens and not returning as well. I just tried to use Wireshark to dig a bit deeper and I saw the standard communication happens properly, but a bit later in the initialization process, the communication contains the next message: {"type":7,"error":"Connection closed with an error.","allowReconnect":true}\036 It looks like the sending the request message is also delayed exactly 15 seconds: {"type":1,"target":"Request_VM","arguments":["vmName"{"type":6}\036 And also this message differs from the React variant: {"type":1,"target":"Response_VM","arguments":[["vmName" When monitoring the server part in DotNetifyHub we receive messages from the client when I use React DotNetifyHub.cs:125 But when Xamarin project do the same, it gets never fired.

Is this maybe a SignalR issue? How can I change the serialization method to Newtonsoft?

I can send you a sample project if you need it, as I already created it, just let me know.

Thanks for your help!

dsuryd commented 3 years ago

I can take a look if you put a sample project in a public repo. BTW, your client, is it still on version 4? If so, you'll need to upgrade it too. Here's the info on how to switch to Newtonsoft.

BajakiGabesz commented 3 years ago

I will check the communication with Newtonsoft as well. The client and the server also use the latest update from this branch.

BajakiGabesz commented 3 years ago

I just created a sample application to be able to test it on your side. You can find it here.

dsuryd commented 3 years ago

For some reason, I wasn't able to get your sample application to show up on my android emulator (pixel 3). But I could create a Xamarin client demo from scratch (source code added to PR).

The connection retry logic in the PR is working, but there is a long delay of 2 minutes after a server restart until the Xamarin client re-establish the connection. This does not happen with the desktop client. I reduced all the timeout settings exposed by the SignalR connection API to no effect. So perhaps it's another setting that the SignalR API doesn't expose, or it has something to do with the Android itself. At this point, I feel it's no longer an issue with dotNetify, and you should take it up in the SignalR forum.

BajakiGabesz commented 3 years ago

Thank you for your effort. It's really close to that solution what I did on my side. I will check it, but I think we were faced with the same issue, because sometimes I had the feeling that I could reconnect or the SignalR could, but it happened only once when I have tested it. If you don't mind, I make some tests on my side, I could create the UI side of the Xamarin demo application because in this case, others couldn't use it for an upcoming solution, because they don't see how they can bind it to the UI. Please give me some time before closing this issue to be able to test it deeper! Thank you!

dsuryd commented 2 years ago

I'm closing this as the reconnection issue has been addressed with the latest release v5.2. As for the prolonged time it takes for the Xamarin client to re-establish the connection, it doesn't appear to be an issue with dotNetify.