web-dev-qa-db-fra.com

Déclencher et oublier la méthode asynchrone dans asp.net mvc

Les réponses générales telles que ici et ici à tirez et oubliez questions n'est pas d'utiliser asynchrone/attendre, mais d'utiliser Task.Run ou TaskFactory.StartNew en passant à la place la méthode synchrone.
Cependant, parfois la méthode que je veux tirer et oublier est asynchrone et il n'y a pas de méthode de synchronisation équivalente.

Note de mise à jour/Avertissement: Comme Stephen Cleary l'a souligné ci-dessous, il est dangereux de continuer à travailler sur une demande après avoir envoyé la réponse. La raison en est que l'AppDomain peut être arrêté pendant que ce travail est toujours en cours. Voir le lien dans sa réponse pour plus d'informations. Quoi qu'il en soit, je voulais juste le signaler dès le départ, afin de ne pas envoyer quelqu'un sur le mauvais chemin.

Je pense que mon cas est valide parce que le travail réel est effectué par un système différent (un ordinateur différent sur un serveur différent), j'ai donc seulement besoin de savoir que le message est parti pour ce système. S'il y a une exception, le serveur ou l'utilisateur ne peut rien y faire et cela n'affecte pas l'utilisateur, tout ce que je dois faire est de consulter le journal des exceptions et de nettoyer manuellement (ou de mettre en œuvre un mécanisme automatisé). Si l'AppDomain est arrêté, j'aurai un fichier résiduel dans un système distant, mais je le récupérerai dans le cadre de mon cycle de maintenance habituel et puisque son existence n'est plus connue de mon serveur Web (base de données) et son nom est unique horodaté, il ne causera aucun problème tant qu'il persistera.

Ce serait idéal si j'avais accès à un mécanisme de persistance comme l'a souligné Stephen Cleary, mais malheureusement je ne le fais pas pour le moment.

J'ai envisagé de simplement prétendre que la demande DeleteFoo s'est bien terminée du côté client (javascript) tout en gardant la demande ouverte, mais j'ai besoin d'informations dans la réponse pour continuer, donc cela tiendrait les choses.

Donc, la question d'origine ...

par exemple:

//External library
public async Task DeleteFooAsync();

Dans mon code mvc asp.net, je veux appeler DeleteFooAsync d'une manière qui tire et oublie - je ne veux pas retarder la réponse en attendant que DeleteFooAsync se termine. Si DeleteFooAsync échoue (ou lève une exception) pour une raison quelconque, il n'y a rien que l'utilisateur ou le programme puisse faire à ce sujet, donc je veux simplement enregistrer une erreur.

Maintenant, je sais que toute exception entraînera des exceptions non observées, donc le cas le plus simple auquel je peux penser est:

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

Y a-t-il des risques à le faire?

L'autre option à laquelle je peux penser est de créer mon propre emballage tel que:

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

puis appelez cela avec TaskFactory.StartNew (encapsulant probablement dans une action asynchrone). Cependant, cela ressemble à beaucoup de code wrapper chaque fois que je veux appeler une méthode asynchrone de façon incendiaire.

Ma question est la suivante: quelle est la bonne façon d'appeler une méthode asynchrone d'une manière qui tire et oublie?

MISE À JOUR:

Eh bien, j'ai constaté que ce qui suit dans mon contrôleur (pas que l'action du contrôleur doit être asynchrone car d'autres appels asynchrones sont attendus):

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

a provoqué une exception de la forme:

Exception non gérée: System.NullReferenceException: référence d'objet non définie sur une instance d'un objet. à System.Web.ThreadContext.AssociateWithCurrentThread (BooleansetImpersonationContext)

Ceci est discuté ici et semble être lié au SynchronizationContext et 'la tâche retournée a été transférée dans un état terminal avant que tout le travail asynchrone soit terminé'.

Donc, la seule méthode qui a fonctionné était:

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

Ma compréhension de la raison pour laquelle cela fonctionne est que StartNew obtient un nouveau thread sur lequel DeleteFooAsync peut travailler.

Malheureusement, la suggestion de Scott ci-dessous ne fonctionne pas pour gérer les exceptions dans ce cas, car foo n'est plus une tâche DeleteFooAsync, mais plutôt la tâche de Task.Run, donc ne gère pas les exceptions de DeleteFooAsync. Mon UnobservedTaskException finit par être appelée, donc au moins cela fonctionne toujours.

Donc, je suppose que la question se pose toujours, comment faites-vous pour tirer et oublier une méthode asynchrone dans asp.net mvc?

52
acarlon

Tout d'abord, permettez-moi de souligner que "tirer et oublier" est presque toujours une erreur dans les applications ASP.NET. "Tirez et oubliez" n'est une approche acceptable que si vous ne vous souciez pas de savoir si DeleteFooAsync se termine réellement.

Si vous êtes prêt à accepter cette limitation, j'ai du code sur mon blog qui enregistrera les tâches avec le runtime ASP.NET, et il accepte à la fois le travail synchrone et asynchrone.

Vous pouvez écrire une méthode d'encapsulation unique pour la journalisation des exceptions en tant que telle:

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

Et puis utilisez le BackgroundTaskManager de mon blog en tant que tel:

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

Vous pouvez également conserver TaskScheduler.UnobservedTaskException et appelez-le comme ceci:

BackgroundTaskManager.Run(() => DeleteFooAsync());
50
Stephen Cleary

Depuis .NET 4.5.2, vous pouvez effectuer les opérations suivantes

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

Mais cela ne fonctionne que dans le domaine ASP.NET

La méthode HostingEnvironment.QueueBackgroundWorkItem vous permet de planifier de petits éléments de travail en arrière-plan. ASP.NET effectue le suivi de ces éléments et empêche IIS d'arrêter brutalement le processus de travail jusqu'à ce que tous les éléments de travail en arrière-plan soient terminés. Cette méthode ne peut pas être appelée en dehors d'un domaine d'application géré par ASP.NET.

Plus ici: https://msdn.Microsoft.com/en-us/library/ms171868 (v = vs.110) .aspx # v452

14
Korayem

La meilleure façon de le gérer est d'utiliser la méthode ContinueWith et de passer l'option OnlyOnFaulted .

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

Où je mettrais ma boîte de message, vous mettriez votre enregistreur.

8
Scott Chamberlain