假设我有一个比萨饼烤箱和一排我需要烤的比萨饼。我的烤箱一次只能烘烤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类型?
答案 0 :(得分:2)
此用例通常不能很好地映射到Akka Streams。在引擎盖下,Akka Stream是reactive stream;来自documentation:
Akka Streams实现使用Reactive Streams接口 在内部在不同的处理阶段之间传递数据。
您的披萨示例并不适用于流,因为您的某些外部事件与您的信息流的接收器一样具有广泛的需求。事实上,你公开表明&#34;第一个流程根本没有容量概念&#34;意味着您没有将流用于其预期目的。
总是可以使用一些奇怪的编码ju-jitsu来笨拙地弯曲流来解决并发问题,但是你可能很难将这些代码保持在线下。我建议你考虑使用Futures,Actors或普通的Threads作为你的并发机制。如果您的烤箱具有无限的容量来烹饪比萨饼,那么就不需要开始流。
我还会重新检查你的整个设计,因为你正在使用时钟时间作为需求的信号(即你的&#34;蛋计时器&#34;)。这通常表明工艺设计存在缺陷。如果您无法绕过此要求,那么您应该评估其他设计模式:
答案 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()