Async network operations never finish
I have several asynchronous network operations that return a task that may never finish:
-
UdpClient.ReceiveAsync
doesn't accept aCancellationToken
-
TcpClient.GetStream
returns aNetworkStream
that doesn't respect theCancellationToken
onStream.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:
- I'd only be catching
OperationCanceledException
in the client code, i.e.:
- Yes, I'll be using
WithCancellation
with eachReadAsync
call and I like that fact, for the following reasons. Firstly, I can create an extensionReceiveAsyncWithToken
:
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
.