Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async network operations never finish

I have several asynchronous network operations that return a task that may never finish:

  1. UdpClient.ReceiveAsync doesn't accept a CancellationToken
  2. TcpClient.GetStream returns a NetworkStream that doesn't respect the CancellationToken on Stream.ReadAsync (checking for cancellation only at the start of the operation)

Both wait for a message that may never come (because of packet loss or no response for example). That means I have phantom tasks that never complete, continuations that will never run and used sockets on hold. I know i can use TimeoutAfter, but that will only fix the continuation problem.

So what am I supposed to do?

like image 726
i3arnon Avatar asked Jan 30 '14 21:01

i3arnon


2 Answers

So i've made an extension method on IDisposable that creates a CancellationToken that disposes the connection on timeout, so the task finishes and everything carries on:

public static IDisposable CreateTimeoutScope(this IDisposable disposable, TimeSpan timeSpan)
{
    var cancellationTokenSource = new CancellationTokenSource(timeSpan);
    var cancellationTokenRegistration = cancellationTokenSource.Token.Register(disposable.Dispose);
    return new DisposableScope(
        () =>
        {
            cancellationTokenRegistration.Dispose();
            cancellationTokenSource.Dispose();
            disposable.Dispose();
        });
}

And the usage is extremely simple:

try
{
    var client = new UdpClient();
    using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2)))
    {
        var result = await client.ReceiveAsync();
        // Handle result
    }
}
catch (ObjectDisposedException)
{
    return null;
}

Extra Info:

public sealed class DisposableScope : IDisposable
{
    private readonly Action _closeScopeAction;
    public DisposableScope(Action closeScopeAction)
    {
        _closeScopeAction = closeScopeAction;
    }
    public void Dispose()
    {
        _closeScopeAction();
    }
}
like image 160
i3arnon Avatar answered Oct 21 '22 14:10

i3arnon


So what am I supposed to do?

In this particular case, I would rather use UdpClient.Client.ReceiveTimeout and TcpClient.ReceiveTimeout to time out a UDP or TCP receive operation gracefully. I'd like to have the time-out error coming from the socket, rather than from any external source.

If in addition to that I need to observe some other cancellation event, like a UI button click, I would just use WithCancellation from Stephen Toub's "How do I cancel non-cancelable async operations?", like this:

using (var client = new UdpClient())
{
    UdpClient.Client.ReceiveTimeout = 2000;

    var result = await client.ReceiveAsync().WithCancellation(userToken);
    // ...
}

To address the comment, in case ReceiveTimeout has no effect on ReceiveAsync, I'd still use WithCancellation:

using (var client = new UdpClient())
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken))
{
    UdpClient.Client.ReceiveTimeout = 2000;
    cts.CancelAfter(2000);

    var result = await client.ReceiveAsync().WithCancellation(cts.Token);
    // ...
}

IMO, this more clearly shows my intentions as a developer and is more readable to a 3rd party. Also, I don't need to catch ObjectDisposedException exeception. I still need to observe OperationCanceledException somewhere in my client code which calls this, but I'd be doing that anyway. OperationCanceledException usually stands out from other exceptions, and I have an option to check OperationCanceledException.CancellationToken to observe the reason for cancellation.

Other than that, there's not much difference from @I3arnon's answer. I just don't feel like I need another pattern for this, as I already have WithCancellation at my disposal.

To further address the comments:

  • I'd only be catching OperationCanceledException in the client code, i.e.:

async void Button_Click(sender o, EventArgs args) { try { await DoSocketStuffAsync(_userCancellationToken.Token); } catch (Exception ex) { while (ex is AggregateException) ex = ex.InnerException; if (ex is OperationCanceledException) return; // ignore if cancelled // report otherwise MessageBox.Show(ex.Message); } }
  • Yes, I'll be using WithCancellation with each ReadAsync call and I like that fact, for the following reasons. Firstly, I can create an extension ReceiveAsyncWithToken:

public static class UdpClientExt
{
    public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
        this UdpClient client, CancellationToken token)
    {
        return client.ReceiveAsync().WithCancellation(token);
    }
}

Secondly, in 3yrs from now I may be reviewing this code for .NET 6.0. By then, Microsoft may have a new API, UdpClient.ReceiveAsyncWithTimeout. In my case, I'll simply replace ReceiveAsyncWithToken(token) or ReceiveAsync().WithCancellation(token) with ReceiveAsyncWithTimeout(timeout, userToken). It would not be so obvious to deal with CreateTimeoutScope.

like image 3
noseratio Avatar answered Oct 21 '22 13:10

noseratio