ChatGPT解决这个技术问题 Extra ChatGPT

Fire and forget async method in asp.net mvc

The general answers such as here and here to fire-and-forget questions is not to use async/await, but to use Task.Run or TaskFactory.StartNew passing in the synchronous method instead.
However, sometimes the method that I want to fire-and-forget is async and there is no equivalent sync method.

Update Note/Warning: As Stephen Cleary pointed out below, it is dangerous to continue working on a request after you have sent the response. The reason is because the AppDomain may be shut down while that work is still in progress. See the link in his response for more information. Anyways, I just wanted to point that out upfront, so that I don't send anyone down the wrong path.

I think my case is valid because the actual work is done by a different system (different computer on a different server) so I only need to know that the message has left for that system. If there is an exception there is nothing that the server or user can do about it and it does not affect the user, all I need to do is refer to the exception log and clean up manually (or implement some automated mechanism). If the AppDomain is shut down I will have a residual file in a remote system, but I will pick that up as part of my usual maintenance cycle and since its existence is no longer known by my web server (database) and its name is uniquely timestamped, it will not cause any issues while it still lingers.

It would be ideal if I had access to a persistence mechanism as Stephen Cleary pointed out, but unfortunately I don't at this time.

I considered just pretending that the DeleteFoo request has completed fine on the client side (javascript) while keeping the request open, but I need information in the response to continue, so it would hold things up.

So, the original question...

for example:

//External library
public async Task DeleteFooAsync();

In my asp.net mvc code I want to call DeleteFooAsync in a fire-and-forget fashion - I don't want to hold up the response waiting for DeleteFooAsync to complete. If DeleteFooAsync fails (or throws an exception) for some reason, there is nothing that the user or the program can do about it so I just want to log an error.

Now, I know that any exceptions will result in unobserved exceptions, so the simplest case I can think of is:

//In my code
Task deleteTask = DeleteFooAsync()

//In my App_Start
TaskScheduler.UnobservedTaskException += ( sender, e ) =>
{
    m_log.Debug( "Unobserved exception! This exception would have been unobserved: {0}", e.Exception );
    e.SetObserved();
};

Are there any risks in doing this?

The other option that I can think of is to make my own wrapper such as:

private void async DeleteFooWrapperAsync()
{
    try
    {
        await DeleteFooAsync();
    }
    catch(Exception exception )
    {
        m_log.Error("DeleteFooAsync failed: " + exception.ToString());
    }
}

and then call that with TaskFactory.StartNew (probably wrapping in an async action). However this seems like a lot of wrapper code each time I want to call an async method in a fire-and-forget fashion.

My question is, what it the correct way to call an async method in a fire-and-forget fashion?

UPDATE:

Well, I found that the following in my controller (not that the controller action needs to be async because there are other async calls that are awaited):

[AcceptVerbs( HttpVerbs.Post )]
public async Task<JsonResult> DeleteItemAsync()
{
    Task deleteTask = DeleteFooAsync();
    ...
}

caused an exception of the form:

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.Web.ThreadContext.AssociateWithCurrentThread(BooleansetImpersonationContext)

This is discussed here and seems to be to do with the SynchronizationContext and 'the returned Task was transitioned to a terminal state before all async work completed'.

So, the only method that worked was:

Task foo = Task.Run( () => DeleteFooAsync() );

My understanding of why this works is because StartNew gets a new thread for DeleteFooAsync to work on.

Sadly, Scott's suggestion below does not work for handling exceptions in this case, because foo is not a DeleteFooAsync task anymore, but rather the task from Task.Run, so does not handle the exceptions from DeleteFooAsync. My UnobservedTaskException does eventually get called, so at least that still works.

So, I guess the question still stands, how do you do fire-and-forget an async method in asp.net mvc?

lol. That is another question that I asked. That question is about how to handle unobserved exceptions. This question is about how to do fire-and-forget method calls where the fire-and-forget method is async.
As Stephen C points out, this is problematic on asp.net. On Azure, you can do this reliably with web jobs. See curah.microsoft.com/52143/…
Does DeleteFooAsync have await calls in its body? I wonder if simply adding .ConfigureAwait(false) to those would do the trick (of course the fire-and-forget method call itself is not awaited). Something at some point is trying to post back to a particular thread via the synchronization context. If you configure the awaitable to not bother, i would imagine it would come back to an available thread. Just a theory. I had the same problem in my project and decided to refactor to not be fire and forget. I did notice i had some await statements in mine without ConfigureAwait(false)

S
Stephen Cleary

First off, let me point out that "fire and forget" is almost always a mistake in ASP.NET applications. "Fire and forget" is only an acceptable approach if you don't care whether DeleteFooAsync actually completes.

If you're willing to accept that limitation, I have some code on my blog that will register tasks with the ASP.NET runtime, and it accepts both synchronous and asynchronous work.

You can write a one-time wrapper method for logging exceptions as such:

private async Task LogExceptionsAsync(Func<Task> code)
{
  try
  {
    await code();
  }
  catch(Exception exception)
  {
    m_log.Error("Call failed: " + exception.ToString());
  }
}

And then use the BackgroundTaskManager from my blog as such:

BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));

Alternatively, you can keep TaskScheduler.UnobservedTaskException and just call it like this:

BackgroundTaskManager.Run(() => DeleteFooAsync());

Thanks, that is brilliant. I see from the blog that the reason that this is really bad is because the AppDomain can be torn down during the fire-and-forget op. In my case DeleteFooAsync is handled by a different system, so it is a matter of knowing that the message has left my process. The reason that it is fire-and-forget is because it is a reasonably long running operation that I don't want to hold the user up waiting for a response and neither the user nor the server can do anything about an exception and there are no adverse effect on the user. Thanks again, I will think about it.
I am sufficiently confused by this question and others about exceptions because........ why would you do something long running that the user doesn't care about?!. ....."...there are no adverse effect on the user."
@Andyz Smith: Simple contrived example - a maintenance cycle that is triggered every nth user image retrieval request to index/rationalise a local cache of heavily used files on the Web Server where the main file repository is remote storage, such as Azure file store. The user doesn't care about it. It doesn't really affect the user if it fails. No major fuss if the AppDomain is closed down and restarted, the maintenance will happen on the next cycle.
Disconnect this from the UI. Use an Observer pattern. At the very least, abandon this asynchronous system and instead just complete a atomic update to a table that says, hey this user used this image again.
@BrettMathe: It's not about crashing; it's about recycling. By design, ASP.NET will periodically recycle your AppDomain "Just Because(tm)".
K
Korayem

As of .NET 4.5.2, you can do the following

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync());

But it only works within ASP.NET domain

The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed. This method can't be called outside an ASP.NET managed app domain.

More here: https://msdn.microsoft.com/en-us/library/ms171868(v=vs.110).aspx#v452


I couldn't get it to work using an async callback calling an awaited method within. It would enter into the nested method, but never complete. The following worked for me though: HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => LongMethodAsync());
@Sinjai - Because as per this answer the background tasks hold down threads from the normal thread pool. if you get too many long running tasks you will run out of threads in your pool and won't be able to serve requests.
S
Scott Chamberlain

The best way to handle it is use the ContinueWith method and pass in the OnlyOnFaulted option.

private void button1_Click(object sender, EventArgs e)
{
    var deleteFooTask = DeleteFooAsync();
    deleteFooTask.ContinueWith(ErrorHandeler, TaskContinuationOptions.OnlyOnFaulted);
}

private void ErrorHandeler(Task obj)
{
    MessageBox.Show(String.Format("Exception happened in the background of DeleteFooAsync.\n{0}", obj.Exception));
}

public async Task DeleteFooAsync()
{
    await Task.Delay(5000);
    throw new Exception("Oops");
}

Where I put my message box you would put your logger.


Thanks, that is quite neat. I guess it means an extra line for each fire-and-forget async call. Why is this better than handling with TaskScheduler.UnobservedTaskException?
I guess it allows for more fine grained handling of the exception. For example you may want to log errors in a special way for some parts of the code or do a special action (say the Exception was a TimeoutException, maybe try again a few times before giving up and just logging the error), TaskScheduler.UnobservedTaskException is a global option so you don't have that control.
Yup, that is what I thought. Makes sense.
I think I will use this approach for specific cases and TaskScheduler.UnobservedTaskException for the general case. Do you know if when ContinueWith is called the exception is then observed? I.e. TaskScheduler.UnobservedTaskException will not be called with this exception.
Sounds good. Is there a canonical reference to a Best Practice someone can provide on this technique?