如何建立一个简单的akka​​演员系统?

时间:2013-09-17 15:30:27

标签: scala akka

这是一个关于如何模拟akka演员的更一般的设计问题。情况就一个非常简单的例子进行了解释,我想对可能性和方法及其优缺点进行更全面的回答。

有一个用户对象可以用来做某事。让我们说:doSomethingWithUser(user: User)。比方说,用户有一个属性avatar: Option[String],这是一个网址。如果存在,则应在运行doSomethingWithUser方法之前抓取网址后面的实际图像。在谈论Akka,我会创建一个可以收到两条消息的演员DoSomethingWithUserActor

case class NewUser(user: User)
case class NewUserWithImage(user: User, imageData: Array[Byte])

抓取图像数据实现为可以处理一条消息的Actor FetchImageActor

case class FetchImage(url: String)

并生成一条消息:

case class GotImage(imageData: Array[Byte])

MainActor是根演员,只接收一条消息NewUser,其处理方式如下:

def receive {
  case newUser: NewUser => {
    newUser.avatar match {
      case Some(avatar) => {
        // here I would like to send a message to the FetchImageActor, 
        // wait for the response (GotImage) and once it's there send a
        // NewUserWithImage message to the DoSomethingWithUser actor.
        //
        // How can this be done?
        // Is it a good idea to use a Future here, and if so, how can this
        // be done?
        //
        // pseudocode:
        val gotImage: GotImage = // get it somehow
        doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
      }
      case _ => doSomethingWithUserActor forward NewUser(newUser.user)
  }
}

DoSomethingWithUserActor处理邮件NewUserNewUserWithImage。也许是这样的:

def receive {
  case newUser: NewUser => doSomethingWithUser(newUser.user)
  case newUserWithImage: NewUserWithImage => {
    doSomethingWithImage(newUserWithImage.imageData)
    doSomethingWithUser(newUser.user)
  }
}

private def doSomethingWithUser(user: User) = { ... }
private def doSomethingWithImage(imageData: Array[Byte]) = { ... }

首先我不知道,如果用户有一个头像,如何进行异步调用,其次,我不知道这是否一般是以这种方式处理这个问题的好方法。

另一种方法可能是将NewUser消息转发到FetchImageActor。然后,该actor检查用户是否具有avatar属性集,如果是,则获取图像并将NewUserWithImage消息发送回MainActor,MainActor将此消息转发给DoSomethingWithUserActor,然后DoSomethingWithUserActor实际上对包含的用户对象和图像执行某些操作数据。我觉得这很糟糕,因为FetchImageActor需要有关用户的知识,但它只是用于获取图像。这是两个不同的独立方面,不应混合在一起。在这种情况下,FetchImage消息对象也需要用户属性(我不喜欢如前所述)。

解决这个问题的正确或“好”策略是什么?

2 个答案:

答案 0 :(得分:1)

  1. 如果您需要互连演员,您可能会发现SynapseGrid库很有用。

  2. 对于异步调用Future是常用方法。

    val gotImageFuture = new Future { fetchImageActor ? FetchImage(avatar) }
    gotImageFuture.onSuccess( (gotImage: GotImage) =>
      doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
    )
    
  3. 如果您添加相关令牌(User或至少url)以获取消息:

    GotImage(user:User, imageData: Array[Byte])
    FetchImage(url:String, user:User)
    
    那么你可以简单地使用fire-forget。主要演员只需处理GotImage

    ...
        case GotImage(user, imageData) => 
          doSomethingWithUserActor ! NewUserWithImage(user, imageData)
    

    这种方法是畅通无阻的。

  4. P.S。一个小建议:

    case class NewUser(user: User, imageData:Option[Array[Byte]])
    

    可以使处理更容易。

答案 1 :(得分:1)

对您的用例非常有用的一种模式是使用ask-pipeTo。

使用ask模式获取请求的未来,然后(可选)对结果进行一些转换(下面的示例结合了几个响应,您也可以在将来调用map)并使用pipeTo模式将未来的结果发送给不同的演员。这与Arseniy在第2点中的建议非常相似,实际上pipeTo在未来使用onSuccess进行注册。

以下是优秀akka docs

的示例
import akka.pattern.{ ask, pipe }
import system.dispatcher // The ExecutionContext that will be used
case class Result(x: Int, s: String, d: Double)
case object Request

implicit val timeout = Timeout(5 seconds) // needed for `?` below

val f: Future[Result] =
  for {
    x ← ask(actorA, Request).mapTo[Int] // call pattern directly
    s ← (actorB ask Request).mapTo[String] // call by implicit conversion
    d ← (actorC ? Request).mapTo[Double] // call by symbolic name
  } yield Result(x, s, d)

f pipeTo actorD // .. or ..
pipe(f) to actorD

我喜欢在这种情况下使用的另一种模式是使用临时演员。请记住,actor与线程不同,它们非常轻量级,创建它们不会带来太多开销。

你可以这样做:

val tempActor = context.actorOf(Props(classOf[DoSomethingWithUserActor], newUser.user)
fetchImageActor.tell(FetchImage(image), tempActor)

这样,DoSomethingActor已经有了对用户的引用,并且通过将其设置为发送者,FetchImageActor可以只回复该消息,并且您的临时actor将接收该图像。稍微使用这个想法,我发现它在正确使用时非常强大。