Scala
Akka

Akka実践バイブルをゆっくり読み解く 第11章有限状態マシンとエージェント

Akka実践バイブルをゆっくり読み解く企画の第11章です。

第11章 有限状態マシンとエージェント

8章で登場したアグリゲータや9章で登場したbecome/unbecomeでもアクターの状態を表現することは可能である。さらに、有限状態マシンを用いることでもアクターの状態を扱うことが可能となるので、それを見ていく。

有限状態マシン

有限状態マシンとは?

ウイキペディアでは、

有限個の状態と遷移と動作の組み合わせからなる数学的に抽象化された「ふるまいのモデル」

とか

ある一連の状態をとったときどのように論理が流れるかを調べることができる。

とか説明されている。つまり、対象の持つ「状態」とその状態の「遷移」を表現するためのモデルを指す。
Akkaでは有限状態マシンを扱うための)FSMトレイトが用意されている。

FSMの何が嬉しいのか?

become/unbecomeでも有限状態マシンを実現することはできる。しかし、状態の数が多くなってくると保守が困難となる。

become/unbecomeの例
class StatePattern() extends Actor {

  def state01: Receive = {
    // 状態パターン1 
  }

  def state02: Receive = {
    // 状態パターン2 
  }

  def state99: Receive = {
    // 状態パターン99 
  }

  // receiveは99個のパターン存在しうるが、
  // state01〜state99が色んなところに散らばると、
  // receiveがどれだけのパターンを取りうるのかを把握するのが困難となる
  def receive = {
    case msg: AnyRef => state01(msg)
  }
}

FSMトレイトを用いると、保守性を保ったまま有限状態マシンを扱うことができる。

有限状態マシンモデルの実装

有限状態マシンは、以下のような構成を取る。

有限状態マシンの基本構成
class 有限状態マシンクラス() extends Actor with FSM[状態, 状態値] {

  // 初期状態の定義
  startWith()

  // 状態遷移の定義
  when(状態) {}

  // 開始アクションの定義
  onTransition {}

  // 有限状態マシンを初期化して起動する
  initialize
}

状態の定義

Stateトレイトを準備し、それを実装した状態を作成する。ここで作成した状態が有限状態マシンが取りうる状態のパターンとなる。

状態の定義
sealed trait State
case object State01 extends State
case object State02 extends State
...

状態値の定義

状態の詳細な情報を格納するためのStateDataクラスを作成する。

状態値の定義
case class StateData(stateValue: Int)

有限状態マシンの定義

状態と状態値を用いて、有限状態マシンを定義する。

有限状態マシンの定義
// FSMは状態と状態値をとる
// State:状態
// StateData:状態値
class StateFlow() extends Actor with FSM[State, StateData] {
}

初期状態の定義

有限状態マシンの初期状態を定義する。初期状態を定義するとともに、状態値の初期値もあわせて定義する。

初期状態の定義
class StateFlow() extends Actor with FSM[State, StateData] {
  // 開始時状態:State01
  // 状態値の初期値:stateValue=0
  startWith(State01, new StateData(0))
}

イベントの定義

状態遷移を発生させるトリガーとなるイベントを定義する。

イベントの定義
case class GoodRequest
case class BadRequest

状態遷移の実装

各状態ごとに、次の状態への状態遷移を定義する。

初期状態の定義
class StateFlow() extends Actor with FSM[State, StateData] {

  // State01の時の状態遷移を宣言
  when(State01) {
    // GoodRequestメッセージを受信した時の状態遷移
    case Event(request: GoodRequest, data: StateData) => {
      // 状態値を更新する
      val newStateData = data.copy(stateValue = stateValue + 1)
      // 次の状態に遷移する
      goto(State02) using newStateData
    }
    // BadRequestメッセージを受信した時の状態遷移
    case Event(request: BadRequest, data: StateData) => {
      // 状態遷移しない
      stay
    }
  }
  // State02の時の状態遷移を宣言
  when(State02) {
    // GoodRequestに対する状態遷移のみを定義
    case Event(request: GoodRequest, data: StateData) => {
      // 次の状態に遷移する
      goto(State03) using newStateData
    }
  }
  // 個々の状態で定義されていないメッセージについては、whenUnhandledで処理される
  whenUnhandled {
    // BadRequestメッセージ受信時の状態遷移を定義する
    case Event(request: BadRequest, data: StateData) => {
      stay 
    }
    case Event(e, s) => {
      // イベントが処理されなかった場合は状態値をリセット
      stay using new StateData(0)
    }
  }
}

状態遷移時のアクション

状態遷移時の開始アクションをonTransactionで定義することができる。開始アクションが存在しない状態については、宣言を省略することができる。

開始アクションの定義
class StateFlow() extends Actor with FSM[State, StateData] {

  onTransition {
    // State01の開始アクション
    case _ -> State01 => {
      self ! GoodRequest
    }
    // State02の開始アクション
    case _ -> State02 => {
      if (0 < nextStateData.stateValue) {
        // 次の状態にメッセージを送信
        self ! GoodRequest
      }
    }
  }
}

タイマーによる状態遷移

タイマーを開始アクションに指定することもできる。状態が遷移してから指定の時間が経過すると状態遷移が実行される。
また、遷移時に遷移先でのタイマーを設定することもできる。

初期状態の定義
class StateFlow() extends Actor with FSM[State, StateData] {

  // State99の時の状態遷移を宣言
  when(State99, stateTimeout = 5 seconds) {
    // 5秒経過で状態遷移する
    // 遷移先では3秒でSteteTimeoutが発生する
    case Event(SteteTimeout, _) => goto(State01) using new StateData(0) forMax (3 seconds)
  }
}

有限状態マシンの終了

有限状態マシンのアクターが終了する際にクリーンアップが必要となるケースがある。その場合、onTerminationを使用する。

有限状態マシンの終了時処理の定義
class StateFlow() extends Actor with FSM[State, StateData] {

  // 終了時処理の定義
  onTermination {
    case StopEvent(FSM.Normal, state, data) =>        // 正常終了時の処理
    case StopEvent(FSM.Shutdown, state, data) =>      // シャットダウンによる終了時の処理
    case StopEvent(FSM.Failure(cause, state, data) => // 障害が原因による終了時の処理
  }
}

状態の共有

エージェントを使うことにより、異なるアクター間で状態を共有することもできる。エージェントを使えば、オブジェクトのロックを気にする必要もない。。。はずだったんだけど、Akka 2.5.1からエージェントの利用が非推奨となり、Akka 2.6.xでは削除される予定らしい(2018/06時点の最新は、2.5.13)。代わりにAkka Typedを使えってことになるみたい。ところで、Akka Typedって正式リリースされたんだっけ・・・?
よって、当項目は割愛!

個人的まとめ

普段ゴリゴリの業務アプリケーションを作ることが多いので、状態遷移の制御はとても気を遣うところ。そして、気を遣ってもわけのわからんバグが発生することろ・・・自分が作ったものであればまだしも、他人が作ったものであったり、しかもそれがスパゲッティであったりすると、特定ケースでのバグは不可避!
状態の制御をツール側でサポートしてくれたら他人の書いたコードでも安心できそうだし、そもそもスパゲッティになりにくいし、良いことしかない。この部分だけ切り出したプラグインとか無いのだろうか。少し探してみたけど、Javaでは存在しなさそう。いや、それならJavaでAkkaを使えってことになるのか・・・