6.2.3 远程查询

现在我们不直接在 RestInterface actor 中创建 BoxOffice actor,我们在后台节点来查询它。 图 6.4 展示了我们要尝试和达到的:

Figure 6.4 对 Boxoffice actor 的远程查询

在上一版的代码中, RestInterface 直接创建子 BoxOffice actor:

val boxOffice = context.actorOf(Props[BoxOffice], "boxOffice")

这个调用使得 boxOffice 成为 RestInterface 的直接孩子(TODO:换个词)。 为使应用更灵活,且可以同时在单节点和客户服务器方式运行,我们把代码移到一个我们可以混入(mix in)的 trait 里。 trait 如列表 6.5 所示,以及我们对 RestInterface 代码所做的改变:

Listing 6.5 BoxOffice 的创建者

trait BoxOfficeCreator { this: Actor =>
  def createBoxOffice:ActorRef = {
    context.actorOf(Props[BoxOffice], "boxOffice")
    }
  }

class RestInterface extends HttpServiceActor with RestApi {
  def receive = runRoute(routes)
}

trait RestApi extends HttpService
          with ActorLogging
          with BoxOfficeCreator { actor: Actor =>
            val boxOffice = createBoxOffice
            // rest of the code of the RestApi ...
  • 这个 trait 需要混入进 Actors,这样才能使用 actor context。
  • createBoxOffice 方法创建了一个 BoxOffice actor,返回一个 ActorRef。
  • RestApi trait 包含 RestInterface 的所有逻辑。
  • BoxOfficeCreator 被混入了 RestApi trait。
  • BoxOffice 是使用 createBoxOffice 方法创建的。

我们现在把创建 BoxOffice 的代码分离到一个单独的 trait 中,令其成为 RestInterface 创建本地 boxOffice 的默认行为。 RemoteBoxOfficeCreator trait 会覆盖默认行为,细节稍后给出。 SingleNodeMain,FrontendMain 和 BackendMain 创建出来以单节点模式启动 app,或者分别启动 frontend 和 backend。 列表 6.6 展示了三个主类中有趣的代码(片段):

Listing 6.6 核心 actors 的亮点

//Snippet from SingleNodeMain
val system = ActorSystem("singlenode", config)
val restInterface = system.actorOf(Props[RestInterface],
                               "restInterface")
//Snippet from FrontendMain
val system = ActorSystem("frontend", config)
class FrontendRestInterface extends RestInterface
                        with RemoteBoxOfficeCreator
val restInterface = system.actorOf(Props[FrontendRestInterface],
                                "restInterface")
//Snippet from BackendMain
val system = ActorSystem("backend", config)
val config = ConfigFactory.load("backend")
val system = ActorSystem("backend", config)
system.actorOf(Props[BoxOffice], "boxOffice")
  • 像前边一样,创建一个 rest 接口
  • 将 RemoteBoxOfficeCreator 混入
  • 创建 rest 接口,混入了 RemoteBoxOfficeCreator
  • 在后台创建顶级的 boxOffice

所有的主类从一个特定的配置文件中加载其配置,SingleNodeMain,FrontendMain 和 BackendMain 分别加载文件 singlenode.conf,frontend.conf,和 backend.conf。 frontend.conf 文件有一个额外的配置区,用来查询 boxoffice actor。 RemoteBoxOfficeCreator 加载这些额外的配置属性:

backend {
    host = "0.0.0.0"
    port = 2552
    protocol = "akka.tcp"
    system = "backend"
    actor = "user/boxOffice"
}

到 boxoffice 的 actor 的路径是从这个配置区构建的。 可以在 REPL 控制台获取到远程 actor 的引用(TODO: Selection),当我们确定后台存在时,只需要试着发送一条消息。 在本例中,我们想使用 ActorRef,因为单节点模式使用了一个。 RemoteBoxOfficeCreator trait 如列表 6.7 所示:

Listing 6.7 用来创建远程 BoxOffice 的 Trait

object RemoteBoxOfficeCreator {
    val config = ConfigFactory.load("frontend").getConfig("backend")
    val host = config.getString("host")
    val port = config.getInt("port")
    val protocol = config.getString("protocol")
    val systemName = config.getString("system")
    val actorName = config.getString("actor")
}

trait RemoteBoxOfficeCreator extends BoxOfficeCreator { this:Actor =>
    import RemoteBoxOfficeCreator._
    def createPath:String = {
        s"$protocol://$systemName@$host:$port/$actorName"
    }

    override def createBoxOffice = {
        val path = createPath
        context.actorOf(Props(classOf[RemoteLookup],path),
            "lookupBoxOffice")
    }
}
  • 从 frontend.conf 配置中加载,获取 backend 配置区的特性构建路径
  • 创建到 boxoffice 的路径
  • 返回一个查询票房 actor 的 Actor,之后会介绍。Actor 使用一个参数来构建,即到远程 boxOffice 的路径。

RemoteBoxOfficeCreator 创建了一个单独的 RemoteLookup Actor 来查询票房。 在上一版的 akka 中你可以使用 actorFor 方法来直接获取到远程 actor 的 ActorRef。 这个方法现在过时了,因为如果相关的 actor 死了的话,返回的 ActorRef 与 本地的 ActorRef 的行为并不完全一致。 actorFor 返回的 ActorRef 能指向新产生的远程 actor 实例,在本地环境下这不会发生。 这是 Remote Actors 不能监视是否终止,就像本地的 actors 一样,这是另一个将这个方法标为过时的原因。

上述原因导致了我们使用 RemoteLookup actor 原因:

  • 后台 actor 系统也许还没启动,或者崩溃了,或者被重启了。
  • boxOffice actor 本身也可能崩溃或重启了。
  • 理想情况下,我们可以在前台启动之前启动后台,这样前台就可以一启动就查询。

RemoteLookup actor 会处理好这些场景。 图 6.5 展示了 RemoteLookup 居于 RestInterface 和 BoxOffice 之间。 它透明地把消息传递给 RestInterface。

Figure 6.5 RemoteLookup actor

RemoteLookup actor 是个状态机,只能处于我们定义的两种状态之一:识别和活动。 它利用 become 方法来将它的接收方法切换为 identify 或 active。 当在 identify 状态是,如果没有一个对 BoxOffice 的 ActorRef 引用时,RemoteLookup 试图获取一个有效的对 BoxOffice 的 ActorRef, 否则在 active 状态,将所有发送给有效的 ActorRef 的消息转发给 BoxOffice。 当一段时间没有收到消息时,如果 RemoteLookup 探测到 BoxOffice 终止了,它会试着获取再一个有效的 ActorRef。 对于这一点,我们使用 Remote Deathwatch。 听起来像新东西,但是从 API 使用的角度,它与普通的 actor 监视完全一样。 列表 6.8 展示了代码:

Listing 6.8 远程查询

import scala.concurrent.duration._

class RemoteLookup(path:String) extends Actor with ActorLogging {
    context.setReceiveTimeout(3 seconds)
    sendIdentifyRequest()

    def sendIdentifyRequest(): Unit = {
        val selection = context.actorSelection(path)
        selection ! Identify(path)
    }

    def receive = identify
    def identify: Receive = {
        case ActorIdentity(`path`, Some(actor)) =>
            context.setReceiveTimeout(Duration.Undefined)
            log.info("switching to active state")
            context.become(active(actor))
            context.watch(actor)

        case ActorIdentity(`path`, None) =>
            log.error(s"Remote actor with path $path is not available.")

        case ReceiveTimeout =>
            sendIdentifyRequest()

        case msg:Any =>
            log.error(s"Ignoring message $msg, not ready yet.")
    }

    def active(actor: ActorRef): Receive = {
        case Terminated(actorRef) =>
            log.info("Actor $actorRef terminated.")
            context.become(identify)
            log.info("switching to identify state")
            context.setReceiveTimeout(3 seconds)
            sendIdentifyRequest()

        case msg:Any => actor forward msg
    }
}
  • 如果三秒钟没有收到消息,发送 ReceiveTimeout 消息
  • 立即启动来获取 actor 的身份
  • 通过路径选择 actor
  • 发送认证信息给 actorSelection
  • actor 初始时处于 identity 接受状态
  • actor 被确认,返回一个 ActorRef
  • 如果 actor 没有得到消息,就不再发送 ReceiveTimeout,因为现在是 active 状态
  • 转变为 active 接收状态
  • 监视远程 actor 是否停止
  • actor 还不可以用,后台不能到达或没启动
  • 如果没收到消息,不断试图认证远程 actor
  • 在 identify 接收状态不发送消息
  • active 接收状态
  • 如果远程 actor 终止了,RemoteLookup actor 应该切换其行为到 identify 接收状态
  • 当远程 actor 活动时,转发所有消息

如你所看到的, 第三章中描述的 Death 监视 API 对本地还是远程的 actors 都完全一样。 简单地监视一个 ActorRef 会保证 actor 在被监视 actor 终止时会得到通知,无论被监视的是远程的,还是本地的。 Akka 使用了非常复杂地协议来从统计上探测一个节点是否可达。 我们在第十三章中更具体地来看看这个协议。 使用特殊的认证消息来获取对 boxOffice 的 ActorRef,消息发送给 ActorSelection。 后台 ActorSystem 的远程模块用 ActorIdentity 消息来响应,其中包含对远程 actor 的 ActorRef。

这些总结了我们需要对 goticks.com 所做的改变,来从单个节点转变为前台节点和后台节点。 除了可以远程通信,前台和后台可以分别启动,前台查询票房,当其可用时与其通信,不可用时采取其他行动。

你最后需要做的是,实际启动 FrontendMain 和 BackendMain 类。 我们启动两个终端,使用 sbt 来在项目中运行主类。 你会在终端中得到如下输出:

[info] ...
[info] ... (sbt messages)
[info] ...
Multiple main classes detected, select one to run:
[1] com.goticks.SingleNodeMain
[2] com.goticks.FrontendMain
[3] com.goticks.BackendMain
Enter number:

在一个终端中选择 FrontendMain,另一个中选择 BackendMain。 看看如果你杀死了运行 BackendMain 的 sbt 进程并重启,会发生什么。 你可以测试应用是否和以前一样可以与 httpit 一起工作,例如 http PUT localhost:8000/events event=RHCP nrOfTickets:=10 来创建一个活动,有10张票,再通过http GET localhost:5000/ticket/RHCP来获取活动的一张票。 如果你真滴杀死了后台进程,然后重启,你会在控制台上看到 RemoteLookup 类从 active 状态转换为 identify,再转换回来。 你还会注意到,Akka 报告了关于与其他节点的远程连接终端的错误。 如果你对远程生命周期活动的日志不感兴趣,你可以关闭日志,通过将如下内容添加到远程配置区:

remote {
    log-remote-lifecycle-events = off
}

默认远程生命周期活动是被记录的。 这使得当你启动远程模块,在 actor 路径语法上犯了个小错误时,很容易找出问题。 你可以使用关于频道的第十章中介绍的 actor 系统的 eventStream 来订阅远程生命周期活动。 既然远程 actors 可以像任何本地 actor 一样被监视,就没有必要因为连接管理的原因而单独处理这些活动。

来看看这些改变:

  • BoxOfficeCreator trait 从代码中抽取出来,加入一个远程版来查询后台的 BoxOffice。
  • RemoteBoxOfficeCreator 在 RestInterface 和 BoxOffice 之间加入了一个 RemoteLookup,它把所有收到的消息转发给 BoxOffice。它认证对 BoxOffice 的 ActorRef 并远程监视它。

如在本届开始时说的,Akka 提供了两种方式来获取远程 actor 的 ActorRef。 下一节我们来看看第二个选项,即远程部署。