重用akka-stream流的优雅方式

时间:2016-12-28 16:28:00

标签: scala akka akka-stream

我正在寻找一种轻松重用akka-stream流的方法。

我将Flow打算重新用作函数,所以我想保留它的签名:

Flow[Input, Output, NotUsed]

现在,当我使用此流程时,我希望能够“调用”此流程并将结果保留在一边以便进一步处理。

所以我想从Flow发射[Input]开始,应用我的流程,然后继续使用Flow发光[(Input, Output)]

示例:

val s: Source[Int, NotUsed] = Source(1 to 10)

val stringIfEven = Flow[Int].filter(_ % 2 == 0).map(_.toString)

val via: Source[(Int, String), NotUsed] = ???

现在这不可能以直截了当的方式进行,因为将流与.via()结合起来会让我只发出流量[Output]

val via: Source[String, NotUsed] = s.via(stringIfEven)

替代方法是使我的可重用流发出[(Input, Output)],但这需要每个流都将其输入推送到所有阶段并使我的代码看起来很糟糕。

所以我想出了一个这样的组合器:

def tupledFlow[In,Out](flow: Flow[In, Out, _]):Flow[In, (In,Out), NotUsed] = {
  Flow.fromGraph(GraphDSL.create() { implicit b =>
  import GraphDSL.Implicits._

  val broadcast = b.add(Broadcast[In](2))
  val zip = b.add(Zip[In, Out])

  broadcast.out(0) ~> zip.in0
  broadcast.out(1) ~> flow ~> zip.in1

  FlowShape(broadcast.in, zip.out)
})

}

即将输入广播到流并且也直接在平行线中广播 - >两者都在'Zip'阶段,我将值加入元组。然后可以优雅地应用它:

val tupled: Source[(Int, String), NotUsed] = s.via(tupledFlow(stringIfEven))

一切都很棒但是当给定流量正在进行“过滤”操作时 - 这个组合器被卡住并停止处理更多事件。

我想这是由于'Zip'行为要求所有子流都做同样的事情 - 在我的情况下,一个分支直接传递给定对象,所以另一个子流不能忽略这个元素。 filter(),因为它确实 - 流停止,因为Zip正在等待推送。

有没有更好的方法来实现流量组合? 当'flow'忽略带有'filter'的元素时,我可以在tupledFlow中做些什么来获得所需的行为吗?

2 个答案:

答案 0 :(得分:3)

两种可能的方法 - 具有可争议的优雅 - 是:

1)避免使用过滤阶段,将过滤器变为Flow[Int, Option[Int], NotUsed]。这样您就可以像原始计划一样在整个图表周围应用压缩包装器。但是,代码看起来更加污染,并且通过传递None来增加开销。

val stringIfEvenOrNone = Flow[Int].map{
  case x if x % 2 == 0 => Some(x.toString)
  case _ => None
}

val tupled: Source[(Int, String), NotUsed] = s.via(tupledFlow(stringIfEvenOrNone)).collect{
  case (num, Some(str)) => (num,str)
}

2)分离过滤和转换阶段,并在压缩包装之前应用过滤阶段。可能是一个更轻量级和更好的妥协。

val filterEven = Flow[Int].filter(_ % 2 == 0)

val toString = Flow[Int].map(_.toString)

val tupled: Source[(Int, String), NotUsed] = s.via(filterEven).via(tupledFlow(toString))

修改

3)根据评论中的讨论,为了清楚起见,在此处发布另一个解决方案。

此流程包装器允许从给定流中发出每个元素,并与生成它的原始输入元素配对。它适用于任何类型的内部流(每个输入发出0,1个或更多元素)。

  def tupledFlow[In,Out](flow: Flow[In, Out, _]): Flow[In, (In,Out), NotUsed] =
    Flow[In].flatMapConcat(in => Source.single(in).via(flow).map( out => in -> out))

答案 1 :(得分:1)

我想出了一个TupledFlow的实现,当Flow使用filter()mapAsync()并且Flow包裹{0}或N时每个输入的元素:

   def tupledFlow[In,Out](flow: Flow[In, Out, _])(implicit materializer: Materializer, executionContext: ExecutionContext):Flow[In, (In,Out), NotUsed] = {
  val v:Flow[In, Seq[(In, Out)], NotUsed]  = Flow[In].mapAsync(4) { in: In =>
    val outFuture: Future[Seq[Out]] = Source.single(in).via(flow).runWith(Sink.seq)
    val bothFuture: Future[Seq[(In,Out)]] = outFuture.map( seqOfOut => seqOfOut.map((in,_)) )
    bothFuture
  }
  val onlyDefined: Flow[In, (In, Out), NotUsed] = v.mapConcat[(In, Out)](seq => seq.to[scala.collection.immutable.Iterable])
  onlyDefined
}

我在这里看到的唯一缺点就是我实例化并为单个实体实现了一个流 - 只是为了得到一个'将流调用为函数'的概念。

我没有对此进行过任何性能测试 - 但是由于在将来执行的包裹的Flow中进行了繁重的操作 - 我相信这样就可以了。

此实现传递了https://gist.github.com/kretes/8d5f2925de55b2a274148b69f79e55ac#file-tupledflowspec-scala

中的所有测试