testing
testing

Reputation: 20289

Async/await with CancellationToken doesn't cancel the operation

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;
}

Upvotes: 1

Views: 5203

Answers (2)

Peter Wishart
Peter Wishart

Reputation: 12320

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.

Upvotes: 0

Alex
Alex

Reputation: 13234

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

Upvotes: 6

Related Questions