Search code examples
c#network-programmingasync-awaitidisposablecancellation-token

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?


Solution

  • 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();
        }
    }