Richard Imaoka's Blog

2017年より職業Scalaプログラマになった、リチャード・伊真岡のブログです。

Typesafe Akka Remote Sampleの図解 - 2/2 LookupApplication編

TypeSafeのAkka Remote Samples with Scalaに含まれる2つ目のサンプルアプリケーション

前回の記事に引き続き、何かとわかりにくいTypeSafe社の@TypeSafeのAkka Remoteのサンプルについて、図解していきたいと思います。

サンプルの中には2つのアプリケーションが含まれていて、この記事はその2つ目、LookupApplicationについてです

メッセージの型としてのcase class MathOpは前回の記事でも解説した通りです

f:id:richard-imaoka:20151105024105p:plain

// sample/remote/calculator/MathOp.scala
trait MathOp
final case class Add(nbr1: Int, nbr2: Int) extends MathOp
final case class Subtract(nbr1: Int, nbr2: Int) extends MathOp
final case class Multiply(nbr1: Int, nbr2: Int) extends MathOp
final case class Divide(nbr1: Double, nbr2: Int) extends MathOp

Akkaでよく使われるcase classをメッセージの型として使う方法です。足し、引き、掛け、割り算に相当する以上の4つが定義されています。

MathResultも同様に前回の記事の通りです

f:id:richard-imaoka:20151105024107p:plain

// sample/remote/calculator/MathOp.scala
trait MathResult
final case class AddResult(nbr: Int, nbr2: Int, result: Int) extends MathResult
final case class SubtractResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class MultiplicationResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class DivisionResult(nbr1: Double, nbr2: Int, result: Double) extends MathResult

それぞれに対する結果型も用意されています。以下で見るようにActorはこれらの型のメッセージをやり取りして、計算の入力と結果を受け渡します。

CalculatorActorは計算入力を受け取って結果を返す、LookupActorはRemoteで生成されたCalculatorActorを探して、監視したうえで、計算を行わせます

f:id:richard-imaoka:20151105024114p:plain

// sample/remote/calculator/calculatorActor.scala
class CalculatorActor extends Actor {
  def receive = {
    case Add(n1, n2) =>
      println("Calculating %d + %d".format(n1, n2))
      sender() ! AddResult(n1, n2, n1 + n2)
    case Subtract(n1, n2) =>
      println("Calculating %d - %d".format(n1, n2))
      sender() ! SubtractResult(n1, n2, n1 - n2)
    case Multiply(n1, n2) =>
      println("Calculating %d * %d".format(n1, n2))
      sender() ! MultiplicationResult(n1, n2, n1 * n2)
    case Divide(n1, n2) =>
      println("Calculating %.0f / %d".format(n1, n2))
      sender() ! DivisionResult(n1, n2, n1 / n2)
  }
}

こちらも前回の記事で解説した通りです。

例えばMultiply型のメッセージを受け取ったときは、その結果であるMultiplicationResult型のメッセージを送信元"sender"に投げ返します。

LookupActorは(LookupActorから見て)RemoteにあるCalculatorActorを探しに行きます

f:id:richard-imaoka:20151106020837p:plain

今まで出てきたActorに比べてLookupActorはやや複雑です

f:id:richard-imaoka:20151106020802p:plain

//sample/remote/calculator/LookupActor.scala
class LookupActor(path: String) extends Actor { ... }

まず、上記のConstructionの部分を見ましょう。path変数には

  • path = "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

が入ってきます。

LookupActorは最初に呼び出される関数sendIdentifyRequest()はActorSelectionに対して、Identifyメッセージを送っています

f:id:richard-imaoka:20151106020816p:plain

Identifyについては後述しますので、まずはActorSelectionについて。

ActorSelectionは、上記の"akka.tcp://..."のようなパス(URL)に対して!メソッドでメッセージを送ることができます。

f:id:richard-imaoka:20151106020826p:plain

つまり、Akkaでは

  • ActorRef
  • ActorSelection

の2つに対して!メソッドでメッセージが送れることになります。

Akkaに備わっているのIdentify, ActorIdentityメッセージ型は、ActorSelection宛てにメッセージを送ったときにActorRefを得ることができます

f:id:richard-imaoka:20151106020843p:plain

次に、Identify, ActorIdentityは、Akkaに備わっているメッセージ型です。

AkkaのActorはIdentifyを受け取ると、ActorIdentityをsender()に返します。その際、ActorIdentityはActorRefを第2引数にもっています。

//akka.actor.ActorIdentity
 case class ActorIdentity(correlationId: Any, ref: Option[ActorRef]) 

LookupActorではその第2引数として返ってきたActorRefを使って、context.watch()しています。

f:id:richard-imaoka:20151106023558p:plain

context.watch()すると、別のActorを監視することができる、すなわち監視対象のActorがStopすると、Terminatedメッセージを受け取ることになります。

f:id:richard-imaoka:20151106020918p:plain

    case Terminated(`actor`) =>
      println("Calculator terminated")
      sendIdentifyRequest()
      context.become(identifying)

LookupActorではこのあともう一度sendIdentifyRequest()を呼んでいるので、TerminatedになったCalculatorActorの代わりのCalculatorActor(別インスタンス)が同じパス

  • "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

上にあれば、再びCalculatorActorを監視に入れることになります。

context.become(active(actor))によって、receiveメソッドの実装はactiveメソッドに切り替わります

f:id:richard-imaoka:20151106020857p:plain

無事CalculatorActorの監視に成功したら、次は

      context.become(active(actor))

によって、receiveメソッドの動作をactive()メソッドに入れ替えます。

  def active(actor: ActorRef): Actor.Receive = {
    case op: MathOp => actor ! op
    case result: MathResult => result match {
      case AddResult(n1, n2, r) =>
        printf("Add result: %d + %d = %d\n", n1, n2, r)
      case SubtractResult(n1, n2, r) =>
        printf("Sub result: %d - %d = %d\n", n1, n2, r)
    }
    case Terminated(`actor`) =>
      println("Calculator terminated")
      sendIdentifyRequest()
      context.become(identifying)
    case ReceiveTimeout =>
    // ignore

これは、

  • MathOpを受け取ればCalculatorActor (actor) に転送
  • MatuResultを受け取ればprintf表示
  • Terminatedを(優位つの監視対象である)CalculatorActorから受け取れば、もう一度でsendIdentifyRequest()監視

f:id:richard-imaoka:20151106023558p:plain

という動作をします。

LokupApplication

最後にアプリケーションの説明です。これも前回の記事同様、main関数はややこしいのですが…、とにかくstartRemoteCalculatorSystem()とstartRemoteLookupSystem()という二つの関数を走らせるだけです。

コマンドライン引数」のCalculatorとLookupを渡すと、2つの関数をの別のプロセスで走らせることができます。

sbt "runMain sample.remote.calculator.CreationApplication Calculator"
sbt "runMain sample.remote.calculator.CreationApplication Lookup"

args.isEmpty、すなわちコマンドライン引数を渡さないと、一つのプロセスの中で2つの関数を走らせます。s

object LookupApplication {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty || args.head == "Calculator")
      startRemoteCalculatorSystem()
    if (args.isEmpty || args.head == "Lookup")
      startRemoteLookupSystem()
  }

  def startRemoteCalculatorSystem(): Unit = {
    ...
  }

  def startRemoteLookupSystem(): Unit = {
    ...
  }
}

startRemoteCalculatorSystem()はActorSystemを初期化したうえで、そこからCalculatorActorも生成してしまいます。

これにより、CalculatorActorは後述のLookupActorから見てRemoteになります。

startRemoteLookupSystem()は,CalculatorActorのパス

  • "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

をLookupActorに渡して、後はLookupActor経由でどんどんAddとSubtractメッセージを投げ続けるだけです。