I have several asynchronous network operations that return a task that may never finish:
UdpClient.ReceiveAsync doesn't accept a CancellationToken
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?
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();
}
}
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:
OperationCanceledException in the client code, i.e.: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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With