I’ve encountered a pretty strange case when task execution is not continued after await in IIS (not sure if it’s related to IIS). I reproduced this issue using Azure Storage and following controller (full solution on github):
public class HomeController : Controller
{
private static int _count;
public ActionResult Index()
{
RunRequest(); //I don't want to wait on this task
return View(_count);
}
public async Task RunRequest()
{
CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
var cloudTable = account.CreateCloudTableClient().GetTableReference("test");
Interlocked.Increment(ref _count);
await Task.Factory.FromAsync<bool>(cloudTable.BeginCreateIfNotExists, cloudTable.EndCreateIfNotExists, null);
Trace.WriteLine("This part of task after await is never executed");
Interlocked.Decrement(ref _count);
}
}
I would expect the value of _count to be always 1 (when rendered in view), but if you hit F5 several time you’ll see that _count is incrementing after each refresh. That means that continuation is not called for some reason.
In fact I’ve lied a bit, I’ve noticed that continuation is called once, when Index is called for the first time. All further F5’s don’t decrement the counter.
If I change the method to be async:
public async Task<ActionResult> Index()
{
await RunRequest(); //I don't want to wait on this task
return View(_count);
}
everything starts working as expected except that I don’t want to keep client waiting for my asynchronous operation to finish.
So my question is: I would like to understand why this happens, and what is the consistent way to run “fire and forget” work, preferably without spanning new threads.
ASP.NET was not designed for fire-and-forget work; it was designed to serve HTTP requests. When an HTTP response is generated (when your action returns), that request/response cycle is complete.
Note that ASP.NET will feel free to take down your AppDomain any time that there are no active requests. This is normally done on shared hosts after an inactivity timeout, or when your AppDomain has had a certain number of garbage collections, or every 29 hours just for no reason at all.
So you don’t really want “fire and forget” – you want to produce the response but not have ASP.NET forget about it. The simple solution of
ConfigureAwait(false)will cause everyone to forget about it, which means that once in a blue moon your continuation could just get “lost”.I have a blog post that goes into more detail on this subject. In short, you want to record the work to be done in a persistent layer (like an Azure table) before your response is generated. That’s the ideal solution.
If you aren’t going to do the ideal solution, then you’re going to live dangerously. There is code in my blog post that will register
Tasks with the ASP.NET runtime, so that you can return a response early but notify ASP.NET that you’re not really done yet. This will prevent ASP.NET from taking down your site while you have outstanding work, but it will not protect you against more fundamental failures like a hard drive crash or someone tripping over the power cord of your server.The code in my blog post is duplicated below; it depends on the
AsyncCountdownEventin my AsyncEx library:It can be used like this for
asyncor synchronous code: