Akka Stream - 根据流量中的元素选择接收器

时间:2018-02-13 02:06:14

标签: scala akka-stream

我正在使用Akka流创建简单的邮件传递服务。该服务就像邮件传递一样,来自源的元素包括destinationcontent,如:

case class Message(destination: String, content: String)

并且服务应根据destination字段将消息传递到适当的接收器。我创建了一个DeliverySink类来让它有一个名字:

case class DeliverySink(name: String, sink: Sink[String, Future[Done]])

现在,我实例化了两个DeliverySink,让我称呼它们为sinkXsinkY,并根据其名称创建了一个地图。实际上,我想提供一个接收器名称列表,列表应该是可配置的。

我面临的挑战是如何根据destination字段动态选择合适的接收器。

最后,我想将Flow[Message]映射到接收器。我试过了:

val sinkNames: List[String] = List("sinkX", "sinkY")
val sinkMapping: Map[String, DeliverySink] = 
   sinkNames.map { name => name -> DeliverySink(name, ???)}.toMap
Flow[Message].map { msg => msg.content }.to(sinks(msg.destination).sink)

但是,显然这不起作用,因为我们无法在地图之外引用msg ...

我想这不是一个正确的方法。我还考虑过将filterbroadcast一起使用,但如果目标缩放到100,我就无法输入每个路由。什么是实现目标的正确方法?

[编辑]

理想情况下,我想让目的地充满活力。因此,我无法静态地键入过滤器或路由逻辑中的所有目标。如果尚未连接目标接收器,则它也应动态创建新接收器。

2 个答案:

答案 0 :(得分:3)

如果您必须使用多个接收器

Sink.combine会直接满足您现有的要求。如果您在每个Flow.filter之前附加适当的Sink,那么他们只会收到相应的消息。

不要使用多个接收器

总的来说,我认为让流的结构和内容包含业务逻辑是不好的设计。您的流应该是一个薄的贴面,用于在业务逻辑之上进行反压并发,这是普通的scala / java代码。

在这种特殊情况下,我认为最好将目标路由包装在单个Sink中,逻辑应该在单独的函数内部实现。例如:

val routeMessage : (Message) => Unit = 
  (message) => 
    if(message.destination equalsIgnoreCase "stdout")
      System.out println message.content
    else if(message.destination equalsIgnoreCase "stderr")
      System.err println message.content

val routeSink : Sink[Message, _] = Sink foreach routeMessage

注意现在测试我的routeMessage要容易得多,因为它不在流内:我不需要任何akka testkit“stuff”来测试routeMessage。如果我的并发设计要改变,我也可以将函数移动到FutureThread

多个目的地

如果您有许多目的地,可以使用Map。例如,假设您要将消息发送到AmazonSQS。您可以定义一个函数来将队列名称转换为队列URL,并使用该函数维护已创建名称的Map:

type QueueName = String

val nameToRequest : (QueueName) => CreateQueueRequest = ???  //implementation unimportant

type QueueURL = String

val nameToURL : (AmazonSQS) => (QueueName) => QueueURL = {
  val nameToURL = mutable.Map.empty[QueueName, QueueURL]

  (sqs) => (queueName) => nameToURL.get(queueName) match {
    case Some(url) => url
    case None => {
      sqs.createQueue(nameToRequest(queueName))
      val url = sqs.getQueueUrl(queueName).getQueueUrl()

      nameToURL put (queueName, url)

      url
    }
  }
}

现在你可以在一个单一的Sink中使用这个非流函数:

val sendMessage : (AmazonSQS) => (Message) => Unit = 
  (sqs) => (message) => 
    sqs sendMessage {
      (new SendMessageRequest())
        .withQueueUrl(nameToURL(sqs)(message.destination))
        .withMessageBody(message.content)
    }

val sqs : AmazonSQS = ???

val messageSink = Sink foreach sendMessage(sqs)

旁注

对于destination,您可能希望使用String以外的其他内容。 coproduct通常更好,因为它们可以与case语句一起使用,如果您错过了其中一种可能性,您将获得有用的编译器错误:

sealed trait Destination

object Out extends Destination
object Err extends Destination
object SomethingElse extends Destination

case class Message(destination: Destination, content: String)

//This function won't compile because SomethingElse doesn't have a case
val routeMessage : (Message) => Unit = 
  (message) => message.destination match {
    case Out =>
      System.out.println(message.content)
    case Err =>
      System.err.println(message.content)
  }

答案 1 :(得分:2)

根据您的要求,您可能想要考虑使用groubBy将流源多路复用到子流中:

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.stream.scaladsl._
import akka.util.ByteString
import akka.{NotUsed, Done}
import akka.stream.IOResult
import scala.concurrent.Future
import java.nio.file.Paths
import java.nio.file.StandardOpenOption._

implicit val system = ActorSystem("sys")
implicit val materializer = ActorMaterializer()
import system.dispatcher

case class Message(destination: String, content: String)
case class DeliverySink(name: String, sink: Sink[ByteString, Future[IOResult]])

val messageSource: Source[Message, NotUsed] = Source(List(
  Message("a", "uuu"), Message("a", "vvv"),
  Message("b", "xxx"), Message("b", "yyy"), Message("b", "zzz")
))

val sinkA = DeliverySink("sink-a", FileIO.toPath(
  Paths.get("/path/to/sink-a.txt"), options = Set(CREATE, WRITE)
))
val sinkB = DeliverySink("sink-b", FileIO.toPath(
  Paths.get("/path/to/sink-b.txt"), options = Set(CREATE, WRITE)
))

val sinkMapping: Map[String, DeliverySink] = Map("a" -> sinkA, "b" -> sinkB)

val totalDests = 2

messageSource.map(m => (m.destination, m)).
  groupBy(totalDests, _._1).
  fold(("", List.empty[Message])) {
    case ((_, list), (dest, msg)) => (dest, msg :: list)
  }.
  mapAsync(parallelism = totalDests) {
    case (dest: String, msgList: List[Message]) =>
      Source(msgList.reverse).map(_.content).map(ByteString(_)).
        runWith(sinkMapping(dest).sink)
  }.
  mergeSubstreams.
  runWith(Sink.ignore)