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

Scala Akka Streams:如何在2个相关流的系统中建模容量/速率限制?,scala,akka,akka-stream,Scala,Akka,Akka Stream,假设我有一个比萨烤箱和一系列需要烘烤的比萨。我的烤箱一次只能烤4个披萨,可以合理地预计,在一天的过程中,至少有4个在排队,所以烤箱需要尽可能经常满负荷工作 每次我把比萨饼放进烤箱,我都会在手机上设置一个计时器。一旦它熄灭,我就把比萨饼从烤箱里拿出来,给任何想要它的人,容量就变大了 我这里有两个来源,一个是等待烹调的比萨饼的队列,另一个是当比萨饼烹调好后,鸡蛋计时器就会响。系统中还有两个水槽,一个是煮熟的比萨饼的目的地,另一个是发送确认比萨饼已放入烤箱的地方 我现在非常天真地表达了这些,如下所示:

假设我有一个比萨烤箱和一系列需要烘烤的比萨。我的烤箱一次只能烤4个披萨,可以合理地预计,在一天的过程中,至少有4个在排队,所以烤箱需要尽可能经常满负荷工作

每次我把比萨饼放进烤箱,我都会在手机上设置一个计时器。一旦它熄灭,我就把比萨饼从烤箱里拿出来,给任何想要它的人,容量就变大了

我这里有两个来源,一个是等待烹调的比萨饼的队列,另一个是当比萨饼烹调好后,鸡蛋计时器就会响。系统中还有两个水槽,一个是煮熟的比萨饼的目的地,另一个是发送确认比萨饼已放入烤箱的地方

我现在非常天真地表达了这些,如下所示:

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
源。它们通过进入比萨饼队列的事件压缩,这意味着如果没有可用的事件,则不会启动比萨饼处理,因为压缩操作将等待它们

然后,一旦比萨饼做好,一个电容可用的对象被推回池中


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

此用例通常不会很好地映射到Akka流。引擎盖下有一条阿克卡河;从:

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

您的pizza示例不适用于流,因为您有一些外部事件,它们与流的接收器一样是需求的广播者。您公开声明“第一个流根本没有容量概念”这一事实意味着您没有将流用于其预期目的

总是可以使用一些奇怪的编码ju-jitsu来笨拙地弯曲流以解决并发问题,但是您可能会在维护代码时遇到困难。我建议您考虑使用期货、演员或普通老式线程作为并发机制。如果你的烤箱有无限的容量容纳烹饪比萨饼,那么就没有必要从溪流开始

我也会重新检查你的整个设计,因为你使用时钟时间的流逝作为需求的信号(即你的“鸡蛋计时器”)。这通常表明工艺设计中存在缺陷。如果您无法绕过此要求,则应评估其他设计模式:


  • 您可以使用
    mapsyncUnordered
    stage和
    parallelism=4
    来表示烤箱。
    未来的完成可以通过计时器()完成,也可以因为其他原因决定将其从烤箱中取出。

    这就是我最后使用的。在这个问题上,它几乎是一个伪状态机的精确实现。
    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()
    

    你是想实施节流吗?在某种意义上,我想是的。但不是通过时间或速率限制进行节流,而是通过应用程序级业务逻辑进行节流。节流是否会根据我们正在处理的项目而变化,或者是您愿意动态更改的每个流的全局变量?这是任意时间量。基本上,每个“比萨饼”都是一个任务队列中的一个任务,它将被执行,可能需要0到无限秒的时间,但只能同时执行4次。好的,我喜欢你的一般想法。在创建我的初始图表之后,我还意识到我实际上已经对有限状态机进行了建模,因此我将研究akka utils,因为它可能更合适。时钟只是一个例子,现在发生的是,我分派一个任务(一个“比萨饼”),并从另一个无关的来源听到它的完成情况。“我并没有在我的应用程序中真正使用计时器。”杰克古怪我正要说和拉蒙一样的话。当我今天早上收到你的评论时,我离开了,你要么需要使用
    mapsync
    ,要么使用Futures/actors实现它。作为练习,我还尝试使用Akka FSM作为有限状态机重写,并使用工作拉动模式。