web-dev-qa-db-ja.com

Web API + HttpClient:非同期操作がまだ保留中の非同期モジュールまたはハンドラーが完了しました

ASP.NET Web APIを使用していくつかのHTTP要求をプロキシするアプリケーションを作成しており、断続的なエラーの原因を特定するのに苦労しています。競合状態のように見えますが…完全にはわかりません。

詳細を説明する前に、アプリケーションの一般的な通信フローを示します。

  • クライアントはHTTPリクエストをプロキシ1に送信します。
  • プロキシ1 HTTPリクエストのコンテンツをプロキシ2に中継します
  • プロキシ2 HTTPリクエストのコンテンツをターゲットWebアプリケーションに中継します
  • Target Web App HTTP要求に応答し、応答がProxy 2にストリーミング(チャンク転送)されます
  • プロキシ2は、応答をプロキシ1に返します。これは、元の呼び出しに応答しますクライアント

プロキシアプリケーションは、.NET 4.5を使用してASP.NET Web API RTMで記述されています。リレーを実行するコードは次のようになります。

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

断続的に発生するエラーは次のとおりです。

非同期操作がまだ保留中に、非同期モジュールまたはハンドラーが完了しました。

このエラーは通常、プロキシアプリケーションへの最初の数回のリクエストで発生し、その後エラーは再び表示されません。

Visual Studioは、スローされたときに例外をキャッチしません。ただし、エラーはGlobal.asax Application_Errorイベントでキャッチできます。残念ながら、例外にはスタックトレースがありません。

プロキシアプリケーションはAzure Webロールでホストされます。

犯人を特定するための助けをいただければ幸いです。

49
Gavin Osborn

あなたの問題は微妙なものです:asyncに渡すPushStreamContentラムダはasync voidとして解釈されています( PushStreamContentコンストラクターのため はパラメータとしてActionsのみを取ります)。したがって、モジュール/ハンドラーの完了とそのasync voidラムダの完了との間に競合状態があります。

PostStreamContentは、ストリームのクローズを検出し、それをTask(モジュール/ハンドラーの完了)の終わりとして扱います。したがって、まだできるasync voidメソッドがないことを確認する必要がありますストリームが閉じられた後に実行します。 async Taskメソッドは問題ないので、修正する必要があります。

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

プロキシを少し良くしたい場合は、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;
  }
}

以前のコードは、リクエストごとに1つのスレッドをブロックしていました(ヘッダーが受信されるまで)。 asyncをコントローラーレベルまで使用することで、その間スレッドをブロックすることはありません。

65
Stephen Cleary

少し単純なモデルでは、実際にHttpContentsを直接使用して、リレー内でそれらを渡すことができます。比較的簡単な方法でコンテンツをバッファリングせずに、リクエストとレスポンスの両方を非同期で信頼する方法を示すサンプルをアップロードしました。

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

必要に応じて接続を再利用できるため、同じHttpClientインスタンスを再利用することも有益です。

同じエラーでここに上陸した他の人にいくつかの知恵を加えたいと思いますが、あなたのコードはすべて問題ないようです。これが発生する場所からコールツリー全体で関数に渡されるラムダ式を探します。

MVC 5.xコントローラーアクションへのJavaScript JSON呼び出しでこのエラーが発生していました。スタックの上下で行うことはすべてasync Taskで定義され、awaitを使用して呼び出されました。

ただし、Visual Studioの「次のステートメントを設定」機能を使用して、行を体系的にスキップして、どの行が原因かを判断しました。外部のNuGetパッケージの呼び出しに到達するまで、ローカルメソッドへのドリルダウンを続けました。呼び出されたメソッドはパラメータとしてActionを取り、このActionに渡されたラムダ式の前にasyncキーワードがありました。 Stephen Clearyが上記の回答で指摘したように、これはasync voidとして扱われますが、MVCはそれを好みません。幸いなことに、パッケージには同じメソッドの*非同期バージョンがあります。同じパッケージへのいくつかのダウンストリーム呼び出しとともに、それらを使用するように切り替えると、問題が修正されました。

私はこれが問題の新しい解決策ではないことを認識していますが、async voidまたはasync <Action>呼び出しがないと思ったため、このスレッドを何度か検索して問題を解決しようとしました。 、そして私は他の誰かがそれを避けるのを助けたかった。

3
Dave Parker