Akka Streams:如何在2个相关流的系统中建模容量/速率限制?

时间:2016-10-31 00:14:02

标签: scala akka akka-stream

假设我有一个比萨饼烤箱和一排我需要烤的比萨饼。我的烤箱一次只能烘烤4个比萨饼,并且有理由期待在一天中总是至少有4个比萨饼,所以烤箱需要尽可能多地满负荷。< / p>

每次我把烤披萨放入烤箱时,我都会在手机上设置一个计时器。一旦发生这种情况,我就把披萨从烤箱中拿出来,送给任何想要它的人,并且能够提供容量。

我这里有2个来源,一个是要煮熟的比萨饼的队列,另一个是比萨饼煮熟时结束的鸡蛋计时器。系统中还有2个水槽,一个是熟披萨的目的地,另一个是确认披萨已经放入烤箱的地方。

我目前非常天真地代表这些,如下:

Source.fromIterator(() => pizzas)
    .map(putInOven) // puts in oven and sets a timer
    .runWith(Sink.actorRef(confirmationDest, EndSignal))

Source.fromIterator(() => timerAlerts)
    .map(removePizza)
    .runWith(Sink.actorRef(pizzaDest, EndSignal))

但是,这两个流目前完全相互独立。 eggTimer正常运行,无论何时收集披萨。但它无法向先前的流量发出信号,表明容量已经可用。事实上,第一个流程根本没有容量概念,只要他们加入生产线就会尝试将比萨饼塞进烤箱。

可以使用什么Akka概念来组合这些流,这样第一种方法只有在容量时从队列中取出比萨饼,并且第二种流可以“警告”第一种流量时披萨的容量变化从烤箱中取出。

我最初的印象是实现这样的流程图:

   ┌─────────────┐                                                          
┌─>│CapacityAvail│>──┐                                                      
│  └─────────────┘   │   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  ┌─────────────┐   ├──>│     Zip     │>─>│  PutInOven  │>─>│   Confirm   │
│  │    Queue    │>──┘   └─────────────┘   └─────────────┘   └─────────────┘
│  └─────────────┘                                                          
│  ┌─────────────┐       ┌─────────────┐                                    
│  │    Done     │>─────>│  SendPizza  │                                    
│  └─────────────┘       └─────────────┘                                    
│         v                                                                 
│         │                                                                 
└─────────┘                    

支持这一点的原则是,有一定数量的CapacityAvailable对象填充CapacityAvail源。它们被拉上了披萨队列中的事件,这意味着如果没有可用的话,就不会开始披萨处理,因为zip操作会等待它们。

然后,一旦完成披萨,就会将CapacityAvailable对象推回到池中。

我看到这个实现的主要障碍是我不确定如何为CapacityAvail源创建和填充池,我也不确定Source是否也可以是一个接收器。是否有适合此实现的Source / Sink / Flow类型?

3 个答案:

答案 0 :(得分:2)

此用例通常不能很好地映射到Akka Streams。在引擎盖下,Akka Stream是reactive stream;来自documentation

  

Akka Streams实现使用Reactive Streams接口   在内部在不同的处理阶段之间传递数据。

您的披萨示例并不适用于流,因为您的某些外部事件与您的信息流的接收器一样具有广泛的需求。事实上,你公开表明&#34;第一个流程根本没有容量概念&#34;意味着您没有将流用于其预期目的。

总是可以使用一些奇怪的编码ju-jitsu来笨拙地弯曲流来解决并发问题,但是你可能很难将这些代码保持在线下。我建议你考虑使用Futures,Actors或普通的Threads作为你的并发机制。如果您的烤箱具有无限的容量来烹饪比萨饼,那么就不需要开始流。

我还会重新检查你的整个设计,因为你正在使用时钟时间作为需求的信号(即你的&#34;蛋计时器&#34;)。这通常表明工艺设计存在缺陷。如果您无法绕过此要求,那么您应该评估其他设计模式:

  1. Periodic Message Scheduling
  2. Non Thread Block Timeouts

答案 1 :(得分:1)

您可以使用mapAsyncUnordered parallelism=4阶段代表烤箱。完成Future可以来自计时器(http://doc.akka.io/docs/akka/2.4/scala/futures.html#After),或者您决定将其从烤箱中取出以用于其他原因。

答案 2 :(得分:1)

这是我最终使用的。它几乎是问题中人造机器的精确实现。 Source.queue的机制比我希望的更笨拙,但它却非常干净。真正的接收器和源是作为参数提供的,并且是在其他地方构建的,因此实际实现的模板少于此。

RunnableGraph.fromGraph(GraphDSL.create() {
  implicit builder: GraphDSL.Builder[NotUsed] =>
    import GraphDSL.Implicits._

    // Our Capacity Bucket. Can be refilled by passing CapacityAvaiable objects 
    // into capacitySrc. Can be consumed by using capacity as a Source.
    val (capacity, capacitySrc) =
      peekMatValue(Source.queue[CapacityAvailable.type](CONCURRENT_CAPACITY,
                                                        OverflowStrategy.fail))

    // Set initial capacity
    capacitySrc.foreach(c =>
      Seq.fill(CONCURRENT_CAPACITY)(CapacityAvailable).foreach(c.offer))


    // Pull pizzas from the RabbitMQ queue
    val cookQ = RabbitSource(rabbitControl, channel(qos = CONCURRENT_CAPACITY),
                             consume(queue("pizzas-to-cook")), body(as[TaskRun]))

    // Take the blocking events stream and turn into a source
    // (Blocking in a separate dispatcher)
    val cookEventsQ = Source.fromIterator(() => oven.events().asScala)
        .withAttributes(ActorAttributes.dispatcher("blocking-dispatcher"))

    // Split the events stream into two sources so 2 flows can be attached
    val bc = builder.add(Broadcast[PizzaEvent](2))

    // Zip pizzas with the capacity pool. Stops cooking pizzas when oven full.
    // When cooking starts, send the confirmation back to rabbitMQ
    cookQ.zip(AckedSource(capacity)).map(_._1)
      .mapAsync(CONCURRENT_CAPACITY)(pizzaOven.cook)
      .map(Message.queue(_, "pizzas-started-cooking"))
      .acked ~> Sink.actorRef(rabbitControl, HostDied)

    // Send the cook events stream into two flows
    cookEventsQ ~> bc.in

    // The first tops up the capacity pool
    bc.out(0)
      .mapAsync(CONCURRENT_CAPACITY)(e =>
         capacitySrc.flatMap(cs => cs.offer(CapacityAvailable))
      ) ~> Sink.ignore

    // The second sends out cooked events
    bc.out(1)
      .map(p => Message.queue(Cooked(p.id()), "pizzas-cooked")
    ) ~> Sink.actorRef(rabbitControl, HostDied)

    ClosedShape
}).run()