web-dev-qa-db-fra.com

Web Api + HttpClient: module ou gestionnaire asynchrone terminé alors qu'une opération asynchrone était toujours en attente

J'écris une application qui procède par proxy à certaines requêtes HTTP à l'aide de l'API Web ASP.NET et j'ai du mal à identifier la source d'une erreur intermittente. Cela semble être une condition de course ... mais je ne suis pas tout à fait sûr.

Avant d'entrer dans les détails, voici le flux de communication général de l'application:

  • Client fait une requête HTTP à Proxy 1.
  • Proxy 1 relaie le contenu de la requête HTTP vers Proxy 2
  • Proxy 2 relaie le contenu de la requête HTTP vers l'application Web cible
  • Application Web cible répond à la demande HTTP et la réponse est diffusée (transfert par blocs) vers Proxy 2
  • Proxy 2 renvoie la réponse à Proxy 1 qui à son tour répond à l'appel d'origine Client.

Les applications proxy sont écrites dans l'API Web ASP.NET RTM en utilisant .NET 4.5. Le code pour effectuer le relais ressemble à ceci:

//Controller entry point.
public HttpResponseMessage Post()
{
    using (var client = new HttpClient())
    {
        var request = BuildRelayHttpRequest(this.Request);

        //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
        //As it begins to filter in.
        var relayResult = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;

        var returnMessage = BuildResponse(relayResult);
        return returnMessage;
    }
}

private static HttpRequestMessage BuildRelayHttpRequest(HttpRequestMessage incomingRequest)
{
    var requestUri = BuildRequestUri();
    var relayRequest = new HttpRequestMessage(incomingRequest.Method, requestUri);
    if (incomingRequest.Method != HttpMethod.Get && incomingRequest.Content != null)
    {
       relayRequest.Content = incomingRequest.Content;
    }

    //Copies all safe HTTP headers (mainly content) to the relay request
    CopyHeaders(relayRequest, incomingRequest);
    return relayRequest;
}

private static HttpRequestMessage BuildResponse(HttpResponseMessage responseMessage)
{
    var returnMessage = Request.CreateResponse(responseMessage.StatusCode);
    returnMessage.ReasonPhrase = responseMessage.ReasonPhrase;
    returnMessage.Content = CopyContentStream(responseMessage);

    //Copies all safe HTTP headers (mainly content) to the response
    CopyHeaders(returnMessage, responseMessage);
}

private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
    var content = new PushStreamContent(async (stream, context, transport) =>
            await sourceContent.Content.ReadAsStreamAsync()
                            .ContinueWith(t1 => t1.Result.CopyToAsync(stream)
                                .ContinueWith(t2 => stream.Dispose())));
    return content;
}

L'erreur qui se produit par intermittence est:

Un module ou gestionnaire asynchrone s'est terminé alors qu'une opération asynchrone était toujours en attente.

Cette erreur se produit généralement sur les premières demandes adressées aux applications proxy, après quoi l'erreur n'est plus vue.

Visual Studio ne capture jamais l'exception lorsqu'il est levé. Mais l'erreur peut être interceptée dans l'événement Global.asax Application_Error. Malheureusement, l'exception n'a pas de trace de pile.

Les applications proxy sont hébergées dans Azure Web Roles.

Toute aide permettant d'identifier le coupable serait appréciée.

49
Gavin Osborn

Votre problème est subtil: le async lambda que vous passez à PushStreamContent est interprété comme un async void (car le constructeur PushStreamContent ne prend que Actions comme paramètres). Il y a donc une condition de concurrence entre la fin de votre module/gestionnaire et la fin de cette async void lambda.

PostStreamContent détecte la fermeture du flux et la traite comme la fin de son Task (achèvement du module/gestionnaire), il vous suffit donc de vous assurer qu'il n'y a pas de async void méthodes qui pourraient encore s'exécuter après la fermeture du flux. async Task les méthodes sont OK, donc cela devrait le corriger:

private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
  Func<Stream, Task> copyStreamAsync = async stream =>
  {
    using (stream)
    using (var sourceStream = await sourceContent.Content.ReadAsStreamAsync())
    {
      await sourceStream.CopyToAsync(stream);
    }
  };
  var content = new PushStreamContent(stream => { var _ = copyStreamAsync(stream); });
  return content;
}

Si vous souhaitez que vos proxys évoluent un peu mieux, je vous recommande également de vous débarrasser de tous les appels Result:

//Controller entry point.
public async Task<HttpResponseMessage> PostAsync()
{
  using (var client = new HttpClient())
  {
    var request = BuildRelayHttpRequest(this.Request);

    //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
    //As it begins to filter in.
    var relayResult = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

    var returnMessage = BuildResponse(relayResult);
    return returnMessage;
  }
}

Votre ancien code bloquerait un thread pour chaque demande (jusqu'à ce que les en-têtes soient reçus); en utilisant async jusqu'au niveau de votre contrôleur, vous ne bloquerez pas un thread pendant ce temps.

65
Stephen Cleary

Un modèle un peu plus simple est que vous pouvez simplement utiliser directement les HttpContents et les passer à l'intérieur du relais. Je viens de télécharger un exemple illustrant comment vous pouvez compter à la fois de manière asynchrone les demandes et les réponses sans mettre le contenu en mémoire tampon de manière relativement simple:

http://aspnet.codeplex.com/SourceControl/changeset/view/7ce67a547fd0#Samples/WebApi/RelaySample/ReadMe.txt

Il est également avantageux de réutiliser la même instance HttpClient car cela vous permet de réutiliser les connexions le cas échéant.

4

Je voudrais ajouter un peu de sagesse à quiconque a atterri ici avec la même erreur, mais tout votre code semble correct. Recherchez toutes les expressions lambda passées dans des fonctions à travers l'arborescence d'appels d'où cela se produit.

J'obtenais cette erreur lors d'un appel JavaScript JSON à une action de contrôleur MVC 5.x. Tout ce que je faisais de haut en bas de la pile était défini async Task et appelé à l'aide de await.

Cependant, en utilisant la fonctionnalité "Définir la déclaration suivante" de Visual Studio, j'ai sauté systématiquement les lignes pour déterminer celle qui l'a provoquée. J'ai continué à explorer les méthodes locales jusqu'à ce que j'appelle dans un package NuGet externe. La méthode appelée a pris un Action comme paramètre et l'expression lambda transmise pour cette action a été précédée du mot clé async. Comme le souligne Stephen Cleary ci-dessus dans sa réponse, cela est traité comme un async void, ce que MVC n'aime pas. Heureusement, le paquet avait des versions * Async des mêmes méthodes. Passer à l'utilisation de ceux-ci, ainsi que certains appels en aval vers le même package ont résolu le problème.

Je me rends compte que ce n'est pas une nouvelle solution au problème, mais j'ai dépassé ce fil plusieurs fois dans mes recherches en essayant de résoudre le problème parce que je pensais que je n'avais pas de async void ou async <Action> appelle, et je voulais aider quelqu'un d'autre à éviter cela.

3
Dave Parker