-1

Related to this question: I need to cancel long running loop in signalR, how do i do this?

I set up a quick way to better understand the situation for myself and found Half a solution. I'm able to cancel a task (not sure how since the CancellationTokenSource is null at .Cancel() but whatever). But there seems to be a new problem. This is my current test code:

The server side hub:

public class MyHub : Hub<IMyHub>
    {
        private CancellationTokenSource cancellationTokenSource;

        public void StartLongRunningTask()
        {
            cancellationTokenSource = new CancellationTokenSource();

            try
            {
                LongRunningTask(cancellationTokenSource.Token);
                // Task completed successfully
            }
            catch (TaskCanceledException)
            {
                // Task was canceled by the client
                // Handle cancellation logic here
            }
            finally
            {
                cancellationTokenSource.Dispose();
            }
        }

        public async void CancelLongRunningTask()
        {
            cancellationTokenSource?.Cancel();

            await Clients.All.SendStatus(false);
        }

        private void LongRunningTask(CancellationToken ct)
        {
            // Your long-running task implementation
            // Ensure to check the cancellation token periodically and stop processing if canceled
            while (!ct.IsCancellationRequested)
            {
                _ = Clients.Caller.SendStatus(true);

                _ = Task.Delay(1000, ct);
                // Continue with other work...
            }
        }
    }

The client side caller:

public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        private readonly HubConnection _hubConnection;

        public bool Running { get; set; }
        public string Status { get; set; }

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;

            _hubConnection = new HubConnectionBuilder()
                .WithUrl("https://localhost:7127/myhub") // Replace with the actual URL of your SignalR hub
                .Build();

            _ = _hubConnection.StartAsync();

            _hubConnection.On<bool>("SendStatus", (status) =>
            {
                Running = status;
            });
        }

        public async Task OnGetAsync()
        {
            // Start the SignalR connection
        }

        public async Task<IActionResult> OnPostOpenAsync(IFormCollection data)
        {
            Status = _hubConnection.State.ToString();

            return Page();
        }

        public async Task<IActionResult> OnPostStartAsync(IFormCollection data)
        {
            _ = _hubConnection.InvokeAsync("StartLongRunningTask");

            Status = _hubConnection.State.ToString();

            return Page();
        }

        public async Task<IActionResult> OnPostCancelAsync(IFormCollection data)
        {
            await _hubConnection.InvokeAsync("CancelLongRunningTask");

            Status = _hubConnection.State.ToString();

            return Page();
        }

    }

And you could guess there are buttons behind the open, start and cancel function. But a couple of interesting things happen here.

  1. If I await the invocation of LongRunningTask it will wait untill it is finished, but it never is until I cancel it. So far it's logical to me.

  2. But when I don't await LongRunningTask(CancellationTokenSource.Token); in the Server hub. It should not wait for this task to finish right? Or am I missing something here?

  3. I'm not sure why but Clients.Caller.SendStatus(true) won't send anything untill I move it to the StartlongRunningTask() in the server hub. I could think of a reason why this is but i'm not sure and I would like to be so if someone can explain this behaviour to me, that would be great.

  4. Last but not least, the core reason I made this post. If i await the invocation on the clientside it will wait before I clicked cancel. But when I do this is the result in memory and cpu usage:

enter image description here

The first blue line is where I start the long running task and the second is where I cancel the task. I believe somehow the task keeps running in the background but the frontend stops loading as soon as i click and the "CancelLongRunningTask" on the server hub is triggered. So how does this happen really? The frontend stops loading but in the background it keeps the loop? If anyone could explain this behaviour to me this would be great!

edited

After awaiting everything as Canton7 suggested there is still a question to be answered since the CancelLongRunningTask() function calls Cancel on an empty token? Apart from that, the .SendStatus(true) is never send? enter image description here

  • Note that since you check `!ct.IsCancellationRequested`, you might (or might not, depending on whether this test or the `Task.Delay` is first to find out that the cancellation has happened) respond to a cancellation by returning, rather than throwing an exception. In this case, your `// Handle cancellation logic here` won't run. It's best to avoid checking `ct.IsCancellationRequested` and use `ct.ThrowIfCancellationRequested()`, if you're using exceptions to handle cancellation – canton7 Jul 27 '23 at 13:10
  • 2
    Another problem is that you don't `await` the `Task.Delay`, so you'll spin around that `while` loop as quickly as you can, calling `Clients.Caller.SendStatus(true)` continuously. You don't await the call to `SendStatus` either, so you don't even wait for the previous send to finish before sending another – canton7 Jul 27 '23 at 13:11
  • 1
    The frontend probably stopped loading because it was being spammed by a continuous stream of statuses! – canton7 Jul 27 '23 at 13:13
  • 2
    You've also got a lot of `_ =` in your client code. The warning saying "you should await this task" exists for a reason! You shouldn't normally suppress it like this -- you're causing other problems for yourself – canton7 Jul 27 '23 at 13:20
  • "*since the CancelLongRunningTask() function calls Cancel on an empty token*" -- what exactly do you mean by that? What is an "empty" token? – canton7 Jul 27 '23 at 13:49
  • I added a image of the CancellationTokenSource being null – Thimo Luijsterburg Jul 27 '23 at 13:52
  • So you mean that the `CancellationTokenSource` is `null`, not that the Cancellation*Token* is "empty"? Do you have more than one instance of `MyHub`? How are you creating it? If it's created by an ioc container, is it registered as singleton? – canton7 Jul 27 '23 at 13:59
  • I just used the default .AddSignalR() in the startup so it should be transient. – Thimo Luijsterburg Jul 27 '23 at 14:03
  • 1
    So, think it through. If it's transient, then each time you request an instance you'll get a new instance. So the instance you call `StartLongRunningTask` on won't be the same instance you call `CancelLongRunningTask` on. Fields are specific to the instance. If you call `CancelLongRunningTask` on an instance which you didn't call `StartLongRunningTask`, then the `cancellationTokenSource` will of course be null... – canton7 Jul 27 '23 at 14:09
  • Yea I get that now, but there is 1 more thing I really don't get. Since i awaited the StartLongRunningTask(). CancelLongRunningTask() should be on a new instance but it still cancels the StartLongRunningTask() even tho there are now 2 instances. U get me? – Thimo Luijsterburg Jul 27 '23 at 14:12
  • No, I don't know what you're talking about. I thought you just said that `CancelLongRunningTask` does not cancel the long-running task, because the `cancellationTokenSource` is null – canton7 Jul 27 '23 at 14:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254694/discussion-between-thimo-luijsterburg-and-canton7). – Thimo Luijsterburg Jul 27 '23 at 14:21

1 Answers1

0

Note that since you check !ct.IsCancellationRequested, you might (or might not, depending on whether this test or the Task.Delay is first to find out that the cancellation has happened) respond to a cancellation by returning, rather than throwing an exception. In this case, your // Handle cancellation logic here won't run. It's best to avoid checking ct.IsCancellationRequested and use ct.ThrowIfCancellationRequested(), if you're using exceptions to handle cancellation.

Another problem is that you don't await the Task.Delay, so you'll spin around that while loop as quickly as you can, calling Clients.Caller.SendStatus(true) continuously. You don't await the call to SendStatus either, so you don't even wait for the previous send to finish before sending another.

The frontend probably stopped loading because it was being spammed by a continuous stream of statuses.

Let's make things properly async, and put in the appropriate awaits. This will stop your while loop from spinning, and means that StartLongRunningTask correctly waits for LongRunningTask to complete, without blocking the thread.

I've made StartLongRunningTask async void because the called probably doesn't want to wait until LongRunningTask has completed. We catch all exceptions (with catch (Exception ex)), so there's no risk of the async void method crashing our application.

I'm also catching OperationCanceledException which is the base class of TaskCanceledException and covers more cases.

public class MyHub : Hub<IMyHub>
{
    private CancellationTokenSource cancellationTokenSource;

    public async void StartLongRunningTask()
    {
        cancellationTokenSource = new CancellationTokenSource();

        try
        {
            await LongRunningTask(cancellationTokenSource.Token);
            // Task completed successfully
        }
        catch (OperationCanceledException)
        {
            // Task was canceled by the client
            // Handle cancellation logic here
        }
        catch (Exception ex)
        {
            // Handle failure here
        }
        finally
        {
            cancellationTokenSource.Dispose();
        }
    }

    public async void CancelLongRunningTask()
    {
        cancellationTokenSource?.Cancel();

        await Clients.All.SendStatus(false);
    }

    private async Task LongRunningTask(CancellationToken ct)
    {
        // Your long-running task implementation
        // Ensure to check the cancellation token periodically and stop processing if canceled
        while (true)
        {
            ct.ThrowIfCancellationRequested();
            await Clients.Caller.SendStatus(true);

            await Task.Delay(1000, ct);
            // Continue with other work...
        }
    }
}
canton7
  • 37,633
  • 3
  • 64
  • 77