我正在使用Akka流创建简单的邮件传递服务。该服务就像邮件传递一样,来自源的元素包括destination
和content
,如:
case class Message(destination: String, content: String)
并且服务应根据destination
字段将消息传递到适当的接收器。我创建了一个DeliverySink
类来让它有一个名字:
case class DeliverySink(name: String, sink: Sink[String, Future[Done]])
现在,我实例化了两个DeliverySink
,让我称呼它们为sinkX
和sinkY
,并根据其名称创建了一个地图。实际上,我想提供一个接收器名称列表,列表应该是可配置的。
我面临的挑战是如何根据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
...
我想这不是一个正确的方法。我还考虑过将filter
与broadcast
一起使用,但如果目标缩放到100,我就无法输入每个路由。什么是实现目标的正确方法?
的 [编辑]
理想情况下,我想让目的地充满活力。因此,我无法静态地键入过滤器或路由逻辑中的所有目标。如果尚未连接目标接收器,则它也应动态创建新接收器。
答案 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。如果我的并发设计要改变,我也可以将函数移动到Future
或Thread
。
多个目的地
如果您有许多目的地,可以使用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)