Web Api + HttpClient: 一个异步模块或处理程序在异步操作仍未完成时已经完成了。

50
I'm writing an application that proxies some HTTP requests using the ASP.NET Web API and I am struggling to identify the source of an intermittent error. It seems like a race condition... but I'm not entirely sure.
Before I go into detail here is the general communication flow of the application:
- The Client makes a HTTP request to Proxy 1. - Proxy 1 relays the contents of the HTTP request to Proxy 2. - Proxy 2 relays the contents of the HTTP request to the Target Web Application. - Target Web App responds to the HTTP request and the response is streamed (chunked transfer) to Proxy 2. - Proxy 2 returns the response to Proxy 1 which in turn responds to the original calling Client.
The Proxy applications are written in ASP.NET Web API RTM using .NET 4.5. The code to perform the relay looks like so:
//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角色中。
如果能够帮助识别罪犯将不胜感激。

CopyHeaders是我编写的一个方法,用于中继我认为适合复制到我的应用程序中的HTTP标头。它没有包含在这里,因为它与我尝试解决的问题无关。我最终采取的解决方案类似于下面接受的答案,应该足以让你编写出类似的解决方案。 - Gavin Osborn
3个回答

70
您的问题比较微妙:您传递给PushStreamContentasync lambda被解释为async void(因为PushStreamContent构造函数只接受Action作为参数)。因此,您的模块/处理程序完成和该async void lambda完成之间存在竞争条件。

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

你以前的代码会为每个请求阻塞一个线程(直到接收到头文件);通过在控制器层面上使用async,你在此期间不会阻塞线程。


有趣的 - 我会调查你的解决方案并回复你。我知道我可以通过异步方式进行一些性能改进 - 这是我在消除这个奇怪问题后的下一个挑战。 - Gavin Osborn
1
嗨,史蒂芬,你的解决方案不是关闭了错误的流吗?它通过dispose关闭了传入的流而不是传出的内容流,因此请求永远不会结束(直到超时)。 - Gavin Osborn
@Stephen copyToStreamAsync的执行返回一个Task,所以var a实际上是Task a,对吗?我没有看到任何保证,确保该任务完成。相反,我会更改构造方式为var content = new PushStreamContent((stream, h, t) => copyToStreamAsync(stream).Wait());(显然,应避免使用.Wait,但在混合异步和同步时似乎有时是必要的。)那么你的代码为什么有效?我的发布的代码如何比较? - Dan Friedman
1
@DanFriedman:_ 是一个 Task_CopyToAsync 完成后完成。关键在于当 stream 关闭时,PushStreamContent 将被视为“完成”。因此,只有当 copyToStreamAsync(stream) 关闭 stream 时,您的代码才能正常工作。与 Wait 的主要区别在于它会阻塞线程(并可能导致死锁)。 - Stephen Cleary
这对我很有帮助。虽然我没有太多使用async的经验,但这里的教训似乎是你应该始终将async方法的返回类型设置为TaskTask<>,这让我想到语言应该要求这样做。我的代码看起来不像OP的代码,但我有一个async void,将其更改为async Task解决了问题。 - Jonathan Wood
@StephenCleary copyToStreamAsync 应该是 copyStreamAsync。这样正确吗? - jokab

6
我想为所有遇到相同错误,但代码看起来没有问题的人添加一些智慧。查找从发生错误的调用树中传递给函数的任何 lambda 表达式。
我在对 MVC 5.x 控制器操作进行 JavaScript JSON 调用时遇到了这个错误。我在整个堆栈中所做的一切都定义为 async Task 并使用 await 进行调用。
然而,使用 Visual Studio 的“设置下一个语句”功能,我系统地跳过行以确定哪一行引起了问题。我一直钻研本地方法,直到调用外部 NuGet 包的方法。被调用的方法将一个 Action 作为参数,并且为该 Action 传递的 lambda 表达式前面有 async 关键字。正如 Stephen Cleary 在他的回答中指出的那样,这被视为 async void,MVC 不喜欢。幸运的是,该包有 *Async 版本的相同方法。换成使用它们,连同某些下游对该软件包的调用,解决了问题。
我意识到这不是问题的新颖解决方案,但我在寻找解决此问题的方法时多次跳过了这个线程,因为我认为我没有任何 async voidasync <Action> 的调用,并且我想帮助别人避免这种情况。

非常好的答案。这个问题让我很困惑,但是这个答案帮助我指出了问题的方向。在我的情况下,我正在使用MVC Web API,并从构造函数调用异步方法。将构造函数调用非异步方法解决了这个问题。 - Bern

4

谢谢 Henrik,那实际上就是我最终想出的解决方案。由于我们中继的某些特定情况,我可以在我们系统的90%路线中使用它 - 但我必须依靠PushContent响应来处理其余部分。 - Gavin Osborn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接