Akka流-Source.unfoldAsync

时间:2019-03-23 17:56:17

标签: scala akka akka-stream akka-http

我目前正在尝试读取分页的HTTP资源。每个页面都是一个多部分文档,如果页面中包含更多内容,则页面的响应在标题中会包含一个next链接。然后,自动解析器可以从最早的页面开始,然后使用标题逐页阅读以构造对下一页的请求。

我将Akka Streams和Akka Http用于实现,因为我的目标是创建一个流解决方案。我想到了这一点(我将仅在此处包括代码的相关部分,请随时查看this gist的整个代码):

def read(request: HttpRequest): Source[HttpResponse, _] =
  Source.unfoldAsync[Option[HttpRequest], HttpResponse](Some(request))(Crawl.crawl)

val parse: Flow[HttpResponse, General.BodyPart, _] = Flow[HttpResponse]
  .flatMapConcat(r => Source.fromFuture(Unmarshal(r).to[Multipart.General]))
  .flatMapConcat(_.parts)

....

def crawl(reqOption: Option[HttpRequest]): Future[Option[(Option[HttpRequest], HttpResponse)]] = reqOption match {
  case Some(req) =>
    Http().singleRequest(req).map { response =>
      if (response.status.isFailure()) Some((None, response))
      else nextRequest(response, HttpMethods.GET)
    }
  case None => Future.successful(None)
}

因此,一般的想法是使用Source.unfoldAsync来浏览页面并执行HTTP请求(该想法和实现与this answer中描述的非常接近。这将创建一个{{ 1}}然后可以使用(解组为多部分,拆分为各个部分,...)。

我现在的问题是,Source[HttpResponse, _]的使用可能要花一些时间(如果页面很大,则解组会花费一些时间,也许最后会有一些数据库请求来保存一些数据,.. )。因此,如果下游速度较慢,我希望HttpResponse承受背压。默认情况下,下一个HTTP请求将在前一个HTTP请求结束后立即启动。

所以我的问题是:是否有某种方法可以在缓慢的下游产生Source.unfoldAsync背压?如果没有,是否有其他方法可以使背压成为可能?

我可以想象一种解决方案,该解决方案利用here中所述的akka​​-http提供的主机级客户端API以及一个循环图,在循环图中,第一个请求的响应将用作对生成第二个请求,但是我还没有尝试过,所以我不确定这是否行得通。


编辑:经过几天的闲逛并阅读了文档和一些博客,我不确定我是否假设{{1}的背压行为是否正确}是根本原因。要添加更多观察值:

  • 流开始时,我看到有几个请求正在发送。首先,这没有问题,只要及时消费所产生的Source.unfoldAsync(有关说明,请参见here
  • 如果我不更改默认的Source.unfoldAsync,则会遇到以下错误(我删除了网址):
    HttpResponse
    这将导致response-entity-subscription-timeout终止流:[WARN] [03/30/2019 13:44:58.984] [default-akka.actor.default-dispatcher-16] [default/Pool(shared->http://....)] [1 (WaitingForResponseEntitySubscription)] Response entity was not subscribed after 1 seconds. Make sure to read the response entity body or call discardBytes() on it. GET ... Empty -> 200 OK Chunked
  • 我观察到,响应的解组是流中最慢的部分,这可能是有道理的,因为响应主体是一个多部分文档,因此相对较大。但是,我希望流的这一部分向上游发出较少的信号(在本例中为IllegalStateException部分)。这应该导致这样一个事实,即发出的请求更少。
  • 有些谷歌搜索将我引向a discussion about an issue that seems to describe a similar problem。他们还讨论了响应处理速度不够快时出现的问题。 associated merge request将带来文档更改,这些更改建议在继续流之前完全消耗java.lang.IllegalStateException: Substream Source cannot be materialized more than once。在对该问题的讨论中,对whether or not it's a good idea to combine Akka Http with Akka Streams也有疑问。因此,也许我将不得不更改实现以直接在Source.unfoldAsync所调用的函数内进行解组。

2 个答案:

答案 0 :(得分:1)

根据Source.unfoldAsync的{​​{3}},仅在拉出源时才调用传入的函数:

def onPull(): Unit = f(state).onComplete(asyncHandler)(akka.dispatch.ExecutionContexts.sameThreadExecutionContext)

因此,如果下游没有拉动(向后加压),则不会调用传递给源的函数。

在要点上,您使用runForeach(与runWith(Sink.foreach)相同)将println完成后拉到上游。因此,在这里很难注意到背压。

尝试将示例更改为runWith(Sink.queue),这将为您带来SinkQueueWithCancel的物化值。然后,除非您在队列上调用pull,否则流将被反压且不会发出请求。

请注意,可能存在一个或多个初始请求,直到背压在所有流中传播为止。

答案 1 :(得分:0)

我想我明白了。正如我在问题编辑中已经提到的那样,我发现this comment是Akka HTTP中的一个问题,作者说:

  

...将Akka http混合到更大的处理流中根本不是最佳实践。相反,您需要在流的Akka http部分周围设置边界,以确保它们在允许外部处理流继续之前始终消耗其响应。

因此,我继续进行尝试:我没有进行HTTP请求和在流的不同阶段进行解组,而是通过flatMapFuture[HttpResponse]变成{{1 }}。这样可以确保Future[Multipart.General]被直接使用,并避免了HttpResponse错误。 Response entity was not subscribed after 1 second函数现在看起来有点不同,因为它必须返回未编组的crawl对象(以进行进一步处理)以及原始的Multipart.General(以便能够构造出下一个请求标头):

HttpResponse

因此,其余代码必须更改。我创建了another gist,其中包含等效的代码,例如原始问题的要旨。

我期望两个Akka项目能够更好地集成(文档目前没有提到此限制,而是HTTP API似乎鼓励用户一起使用Akka HTTP和Akka Streams),所以感觉有点像变通办法,但现在可以解决我的问题。我仍然需要弄清楚将这一部分集成到我的较大用例中时遇到的其他问题,但这不是这里问题的一部分。