2

In the OnDisconnectedAsync event of my hub, I want to wait a few second before performing some action. I tried to make it async for using non-blocking Task.Delay:

    public override async Task OnDisconnectedAsync(Exception exception) {
        var session = (VBLightSession)Context.Items["Session"];
        activeUsers.Remove(session.User.Id);

        await Task.Delay(5000);
        if(!activeUsers.Any(u => u.Key == session.User.Id)) {
            await Clients.All.SendAsync("UserOffline", UserOnlineStateDto(session));
        }

        await base.OnDisconnectedAsync(exception);
    }

While this works as expected, I noticed that I can't close the console application immediately. Seems that it waits for the 5 seconds delay to finish. How can I solve this, that exiting the application simply exite those delay, too?

The only alternative I see is Creating a classic thread and inject IHubContext, but this seems not scaling well and some kind of overkill for this simple task.

Backgrund

I have a list of online users. When users navigating through the multi page application, they get disconnected for a short time during the new HTTP request. To avoid such flickering in the online list (user get offline and directly online again), I want to remove the user on disconnect from the user list, but not notify the WS client immediatly.

Instead I want to wait 5 seconds. Only if the client is still missing in the list, I know the client hasn't reconnected and I notify the other users. For this purpose, I need to sleep on the disconnect event. The above solution works well, except the delay on application exit (which is annoying during development).

A single page application like Angular or other frameworks shouldn't be used for a few reasons, mainly performance and SEO.

Lion
  • 16,606
  • 23
  • 86
  • 148

2 Answers2

4

I learned about the CancellationToken, which could be passed to Task.Wait. It could be used to abort the task. Creating such a token using CancellationTokenSource seems good to cancel the token programatically (e.g. on some condition).

But I found the ApplicationStopping token in the IApplicationLifetime interface, that requests cancellation when the application is shutting down. So I could simply inject

namespace MyApp.Hubs {
    public class MyHub : Hub {
        readonly IApplicationLifetime appLifetime;
        static Dictionary<int, VBLightSession> activeUsers = new Dictionary<int, VBLightSession>();
        public MyHub(IApplicationLifetime appLifetime) {
            this.appLifetime = appLifetime;
        }
    }
}

and only sleep if no cancellation is requested from this token

public override async Task OnDisconnectedAsync(Exception exception) {
    var session = (VBLightSession)Context.Items["Session"];
    activeUsers.Remove(session.User.Id);
    // Prevents our application waiting to the delay if it's closed (especially during development this avoids additionally waiting time, since the clients disconnects there)
    if (!appLifetime.ApplicationStopping.IsCancellationRequested) {
        // Avoids flickering when the user switches to another page, that would cause a directly re-connect after he has disconnected. If he's still away after 5s, he closed the tab
        await Task.Delay(5000);
        if (!activeUsers.Any(u => u.Key == session.User.Id)) {
            await Clients.All.SendAsync("UserOffline", UserOnlineStateDto(session));
        }
    }
    await base.OnDisconnectedAsync(exception);
}

This works because when closing the application, SignalR detects this as disconnect (altough it's caused from the server). So he waits 5000s before exit, like I assumed in my question. But with the token, IsCancellationRequested is set to true, so no additional waiting in this case.

Lion
  • 16,606
  • 23
  • 86
  • 148
1

I would not have a problem with the program waiting the X seconds before close, because it is granted that no operation will stays in the middle of the operation.

For example, what if the operation was doing something in the database? IT could leave the connection open.

I would introduce a CancellationToken, and, instead of waiting 5 seconds, wait 1 or less and check again

var cancellationToken = new CancellationToken(); // This in some singleton service

And then

var cont = 0;

while (cont < 5 && !cancellationToken.IsCancellationRequested)
{
    Task.Delay(1000);
    cont++;
}

if (cancellationToken.IsCancellationRequested)
{
    return;
}
// Do something

Then you can add IApplicationLifetime to let the app know when the signal to shit down comes and cancel your CancellationToken.

You could even get this code in other class and generalize it to use it in other places.

rekiem87
  • 1,565
  • 18
  • 33
  • SignalR seems firing the `OnDisconnectedAsync` event when the connection is closed by the server side. But in the meantime I found [a blogpost](https://weblogs.thinktecture.com/pawel/2017/08/aspnet-core-in-production-graceful-shutdown-and-reacting-to-aborted-requests.html) that explains `IApplicationLifetime.ApplicationStopping` cancellation token. If I check this, the delay is not executed and I don't have issues duting application exit. I'll post this more detailled in a seperate answear. – Lion Sep 06 '19 at 21:29
  • After your edit the new code seems to be a similar approach like mine. By dividing the waiting time, you reduce the delay if an user disconnects _during_ the shutdown. Seems a good idea for a high loading site (howver I dont know how much the multiple waits cost compared to a single one). My code only checks once and wait the hole time if no shutdown is present at the moment of checking – Lion Sep 06 '19 at 21:35
  • 1
    You should not worry much about the load, async is very efficient, yes, there will be more load, but I would not worry to become a bottle neck, until starts happening in reality, and still you would have more adjustments, maybe just 2 waits, maybe 10. If it is just for development you can check the environment so you just wait when you are in developing mode. But I would say @Lion answer is more complete, I would try a combination of both, his method but waiting with 2 or 3 cycles, so the app can stop fast even after the event is thrown – rekiem87 Sep 06 '19 at 21:52
  • 1
    And I can say, I have this kind of delays in my app, for waits about 10 seconds, with 20 delays (one each 500ms) and the load differences between waiting just once and 20 are negligible with a process working with 2 million transactions per day. – rekiem87 Sep 06 '19 at 21:56