46

Here the proof.
Any idea what is wrong in this code ?

    [TestMethod]
    public void TestTest()
    {
        var tcp = new TcpClient() { ReceiveTimeout = 5000, SendTimeout = 20000 };
        tcp.Connect(IPAddress.Parse("176.31.100.115"), 25);
        bool ok = Read(tcp.GetStream()).Wait(30000);
        Assert.IsTrue(ok);
    }

    async Task Read(NetworkStream stream)
    {
        using (var cancellationTokenSource = new CancellationTokenSource(5000))
        {
            int receivedCount;
            try
            {
                var buffer = new byte[1000];
                receivedCount = await stream.ReadAsync(buffer, 0, 1000, cancellationTokenSource.Token);
            }
            catch (TimeoutException e)
            {
                receivedCount = -1;
            }
        }
    }
i3arnon
  • 113,022
  • 33
  • 324
  • 344
Softlion
  • 12,281
  • 11
  • 58
  • 88
  • 1
    Could you describe what the code is supposed to do and what it actually does for you? How are you running your code? Does anything change if you run it directly from a console application? – svick Sep 14 '12 at 10:17
  • 1
    To reiterate: What *does* the test do? – Stephen Cleary Sep 14 '12 at 14:04
  • 1
    I faced the same problem with `HttpClient.GetStreamAsync(...).CopyToAsync(...)` which used `ReadOnlyStream` over `WebExceptionWrapperStream` over `ConnectStream` but still `base.ReadAsync()` that does not pass cancellationToken further into `BeginEndReadAsync()`. – SerG Mar 14 '19 at 14:37

6 Answers6

38

I finally found a workaround. Combine the async call with a delay task (Task.Delay) using Task.WaitAny. When the delay elapses before the io task, close the stream. This will force the task to stop. You should handle the async exception on the io task correctly. And you should add a continuation task for both the delayed task and the io task.

It also work with tcp connections. Closing the connection in another thread (you could consider it is the delay task thread) forces all async tasks using/waiting for this connection to stop.

--EDIT--

Another cleaner solution suggested by @vtortola: use the cancellation token to register a call to stream.Close:

async ValueTask Read(NetworkStream stream, TimeSpan timeout = default)
{
    if(timeout == default(TimeSpan))
      timeout = TimeSpan.FromSeconds(5);

    using var cts = new CancellationTokenSource(timeout); //C# 8 syntax
    using(cts.Token.Register(() => stream.Close()))
    {
       int receivedCount;
       try
       {
           var buffer = new byte[30000];
           receivedCount = await stream.ReadAsync(buffer, 0, 30000, tcs.Token).ConfigureAwait(false);
       }
       catch (TimeoutException)
       {
           receivedCount = -1;
       }
    }
}
Softlion
  • 12,281
  • 11
  • 58
  • 88
  • 12
    Other solution I found, is to register a call to `Close` in the cancellation token's `Register` method. So then when the cancellation token is cancelled, it automatically calls the `Socket.Close` method. – vtortola Nov 20 '14 at 17:11
  • Much cleaner than mine. Nice ! – Softlion Nov 21 '14 at 08:14
  • 2
    A little note: In your example you create the token inside the method. Usually, you get the token as parameter, and then you would need to enclose the call to Register in a "using" to ensure the call is unregistered after leaving the method, otherwise it will register the call each time :) – vtortola Nov 21 '14 at 09:51
  • 1
    I ended up doing something similar to this but wrote an empty message (prefix & suffix but no payload) to the stream instead of calling stream.Close() to avoid the ObjectDisposed exception. – Sheldon Neilson Jul 07 '15 at 13:54
  • 1
    Worth noting that NetworkStream.Dispose() (called by Close()) is not officially thread safe. That said, the Mono implementation appears to make an attempt to be, and the both it and the MS implementation appear to be limited to the risk of an ObjectDisposedException (or maybe a null dereference?) at this time. Also worth noting that you really don't have any better options. It's what I'm doing now anyway. :o – tekHedd Oct 14 '16 at 20:16
  • I tried the same with NamedPipeClientStream, but then it does not cancel the ReadAsync call. How can this be adressed? – MiB_Coder Jan 01 '18 at 01:00
  • It seems a shame to have to catch exception to handle a timeout. I guess there is no other way? – jjxtra Sep 18 '18 at 16:48
  • 1
    One thing to keep in mind is that this will close underlying connection upon timing out. In some scenarios it might make sense to keep the connection open. – ZakiMa Feb 24 '20 at 23:35
  • 1
    Closing underlying connection didn't work out for me. Still experienced some hangs. See more details in my answer below. Had to go back to Task.Delay. – ZakiMa Apr 09 '20 at 01:50
23

Cancellation is cooperative. NetworkStream.ReadAsync must cooperate to be able to be cancelled. It is kind of hard for it to do that because that would potentially leave the stream in an undefined state. What bytes have already been read from the Windows TCP stack and what haven't? IO is not easily cancellable.

Reflector shows that NetworkStream does not override ReadAsync. This means that it will get the default behavior of Stream.ReadAsync which just throws the token away. There is no generic way Stream operations can be cancelled so the BCL Stream class does not even try (it cannot try - there is no way to do this).

You should set a timeout on the Socket.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 9
    Too bad, I think network operations are one of the cases where cancellation could make a lot of sense, because they can take very long. – svick Sep 14 '12 at 21:16
  • 2
    Yes. I wish the BCL did support `CancelIO` for all the common IO actions. It would integrate so nicely with `CancellationToken`. – usr Sep 14 '12 at 21:28
  • 3
    The doc says that all timeouts are ignored on the Socket class when an async method is used "Timeout: This option applies to synchronous Receive calls only.". So all .net asynchronous network methods are unusable as they lead potentially to infinite locks .... – Softlion Sep 15 '12 at 18:02
  • 1
    Link to a thread talking about the same problem: http://stackoverflow.com/questions/10930052/net-tcpclient-networkstream-implementation-that-supports-async-operations-and-r which points to this thread http://social.msdn.microsoft.com/Forums/da-DK/async/thread/54632b19-0e9c-4078-aa59-c4389e75b187 which explains that the CancelIoEx function does allow cancellation of a single operation, but was introduced with Vista. But .NET 4 still supports XP-SP3, so the BCL cannot easily make use of that API. – Softlion Sep 15 '12 at 18:16
  • 1
    @Softlion very good point with that API. Async Socket calls not having a timeout is outrageous! I did not know that. This means that many of the new async C# apps using Socket will be broken by default. – usr Sep 15 '12 at 18:26
  • Well there is a solution: await on a combination of ReadAsync and Task.Wait, if the wait occurs first call Close on Socket. This force the socket to close and return from await. You should be careful to not dispose the socket before ReadAsync returns. – Softlion Sep 16 '12 at 18:59
  • Also seems to be the case with `PipeStream` (as when using a `NamedPipeClientStream`) – Erunehtar Sep 24 '18 at 18:10
  • In .NET 5, it seems to be possible to cancel `ReadAsync` in a `NetworkStream` – SomeBody Feb 03 '21 at 13:39
6

Per the description in Softlion's answer:

Combine the async call with a delay task (Task.Delay) using Task.WaitAny. When the delay elapses before the io task, close the stream. This will force the task to stop. You should handle the async exception on the io task correctly. And you should add a continuation task for both the dealy task and the io task.

I've made some code that gives you the async read with timeout:

using System;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace ConsoleApplication2013
{
    class Program
    {
        /// <summary>
        /// Does an async read on the supplied NetworkStream and will timeout after the specified milliseconds.
        /// </summary>
        /// <param name="ns">NetworkStream object on which to do the ReadAsync</param>
        /// <param name="s">Socket associated with ns (needed to close to abort the ReadAsync task if the timeout occurs)</param>
        /// <param name="timeoutMillis">number of milliseconds to wait for the read to complete before timing out</param>
        /// <param name="buffer"> The buffer to write the data into</param>
        /// <param name="offset">The byte offset in buffer at which to begin writing data from the stream</param>
        /// <param name="amountToRead">The maximum number of bytes to read</param>
        /// <returns>
        /// a Tuple where Item1 is true if the ReadAsync completed, and false if the timeout occurred,
        /// and Item2 is set to the amount of data that was read when Item1 is true
        /// </returns>
        public static async Task<Tuple<bool, int>> ReadWithTimeoutAsync(NetworkStream ns, Socket s, int timeoutMillis, byte[] buffer, int offset, int amountToRead)
        {
            Task<int> readTask = ns.ReadAsync(buffer, offset, amountToRead);
            Task timeoutTask = Task.Delay(timeoutMillis);

            int amountRead = 0;

            bool result = await Task.Factory.ContinueWhenAny<bool>(new Task[] { readTask, timeoutTask }, (completedTask) =>
            {
                if (completedTask == timeoutTask) //the timeout task was the first to complete
                {
                    //close the socket (unless you set ownsSocket parameter to true in the NetworkStream constructor, closing the network stream alone was not enough to cause the readTask to get an exception)
                    s.Close();
                    return false; //indicate that a timeout occurred
                }
                else //the readTask completed
                {
                    amountRead = readTask.Result;
                    return true;
                }
            });

            return new Tuple<bool, int>(result, amountRead);
        }

        #region sample usage
        static void Main(string[] args)
        {
            Program p = new Program();
            Task.WaitAll(p.RunAsync());
        }

        public async Task RunAsync()
        {
            Socket s = new Socket(SocketType.Stream, ProtocolType.Tcp);

            Console.WriteLine("Connecting...");
            s.Connect("127.0.0.1", 7894);  //for a simple server to test the timeout, run "ncat -l 127.0.0.1 7894"
            Console.WriteLine("Connected!");

            NetworkStream ns = new NetworkStream(s);

            byte[] buffer = new byte[1024];
            Task<Tuple<bool, int>> readWithTimeoutTask = Program.ReadWithTimeoutAsync(ns, s, 3000, buffer, 0, 1024);
            Console.WriteLine("Read task created");

            Tuple<bool, int> result = await readWithTimeoutTask;

            Console.WriteLine("readWithTimeoutTask is complete!");
            Console.WriteLine("Read succeeded without timeout? " + result.Item1 + ";  Amount read=" + result.Item2);
        }
        #endregion
    }
}
Anssssss
  • 3,087
  • 31
  • 40
4

There are a few problems there that pop out:

  1. CancellationToken throws OperationCanceledException, not TimeoutException (cancellation is not always due to timeout).
  2. ReceiveTimeout doesn't apply, since you're doing an asynchronous read. Even if it did, you'd have a race condition between IOException and OperationCanceledException.
  3. Since you're synchronously connecting the socket, you'll want a high timeout on this test (IIRC, the default connection timeout is ~90 seconds, but can be changed as Windows monitors the network speeds).
  4. The correct way to test asynchronous code is with an asynchronous test:

    [TestMethod]
    public async Task TestTest()
    {
        var tcp = new TcpClient() { ReceiveTimeout = 5000, SendTimeout = 20000 };
        tcp.Connect(IPAddress.Parse("176.31.100.115"), 25);
        await Read(tcp.GetStream());
    }
    
svick
  • 236,525
  • 50
  • 385
  • 514
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1) I know. If you try the code it never throws any exception. – Softlion Sep 14 '12 at 13:52
  • 2) I know. It is used to give the timeout to the inner method in the real class, which i replaced with a real value for this question. – Softlion Sep 14 '12 at 13:53
  • 3) I connect the socket synchronously to demonstrate the problem with ReadAsync. There is no problem with ConnectAsync. Well it may have a problem but i don't have a working test case. You can stay 10mn waiting for this method to return. 10mn is much more than ~90s. So this can not be the problem. – Softlion Sep 14 '12 at 13:56
  • 4) Asynchronous tests are buggy on VS2012.They are not working correctly, and they are not recognized by the test explorer (all projects are .net 4.5). – Softlion Sep 14 '12 at 13:57
  • @Softlion: This is the first I've heard re buggy asynchronous tests. Have you reported this to Microsoft Connect? – Stephen Cleary Sep 14 '12 at 14:00
  • 1
    If you search for this problem, you'll find blogs about it. Some people fixed it by changing the .net framework type to 4.5 for the test project. Mine is already 4.5. I use a dependancy in 4.0 maybe this is the problem. And all bugs i reported to connect previously where nicely "ignored/not planned/by design/..." so i will never post anything again on connect. – Softlion Sep 14 '12 at 14:07
  • Update: async tests works fine, but don't report the stack trace correctly, and when debugging is used, they stop at wrong places or don't show you the code. The worst is when an exception is thrown, it breaks without a correct call stack. This has been fixed in VS2013+Windows8.1. Windows 8.1 is required to have the call stack of the async operation. – Softlion Oct 29 '13 at 13:00
2

Providing more context on three different approaches. My service monitors other web applications availability. So, it needs to establish lots of connections to various web sites. Some of them crash/return errors/become unresponsive.

Axis Y - number of hung tests (sessions). Drops to 0 caused by deployments/restarts.

I. (Jan 25th) After revamping a service, the initial implementation used ReadAsync with a cancellation token. This resulted in lots of tests hanging (running requests against those web sites showed that servers indeed sometimes didn't return content).

II. (Feb 17th) Deployed a change which guarded cancellation with Task.Delay. This completely fixed this issue.

private async Task<int> StreamReadWithCancellationTokenAsync(Stream stream, byte[] buffer, int count, Task cancellationDelayTask)
{
    if (cancellationDelayTask.IsCanceled)
    {
        throw new TaskCanceledException();
    }

    // Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
    // operation is not guarded. As a result if remote server never responds and connection never closed
    // it will lead to this operation hanging forever.
    Task<int> readBytesTask = stream.ReadAsync(
        buffer,
        0,
        count);
    await Task.WhenAny(readBytesTask, cancellationDelayTask).ConfigureAwait(false);

    // Check whether cancellation task is cancelled (or completed).
    if (cancellationDelayTask.IsCanceled || cancellationDelayTask.IsCompleted)
    {
        throw new TaskCanceledException();
    }

    // Means that main task completed. We use Result directly.
    // If the main task failed the following line will throw an exception and
    // we'll catch it above.
    int readBytes = readBytesTask.Result;

    return readBytes;
}

III (March 3rd) Following this StackOverflow implemented closing a stream based on timeout:

using (timeoutToken.Register(() => stream.Close()))
{
    // Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
    // operation is not guarded. As a result if a remote server never responds and connection never closed
    // it will lead to this operation hanging forever.
    // ReSharper disable once MethodSupportsCancellation
    readBytes = await targetStream.ReadAsync(
        buffer,
        0,
        Math.Min(responseBodyLimitInBytes - totalReadBytes, buffer.Length)).ConfigureAwait(false);
}

This implementation brought hangs back (not to the same extent as the initial approach):

enter image description here

Reverted back to Task.Delay solution.

ZakiMa
  • 5,637
  • 1
  • 24
  • 48
1

Just for the heads-up, await _stream.WriteAsync(message,cancellationToken); (_stream is a SslStream) checks behind the scenes if the cancellation token has been cancelled before performing the BeginEndWriteAsync, so you must cancel your token before it starts to write.

public virtual Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        // If cancellation was requested, bail early with an already completed task.
        // Otherwise, return a task that represents the Begin/End methods.
        return cancellationToken.IsCancellationRequested
                    ? Task.FromCanceled(cancellationToken)
                    : BeginEndWriteAsync(buffer, offset, count);
    }
  • 1
    You are right. My bad. However, I believe my answer is still relevant because the question was more around the cancellation token. ReadAsync and WriteAsync behave the same way when it comes to cancellation token. – Daniel Botero Correa Jan 17 '21 at 09:54