简体   繁体   English

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

[英]Akka Stream - Select Sink based on Element in Flow

I'm creating a simple message delivery service using Akka stream. 我正在使用Akka流创建一个简单的消息传递服务。 The service is just like mail delivery, where elements from source include destination and content like: 该服务就像邮件传递一样,来自源的元素包括destinationcontent如:

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

and the service should deliver the messages to appropriate sink based on the destination field. 并且服务应该根据destination字段将消息传递到适当的接收器。 I created a DeliverySink class to let it have a name: 我创建了一个DeliverySink类,让它有一个名字:

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

Now, I instantiated two DeliverySink , let me call them sinkX and sinkY , and created a map based on their name. 现在,我实例化了两个DeliverySink ,让我称之为sinkXsinkY ,并根据其名称创建一个地图。 In practice, I want to provide a list of sink names and the list should be configurable. 实际上,我想提供一个接收器名称列表,该列表应该是可配置的。

The challenge I'm facing is how to dynamically choose an appropriate sink based on the destination field. 我面临的挑战是如何根据destination字段动态选择合适的接收器。

Eventually, I want to map Flow[Message] to a sink. 最后,我想将Flow[Message]映射到接收器。 I tried: 我试过了:

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)

but, obviously this doesn't work because we can't reference msg outside of map... 但是,显然这不起作用,因为我们不能在地图之外引用msg ...

I guess this is not a right approach. 我想这不是一个正确的方法。 I also thought about using filter with broadcast , but if the destination scales to 100, I cannot type every routing. 我还考虑过使用带broadcast filter ,但如果目标扩展到100,我就无法输入每个路由。 What is a right way to achieve my goal? 什么是实现目标的正确方法?

[Edit] [编辑]

Ideally, I would like to make destinations dynamic. 理想情况下,我想让目的地充满活力。 So, I cannot statically type all destinations in filter or routing logic. 因此,我无法静态地键入过滤器或路由逻辑中的所有目标。 If a destination sink has not been connected, it should create a new sink dynamically too. 如果尚未连接目标接收器,则它也应动态创建新接收器。

If You Have To Use Multiple Sinks 如果您必须使用多个接收器

Sink.combine would directly suite your existing requirements. Sink.combine将直接满足您现有的要求。 If you attach an appropriate Flow.filter before each Sink then they'll only receive the appropriate messages. 如果在每个Sink Flow.filter之前附加适当的Flow.filter ,那么它们只会收到相应的消息。

Don't Use Multiple Sinks 不要使用多个接收器

In general I think it is bad design to have the structure, and content, of streams contain business logic. 总的来说,我认为让流的结构和内容包含业务逻辑是不好的设计。 Your stream should be a thin veneer for back-pressured concurrency on top of business logic which is in ordinary scala/java code. 您的流应该是一个薄的贴面,用于在业务逻辑之上进行反压并发,这在普通的scala / java代码中。

In this particular case, I think it would be best to wrap your destination routing inside of a single Sink and the logic should be implemented inside of a separate function. 在这种特殊情况下,我认为最好将目标路由包装在单个Sink中,逻辑应该在单独的函数内部实现。 For example: 例如:

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

Note how much easier it is to now test my routeMessage since it isn't inside of the stream: I don't need any akka testkit "stuff" to test routeMessage. 注意现在测试我的routeMessage是多么容易,因为它不在流内:我不需要任何akka testkit“stuff”来测试routeMessage。 I can also move the function to a Future or a Thread if my concurrency design were to change. 如果我的并发设计要改变,我也可以将函数移动到FutureThread

Many Destinations 许多目的地

If you have many destinations you can use a Map . 如果您有许多目的地,则可以使用Map Suppose, for example, you are sending your messages to AmazonSQS. 例如,假设您要将消息发送到AmazonSQS。 You could define a function to convert a Queue Name to Queue URL and use that function to maintain a Map of already created names: 您可以定义一个函数来将队列名称转换为队列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
    }
  }
}

Now you can use this non-stream function inside of a singular Sink: 现在,您可以在单个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)

Side Note 边注

For destination you probably want to use something other than String . 对于destination您可能希望使用除String其他内容。 A coproduct is usually better because they can be used with case statements and you'll get helpful compiler errors if you miss one of the possibilities: 副产品通常更好,因为它们可以与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)
  }

Given your requirement, maybe you want to consider multiplexing your stream source into substreams using groubBy : 根据您的要求,您可能希望考虑使用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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM