通过akka流链接数据

时间:2019-12-29 09:50:55

标签: scala akka akka-stream

我正在将一些C#代码转换为scala和akka流。

我的C#代码看起来像这样:


Task<Result1> GetPartialResult1Async(Request request) ...
Task<Result2> GetPartialResult2Async(Request request) ...

async Task<Result> GetResultAsync(Request request) 
{
    var result1 = await GetPartialResult1Async(request);
    var result2 = await GetPartialResult2Async(request);
    return new Result(request, result1, result2);
}

现在用于akka流。我没有从结果的RequestTask的功能,而是从请求到结果。

所以我已经有以下两个流程:

val partialResult1Flow: Flow[Request, Result1, NotUsed] = ...
val partialResult2Flow: Flow[Request, Result2, NotUsed] = ...

但是我看不到如何将它们组合成一个完整的流程,因为通过在第一个流程上调用,我们会丢失原始请求,而通过在第二个流程上调用,我们会丢失第一个流程的结果。 / p>

所以我创建了一个WithState monad,它看起来像这样:

case class WithState[+TState, +TValue](value: TValue, state: TState) {
  def map[TResult](func: TValue => TResult): WithState[TState, TResult] = {
    WithState(func(value), state)
  }
  ... bunch more helper functions go here
}

然后,我正在重写原始流程,如下所示:

def partialResult1Flow[TState]: Flow[WithState[TState, Request], WithState[TState, Result1]] = ...
def partialResult2Flow: Flow[WithState[TState, Request], WithState[TState, Result2]] = ...

并像这样使用它们:

val flow = Flow[Request]
    .map(x => WithState(x, x))
    .via(partialResult1Flow)
    .map(x => WithState(x.state, (x.state, x.value))
    .via(partialResult2Flow)
    .map(x => Result(x.state._1, x.state._2, x.value))

现在这可行,但是我当然不能保证将如何使用流程。所以我真的应该使它带有一个State参数:

def flow[TState] = Flow[WithState[TState, Request]]
    .map(x => WithState(x.value, (x.state, x.value)))
    .via(partialResult1Flow)
    .map(x => WithState(x.state._2, (x.state, x.value))
    .via(partialResult2Flow)
    .map(x => WithState(Result(x.state._1._2, x.state._2, x.value), x.state._1._1))

现在,我的代码变得非常难以阅读。我可以通过命名函数,使用案例类而不是元组等来清理它。但是从根本上讲,这里有很多附带的复杂性,这是很难避免的。

我想念什么吗?这不是Akka流的好用例吗?有一些内置的方法吗?

3 个答案:

答案 0 :(得分:1)

我同意使用Akka Streams进行反压很有用。但是,我不认为将partialResult的计算建模为流在这里有用。具有基于Future的“内部”逻辑并将其包装在流程的mapAsync中,以将反压作为一个单元应用于整个操作似乎更简单,甚至更好。

这基本上是Levi Ramsey先前的出色回答的精简重构:

import scala.concurrent.{ ExecutionContext, Future }
import akka.NotUsed
import akka.stream._
import akka.stream.scaladsl._

case class Request()
case class Result1()
case class Result2()
case class Response(r: Request, r1: Result1, r2: Result2)

def partialResult1(req: Request): Future[Result1] = ???
def partialResult2(req: Request): Future[Result2] = ???

val system = akka.actor.ActorSystem()
implicit val ec: ExecutionContext = system.dispatcher

val flow: Flow[Request, Response, NotUsed] =
  Flow[Request]
    .mapAsync(parallelism = 12) { req =>
      for {
        res1 <- partialResult1(req)
        res2 <- partialResult2(req)
      } yield (Response(req, res1, res2))
    }

我将从此开始,只有在您知道有理由将partialResult1partialResult2拆分为单独的阶段时,才在Flow中引入一个中间步骤。根据您的要求,mapAsyncUnordered可能更合适。

答案 1 :(得分:0)

免责声明,我对C#的async / await并不完全熟悉。

根据我对C#文档的快速了解,Task<T>是严格(即渴望,而不是懒惰)评估的计算,如果成功,它将最终包含一个T 。 Scala的等效项是Future[T],其中C#代码的等效项是:

import scala.concurrent.{ ExecutionContext, Future }

def getPartialResult1Async(req: Request): Future[Result1] = ???
def getPartialResult2Async(req: Request): Future[Result2] = ???

def getResultAsync(req: Request)(implicit ectx: ExecutionContext): Future[Result] = {
  val result1 = getPartialResult1Async(req)
  val result2 = getPartialResult2Async(req)
  result1.zipWith(result2) { tup => val (r1, r2) = tup
    new Result(req, r1, r2)
  }
  /* Could also:
   *   for {
   *     r1 <- result1
   *     r2 <- result2
   *    } yield { new Result(req, r1, r2) }
   *
   * Note that both the `result1.zipWith(result2)` and the above `for`
   * construction may compute the two partial results simultaneously.  If you
   * want to ensure that the second partial result is computed after the first 
   * partial result is successfully computed:
   *   for {
   *     r1 <- getPartialResult1Async(req)
   *     r2 <- getPartialResult2Async(req)
   *   } yield new Result(req, r1, r2)
   */
}

在这种情况下,不需要Akka Streams,但是如果您还需要使用Akka Streams,则可以表示为

val actorSystem = ??? // In Akka Streams 2.6, you'd probably have this as an implicit val
val parallelism = ??? // Controls requests in flight

val flow = Flow[Request]
  .mapAsync(parallelism) { req =>
    import actorSystem.dispatcher

    getPartialResult1Async(req).map { r1 => (req, r1) }
  }
  .mapAsync(parallelism) { tup =>
    import actorSystem.dispatcher

    getPartialResult2Async(tup._2).map { r2 =>
      new Result(tup._1, tup._2, r2)
    }
  }

  /* Given the `getResultAsync` function in the previous snippet, you could also:
   *   val flow = Flow[Request].mapAsync(parallelism) { req =>
   *     getResultAsync(req)(actorSystem.dispatcher)
   *   }
   */

基于Future的实现的一个优点是,它很容易与要在给定上下文中使用的任何Scala抽象的并发/并行性(例如cats,akka流,akka)集成。我对Akka Streams集成的一般直觉是在第二个代码块中的注释中指向三层代码。

答案 2 :(得分:0)

我做的事情与我在问题中描述的完全不同。

但是电流可以显着改善:

阶段1:FlowWithContext

可以使用内置的WithState来代替自定义FlowWithContext单子。

这样做的好处是您可以在流上使用标准运算符,而不必担心转换WithState单子。 Akka会为您解决这个问题。

所以不是

def partialResult1Flow[TState]: Flow[WithState[TState, Request], WithState[TState, Result1]] = 
    Flow[WithState[TState, Request]].mapAsync(_ mapAsync {doRequest(_)})

我们可以写:

def partialResult1Flow[TState]: FlowWithContext[Request, TState, Result1, TState, NotUsed] = 
    FlowWithContext[Request, TState].mapAsync(doRequest(_))

不幸的是,虽然FlowWithContext在不需要更改上下文时很容易编写,但是在需要通过流将需要将一些当前数据移入流的情况下使用时有点麻烦。上下文(如我们所做的)。为此,您需要转换为Flow(使用asFlow),然后使用FlowWithContext返回到asFlowWithContext

在这种情况下,我发现最简单的做法是将整个内容写为Flow,最后转换为FlowWithContext

例如:

def flow[TState]: FlowWithContext[Request, TState, Result, TState, NotUsed] = 
  Flow[(Request, TState)]
    .map(x => (x._1, (x._1, x._2)))
    .via(partialResult1Flow)
    .map(x => (x._2._1, (x._2._1, x._1, x._2._2))
    .via(partialResult2Flow)
    .map(x => (Result(x._2._1, x._2._2, x._1), x._2._2))
    .asFlowWithContext((a: Request, b: TState) => (a,b))(_._2)
    .map(_._1)

这更好吗?

在这种情况下,情况可能更糟。在其他情况下,您几乎不需要更改上下文会更好。但是,无论哪种方式,我都建议使用它的内置方式,而不要依赖自定义的monad。

阶段2:通过使用

为了使它更易于使用,我为Flow和FlowWithContext创建了viaUsing扩展方法:

import akka.stream.{FlowShape, Graph}
import akka.stream.scaladsl.{Flow, FlowWithContext}

object FlowExtensions {
  implicit class FlowViaUsingOps[In, Out, Mat](val f: Flow[In, Out, Mat]) extends AnyVal {
    def viaUsing[Out2, Using, Mat2](func: Out => Using)(flow: Graph[FlowShape[(Using, Out), (Out2, Out)], Mat2]) : Flow[In, (Out2, Out), Mat] =
      f.map(x => (func(x), x)).via(flow)
  }

  implicit class FlowWithContextViaUsingOps[In, CtxIn, Out, CtxOut, Mat](val f: FlowWithContext[In, CtxIn, Out, CtxOut, Mat]) extends AnyVal {
    def viaUsing[Out2, Using, Mat2](func: Out => Using)(flow: Graph[FlowShape[(Using, (Out, CtxOut)), (Out2, (Out, CtxOut))], Mat2]):
    FlowWithContext[In, CtxIn, (Out2, Out), CtxOut, Mat] =
      f
        .asFlow
        .map(x => (func(x._1), (x._1, x._2)))
        .via(flow)
        .asFlowWithContext((a: In, b: CtxIn) => (a,b))(_._2._2)
        .map(x => (x._1, x._2._1))
  }
}

viaUsing的目的是从当前输出中为FlowWithContext创建输入,同时通过将其通过上下文来保留当前输出。结果是Flow,其输出是嵌套流和原始流的输出的元组。

使用viaUsing,我们的示例简化为:

  def flow[TState]: FlowWithContext[Request, TState, Result, TState, NotUsed] =
    FlowWithContext[Request, TState]
      .viaUsing(x => x)(partialResult1Flow)
      .viaUsing(x => x._2)(partialResult2Flow)
      .map(x => Result(x._2._2, x._2._1, x._1))

我认为这是一个重大改进。我已请求将viaUsing添加到Akka,而不是依赖扩展方法here