5

On my server I have a route that generates a pdf for a user. When the generation takes longer than a specified amount of time, the route is supposed to return an accepted status code and send a email to the user when the processing is finished. The issue I am having is that the Task.Delay is not being respected. Not matter how low I set PdfGenerationTimeLimitUntilEmail the Wait.Any still does not resolve until the createPdfTask is finished. Interestingly enough when the generation throws an exception or completes, and the time has exceeded the Task.Delay time, execution proceeds to return the Accepted Status. My suspicion is that the culprit is a DeadLock situation.

A related question which I have read thoroughly, but have not been able to apply to my problem: C#/.NET 4.5 - Why does "await Task.WhenAny" never return when provided with a Task.Delay in a WPF application's UI thread?

Is there anything obvious that I am missing? Or something about the thread context that I should know?

 [HttpPost]
 [Route("foo")]
 public async Task<IHttpActionResult> Foo([FromBody] FooBody fooBody)
 {
        var routeUser = await ValidateUser();

        async Task<Stream> CeatePdfFile()
        {
            var pdf = await createPdfFromFooData(fooBody);

            return await pdfFileToStream(pdf);
        }

        var delay = Task.Delay(PdfGenerationTimeLimitUntilEmail);
        var createPdfTask = CeatePdfFile();
        var firstTaskResolved = await Task.WhenAny(createPdfTask , delay);

        if (firstTaskResolved == createPdfTask)
        {
            var pdfFileStream = await createPdfTask ;
            return new FileActionResult(pdfFileStream);
        }

        // Creating the PDF can take a long time, so just send an email when it's done
        async void SendEmail(CancellationToken token)
        {
            var pdfFileStreamToEmail = await createPdfTask ;
            _emailSender.SendDownloadEmail(routeUser.Email, pdfFileStreanToEmail);
        }

        HostingEnvironment.QueueBackgroundWorkItem(token => SendEmail(token));
        return StatusCode(HttpStatusCode.Accepted);
}
Ender Doe
  • 162
  • 1
  • 10
  • 2
    Should `async void SendEmail` be `async Task SendEmail`? That way the method can me awaited. – Igor Feb 20 '19 at 15:20
  • 1
    As a side note (and matter of opinion) I prefer the naming convention `...Async` where you add the `Async` suffix to any method that returns a `Task` (or `Task`). It helps with readability in the code. Example: `CreatePdfFileAsync()` and possibly `SendMailAsync`. – Igor Feb 20 '19 at 15:22
  • 1
    I suspect that `createPdfFromFooData` is doing a long blocking operation before yielding. Try this: `var createPdfTask = Task.Run(() => CeatePdfFile());` – Kevin Gosse Feb 20 '19 at 15:27
  • @KevinGosse there's an easier and more reusable fix for that - see answer – Marc Gravell Feb 20 '19 at 15:36
  • 1
    As for @Igor's question: it isn't really a question - you absolutely **must** not use `async void` in many contexts, including MVC - the sync context for MVC **actively prevents** `async void` - it throws an exception if you try – Marc Gravell Feb 20 '19 at 15:38
  • @KevinGosse thanks for your answer here in the comments as well. – Ender Doe Feb 20 '19 at 15:45
  • @EnderDoe btw, if adding the yield fixes this: you may wish to look deeper into `createPdfFromFooData` to see if it can be improved in terms of when it goes asynchronous – Marc Gravell Feb 20 '19 at 16:13
  • @EnderDoe: I strongly recommend using a standard distributed architecture (send the request to a reliable queue and have a completely separate worker process). Work queued to `QueueBackgroundWorkItem` *can be lost* in several scenarios. – Stephen Cleary Feb 20 '19 at 17:44

1 Answers1

2

If Kevin Gosse is correct, and the real problem here is that createPdfFromFooData and hence CeatePdfFile are doing lots of work before the first await: you can add a Task.Yield() to artificially force an additional await, essentially pushing the rest of the work onto the work queue for the relevant context (or the thread-pool, otherwise):

    async Task<Stream> CeatePdfFile()
    {
        await Task.Yield(); // force asynchronicity
        var pdf = await createPdfFromFooData(fooBody);

        return await pdfFileToStream(pdf);
    }
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900