0

I'm playing with TcpClient and when i use some Async operations they ignore the CancellationToken. After some reading, i know that it is intentionally and also knows that exists some ways to cancel awaits on Asyncs operations.

I just read next StackOverflow questions and articles that clarifies some points:

How to cancel a Task in await?

https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations/

Following previous articles, I could cancel NetworkStream.ReadAsync but that mechanisms doesn't work when i use them on NetworkStream.WriteAsync.

I have this code as minimal example:

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        CancellationTokenSource ctSource;
        private async void button1_Click(object sender, EventArgs e)
        {
            button1.Enabled = false;
            string http_resp = "";
            var request_uri = new Uri("http://icanhazip.com");

            //BIG FILE as postdata to test the WriteAsync (use one that you have on your disk)
            string contents = File.ReadAllText(@"C:\Portables\4test.txt");
            string post_data = contents;

            ctSource = new CancellationTokenSource();
            CancellationToken ct = ctSource.Token;

            Task<string> task = HttpRequestAsync(post_data, request_uri, ct);
            try
            {
                http_resp = await task;
            }
            catch
            {
                http_resp = "General error";
            }

            textBox1.Text = http_resp;

            button1.Enabled = true;
        }

        private static async Task<string> HttpRequestAsync(string post_data, Uri request_uri, CancellationToken ct)
        {
            string result = string.Empty;
            string http_method = "POST";
            string post_content_type = "application/x-www-form-urlencoded";

            var hostname = request_uri.Host;
            var port = request_uri.Port;
            var scheme = request_uri.Scheme;

            using (TcpClient tcpClient = new TcpClient())
            {
                tcpClient.SendTimeout = 15;
                tcpClient.ReceiveTimeout = 15;
                try
                {
                    await tcpClient.ConnectAsync(hostname, port);
                }
                catch (Exception d1)
                {
                    if (ct.IsCancellationRequested)
                    {
                        result = "Cancelation requested on ConnectAsync";
                    }
                    else
                    {
                        result = d1.Message + "\r\n" + d1.GetType().FullName + d1.StackTrace; ;
                    }
                    return result;
                }


                //Build HTTP headers
                string reqString = "";
                string header_host = "Host: " + hostname + "\r\n";
                string header_close = "Connection: Close\r\n";
                string basic_headers = "User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0\r\n";
                basic_headers += "Referer: https://www.google.com\r\n";

                string header_post = "";
                if (http_method == "POST")
                {
                    string header_content_type = "";
                    header_content_type = "Content-type: " + post_content_type + "\r\n";
                    int content_length = 0;
                    content_length = post_data.Length;
                    string header_content_length = "Content-length: " + content_length + "\r\n";
                    header_post = header_content_type + header_content_length;
                }

                reqString = http_method + " " + request_uri.PathAndQuery + " " + "HTTP/1.1" + "\r\n" + header_host + basic_headers + header_close + header_post + "\r\n";
                if (http_method == "POST")
                {
                    reqString += post_data;
                }
                var header_bytes = Encoding.ASCII.GetBytes(reqString.ToString());

                //Starting the I/O Network operations
                using (NetworkStream tcp_stream = tcpClient.GetStream())
                {
                    try
                    {
                        //HERE is where i have problems cancelling this await while WriteAsync is working.
                        await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct).WithCancellation(ct);
                        //await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct);
                    }
                    catch (Exception d2)
                    {
                        if (ct.IsCancellationRequested)
                        {
                            result = "Cancelation requested on WriteAsync";
                        }
                        else
                        {
                            result = d2.Message + "\r\n" + d2.GetType().FullName + d2.StackTrace;
                        }
                        return result;
                    }

                    using (var memory = new MemoryStream())
                    {
                        try
                        {
                            await tcp_stream.CopyToAsync(memory, 81920, ct);
                        }
                        catch (Exception d3)
                        {
                            if (ct.IsCancellationRequested)
                            {
                                result = "Request cancelled by user (on read)";
                            }
                            else
                            {
                                result = d3.Message + "\r\n" + d3.GetType().FullName + d3.StackTrace;
                            }
                            return result;
                        }
                        memory.Position = 0;
                        byte[] data = memory.ToArray();
                        result = Encoding.UTF8.GetString(data);
                    }
                }
            }
            return result;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            ctSource.Cancel();
        }
    }

It works good when i use it on ReadAsync:

await tcp_stream.ReadAsync(response, 0, response.Length, ct).WithCancellation(ct);

It doesn't work when i use it on WriteAsync:

await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct).WithCancellation(ct);

No error is returned, simply the await isn't cancelled. To be more clear i added a minimal example as a Visual Studio 2015 project that you can download here: https://github.com/Zeokat/minimal_ex/archive/master.zip

It also includes a file 4test.rar that you can decompress into a file of 39MB 4test.txt. I use this text file as post_data contents for test because is big enougth to call the Cancel action while the WriteAsync is running.

Can someone give me a hand on this? I spend some days trying to fix this but couldn't achieve a proper solution.

Thanks in advance.

Zeokat
  • 504
  • 1
  • 5
  • 16
  • 1
    You need an overload of the `WithCancellation` method that accepts and returns a simple [`Task`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task) (the non-generic type). – Theodor Zoulias Mar 22 '20 at 13:10
  • I apreciate your answer but i have no idea about how to do that. I already tryed but couldn't achieve the proper way to do that. Will be nice if you can explain with code example. – Zeokat Mar 22 '20 at 13:17
  • [Here](https://stackoverflow.com/questions/59243161/is-there-a-way-i-can-cause-a-running-method-to-stop-immediately-with-a-cts-cance/59267214#59267214) is a simpler implementation of this method, for both the `Task` and `Task` types. It is named `AsCancelable` instead of `WithCancellation`, but otherwise it's the same. – Theodor Zoulias Mar 22 '20 at 13:27
  • Btw these methods allocate at least two objects on each invocation, so they are not suitable for use in a hot path (I.e. don't call them 100,000 times per second)! – Theodor Zoulias Mar 22 '20 at 13:36
  • The code you had linked, sadly don't work for async operations. I'm still at the same point. – Zeokat Mar 22 '20 at 14:11
  • The linked code is tested, but probably isn't doing what you want. There is no way to cancel an async operation that is not cancelable, other than killing the process. You can only cancel the awaiting of the operation. You can then do other things while the async operation is still "running" in the background, except from interacting with the object that owns the incomplete async operation. In that case it will most probably throw an `InvalidOperationException`. – Theodor Zoulias Mar 22 '20 at 15:05
  • And cancel the await is what i want, as i stated in my question: How to cancel await with NetworkStream.WriteAsync? – Zeokat Mar 22 '20 at 15:25
  • Most of the newer TPL-based async operations accept a `TaskCancellationToken` value as a parameter, which you can then use to cancel the operation. So that's one option. See marked duplicate. That said, for TCP I/O, there's no 100% reliable way to safely interrupt communications on a connection. Once you've started sending or receiving data, there may be data "in flight" that you won't be able to account for. It is best, if the code decides cancelling the operation is appropriate, to simply close the socket, forcing the connection to reset, and start over. – Peter Duniho Mar 22 '20 at 20:03
  • I understand your point @Peter Duniho, but thats the purpose of my question. At this moment i couldn't Cancel `await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct)` in any way. But you closed the question so... nothing. – Zeokat Mar 22 '20 at 21:36
  • Unless you've written an extension method that works with `Task` instead of only `Task`, then of course you can't use your extension method with `WriteAsync()`, since that doesn't return a `Task`. You didn't bother to provide a [mcve], so it's not possible to know for sure what your code really looks like. I don't really understand the point of your extension method anyway, since it doesn't really cancel anything that an I/O method already would have anyway. But if the basic remedies for canceling I/O don't help, you need to post a question that is more clear. – Peter Duniho Mar 22 '20 at 22:54
  • The [`NetworkStream.WriteAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream.writeasync) method returns a `ValueTask`. Could you try converting it to a `Task` by using the [`AsTask`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask.astask) method, and then chain the `AsCancelable` method? And if it's still not working, what's the error message? – Theodor Zoulias Mar 23 '20 at 03:03
  • Thanks @Theodor Zoulias, i go to build a "minimal reproducible example" and repost again the question, because is the unique way to allow others help me. – Zeokat Mar 23 '20 at 09:30
  • I have taken it personally that my [linked](https://stackoverflow.com/questions/59243161/is-there-a-way-i-can-cause-a-running-method-to-stop-immediately-with-a-cts-cance/59267214#59267214) extension methods are not working for you. They are so cute! – Theodor Zoulias Mar 23 '20 at 09:37
  • Ok, i added a [minimal example](https://github.com/Zeokat/minimal_ex/archive/master.zip) with code that you can test by youself. Hope someone can bring some light here. – Zeokat Mar 23 '20 at 13:28
  • I think it would be preferable if you could embed the minimal example inside your question. Downloading and unzipping a file containing zipped files of zipped files is not much fun! – Theodor Zoulias Mar 23 '20 at 14:05
  • I was trying to make things confortable, but the i just embed the code now also, so you can choose your source. – Zeokat Mar 23 '20 at 17:09
  • Aha. There is another `NetworkStream.WriteAsync` overload that returns `Task`... How do you know that the cancelable task wrapper is not canceled? You assume this because clicking on the Cancel button doesn't stop the procedure? There is another possibility. Since you are capturing the synchronization context on every `await`, a whole lot of continuations are scheduled in the UI thread, and the responsiveness suffers from ["thousands of paper cuts"](https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#configure-context). – Theodor Zoulias Mar 23 '20 at 18:01
  • Yeah clicking on the Stop button don't cancel it. I run only one await before WriteAsync, anyway you can modify the code to not be GUI dependedent and you will se that the behaviour is the same. – Zeokat Mar 23 '20 at 18:23
  • I suggest that you convert your minimal example to a Console application, and garnish it with logging (`Console.WriteLine`) in critical points, so that you can confirm your assumption that the task is not canceled. The `CancellationTokenSource` constructor has an overload that accepts a `millisecondsDelay` argument. So you don't need a UI button to cancel it, you can make it cancel itself automatically after some time. Btw are you sure that your example is minimal? There is quite a lot of code there, for just checking that a single method works as expected (or not). – Theodor Zoulias Mar 24 '20 at 06:33
  • I have another idea. Maybe the `NetworkStream.WriteAsync` is actually running synchronously, and blocks the current thread. I suggest that you try wrapping it in a `Task.Run`, and then wrap it again with the `WithCancellation` method, to check if this assumption is correct: `await Task.Run(() => tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct)).WithCancellation(ct);` – Theodor Zoulias Mar 24 '20 at 07:21
  • 1
    Thanks for your suggestions Theodor, i already wrapped it into a Task without success. I will try to build a console app for tests. – Zeokat Mar 24 '20 at 07:36

1 Answers1

0

Dont use .WithCancellation(ct) use only await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct).

cts = new CancellationTokenSource();

pass ct = cts.Token

in cancel_event() : if(cts != null) cts.Cancel();