1

I want to use the CancellationToken to abort a file download. This is what I tried:

public async Task retrieveDocument(Document document)
{
    // do some preparation work first before retrieving the document (not shown here)
    if (cancelToken == null)
    {
        cancelToken = new CancellationTokenSource();
        try
        {
            Document documentResult = await webservice.GetDocumentAsync(document.Id, cancelToken.Token);
            // do some other stuff (checks ...)
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("abort download");
        }
        finally
        {
            cancelToken = null;
        }
    }
    else
    {
        cancelToken.Cancel();
        cancelToken = null;
    }
}

public async Task<Document> GetDocumentAsync(string documentId, CancellationToken cancelToken)
{
    Document documentResult = new Document();

    try
    {

        cancelToken.ThrowIfCancellationRequested();

        documentResult = await Task.Run(() => manager.GetDocumentById(documentId));
    }

    return documentResult;
}

The cancelToken should then be used to cancel the operation:

public override void DidReceiveMemoryWarning ()
{
    // Releases the view if it doesn't have a superview.
    base.DidReceiveMemoryWarning ();

    if (cancelToken != null) {
        Console.WriteLine ("Token cancelled");
        cancelToken.Cancel ();
    }
}

It seems that IsCancellationRequested is not updated. So the operation is not cancelled. I also tried to use this

cancelToken.ThrowIfCancellationRequested();
try{
    documentResult = await Task.Run(() => manager.GetDocumentById (documentId), cancelToken);
} catch(TaskCanceledException){
    Console.WriteLine("task canceled here");
}

but nothing changed.

What I'm doing wrong?

Edit:

Here are the missing parts like GetDocumentById:

public Document GetDocumentById(string docid)
{
    GetDocumentByIdResult res;
    try
    {
        res = ws.CallGetDocumentById(session, docid);
    }
    catch (WebException e)
    {
        throw new NoResponseFromServerException(e.Message);
    }

    return res;
}

public Document CallGetDocumentById(Session session, string parmsstring)
{
    XmlDocument soapEnvelope = Factory.GetGetDocumentById(parmsstring);
    HttpWebRequest webRequest = CreateWebRequest(session);
    webRequest = InsertEnvelope(soapEnvelope, webRequest);
    string result = WsGetResponseString(webRequest);
    return ParseDocument(result);
}

static string WsGetResponseString(WebRequest webreq)
{
    string soapResult = "";
    IAsyncResult asyncResult = webreq.BeginGetResponse(null, null);
    if (asyncResult.AsyncWaitHandle.WaitOne(50000))
    {
        using (WebResponse webResponse = webreq.EndGetResponse(asyncResult))
        {
            if (webResponse != null)
            {
                using (var rd = new StreamReader(webResponse.GetResponseStream()))
                {
                    soapResult = rd.ReadToEnd();
                }
            }
        }
    }
    else
    {
        webreq.Abort();
        throw new NoResponseFromServerException();
    }

    return soapResult;
}
testing
  • 19,681
  • 50
  • 236
  • 417
  • 6
    I don't really understand what you're trying to do. You invoke ThrowIfCancellationRequested *before* you start doing the work which is unlikely to do anything. You need to periodically invoke it inside the code that does the work for it to function. For example, inside a loop. – svinja May 05 '15 at 13:07
  • I couldn't reproduce the problem when using `Task.Delay` instead of `webservice.GetDocumentAsync`. So maybe that service call or the code that follows (`do some other stuff`) don't fully support cooperative task cancellation. – Dirk May 05 '15 at 13:12
  • What does `GetDocumentById` do? If you cancel the token after `GetDocumentById` starts executing, no one will notice since it isn't being monitored. – Yuval Itzchakov May 05 '15 at 13:14
  • 1
    What @svinja said is right, and its a tough concept to grasp. `ThrowIfCancellationRequested` is not setting an option, its checking the token and if its set, it throws the error. You need to call this periodically for the exception to be thrown, because that call is what actually throws it. – Ron Beyer May 05 '15 at 13:17
  • @svinja: How would such a loop look like for my example? I saw others using `while(true)`, but I don't know if it's a good idea using this. – testing May 05 '15 at 13:31
  • @RonBeyer: How should I periodically check for it? Can you provide an example to make it clearer? – testing May 05 '15 at 13:36
  • @YuvalItzchakov: It creates the necessary request for the webservice and delivers me the response. If I look deep into the structure then a `StreamReader` is used. – testing May 05 '15 at 13:55
  • @testing unless you have a `manager.GetDocumentByIdAsync` you can call you can't check it periodically. You are checking as often as your code will let you. Show the code for `GetDocumentById` if you wrote it, that is where the loop would be. – Scott Chamberlain May 05 '15 at 13:55
  • @ScottChamberlain: So I've to pass the token down deeper into the methods and check there periodically? The operation of the `StreamReader` will take the biggest part of my code. Should I adapt my code when reading the stream so that it can be checked periodically? – testing May 05 '15 at 13:59
  • @ScottChamberlain: I added now the missing code. – testing May 05 '15 at 14:26

2 Answers2

5

I want to use the CancellationToken to abort a file download

Downloading a file is an I/O operation, for which asynchronous cancelable (I/O completion port based) functions are available on the .NET platform. Yet you seem to not be using them.

Instead you appear to be creating (a chain of) tasks using Task.Run that perform blocking I/O, where a cancelation token is not passed on to each task in your Task.Run chain.

For examples of doing async, awaitable and cancelable file downloads, refer to:

  • Using HttpClient: How to copy HttpContent async and cancelable?
  • Windows Phone: Downloading and saving a file Async in Windows Phone 8
  • Using WebClient: Has its own cancellation mechanism: the CancelAsync method, you can connect it to your cancellation token, using the token's Register method:
    myToken.Register(myWebclient.CancelAsync);
  • Using the abstract WebRequest: If it was not created using an attached cancelation token, as seems to be the case for your edited example, and you are not actually downloading a file, but reading a content string, you need to use a combination of a few of the earlier mentioned methods.

You can do the following:

static async Task<string> WsGetResponseString(WebRequest webreq, CancellationToken cancelToken)`
{
    cancelToken.Register(webreq.Abort);
    using (var response = await webReq.GetResponseAsync())
    using (var stream = response.GetResponseStream())
    using (var destStream = new MemoryStream())
    {
        await stream.CopyToAsync(destStream, 4096, cancelToken);
        return Encoding.UTF8.GetString(destStream.ToArray());
    }
}
Community
  • 1
  • 1
Alex
  • 13,024
  • 33
  • 62
  • I already thought about using `WebClient`, but the implementations I saw all use a URI. In my case I'm building a separate web request (see edited question). So how can it be done here? – testing May 05 '15 at 14:33
  • @testing I updated the answer to include a `WebRequest` variant – Alex May 05 '15 at 15:29
  • Thank you very much. I'll try that. If I want to read the string and want to save it as a file without buffering it into memory, can I simply use `File.Write` instead of `MemoryStream` as in [one](http://stackoverflow.com/a/21573527/426227) of your linked examples? I think that should work and for this `CopyToAsync` and the UTF-8 conversion wouldn't be needed anymore. – testing May 05 '15 at 20:52
  • @testing, yes, you would open the file stream: `using (var destSteam = File.OpenWrite(path))` and remove the `return Encoding ....` – Alex May 05 '15 at 21:26
  • Yep, definitely. I build some things up on your approach and so tested it a little bit more :-) – testing May 08 '15 at 06:02
0

Your code only calls ThrowIfCancellationRequested() once after starting the GetDocumentAsync method, making the window for catching a cancel very small.

You need to pass the CancellationToken to GetDocumentById and have it either call ThrowIfCancellationRequested in between operations or perhaps pass the token straight to some calls at a lower level.

As a quick test of the plumbing between your calling method and the CancellationToken, you could change GetDocumentAsync to read:

cancelToken.ThrowIfCancellationRequested();
documentResult = await Task.Run(() => manager.GetDocumentById(documentId));
cancelToken.ThrowIfCancellationRequested();

And call CancelToken.CancelAfter(50) or simlar just after creating the CancellationTokenSource... You may need to adjust the value of 50 depending on how long GetDocumentById takes to run.

[Edit]

Given your edit to the question, the quickest fix is to pass the CancelToken down to WsGetResponseString and use CancelToken.Register() to call WebRequest.Abort().

You could also use CancelAfter() to implement your 50s timeout, switch from BeginGetResponse..EndGetResponse to GetResponseAsync etc.

Peter Wishart
  • 11,600
  • 1
  • 26
  • 45
  • I get *The operation was canceled.* in the console output, but it seems that it is called after the `await` has been finished. The exception is thrown in *GetDocumentAsync*. – testing May 05 '15 at 13:47
  • Yes it will be thrown at the third line above. You probably don't want to catch `OperationCancelledException` in that method, and let it bubble up to `retrieveDocument`, which should handle it and print to the console. – Peter Wishart May 05 '15 at 13:50