弊社ではAkkaのActorを使って開発している部分があるのですが、Actorは基本的な部分以外はネット上に(日本語の)情報が少なく、公式ドキュメントを読んでもすっきり理解できないことが僕は割とあります。
というわけで、今回は解説しているところが少ないPersistentActorについて、その意義と基本的な使い方を解説します。

PersistentActorとは?

状態を持ったアクター(くだけて言うとvar変数をもったアクター)のための色々な機能をもったアクターです。
状態をもったアクターはPersistentActorを継承することで

  • アクター内の変数の値を変更する際の排他制御
  • クラッシュなどで変数の値が失われること

に対して心配をしなくてよくなります。

ここでは次の3点を解説することでPersistentActorを説明しています。

  • データを記録するpersist、saveSnapshotメソッド
  • リカバリーの仕組みとサンプルコード
  • 最後に例として、PersistentActorを継承したアクターの全体のサンプルコード

persistメソッドについて

PersistentActorでは、以下のようにpersistメソッドを使って状態を更新します。

  1. persist(Evt(n)) でそのイベントを非同期でjournal (データの保存場所の一つ)に記録。
  2. { cmd => name = cmd.name } は、上で行われる非同期処理のコールバックなので、journalにEvt(n)を記録した後に状態(var name)をこの中で更新。

という流れになっています。

persistメソッドを使って状態を更新する利点は以下の二つです。

  • persist(Evt(n)) の部分で、「Evt(n)を受け取った」という記録がjournalに付く。これはリカバリーする際に使用。(「状態を保存」ではなく「受け取ったメッセージを受け取った順に保存」となります。)
  • journalに記録するという処理は非同期で行われるのですが、persistメソッド内の処理 (journalに記録 → 状態を更新) は常に一つのスレッド(Single Execution Context)で行われるので、CmdメッセージをSampleActorが受け取った順に、確実に一つ一つpersistの処理がされていきます。(データ不整合のための排他制御を考えなくていい)

Snapshotについて

persistメソッドで状態を更新していると状態を更新した際のイベントが毎回journalに記録されるのでそれでも復元できるのですが、復元処理がものすごく長くなりがちだという問題があります。

例: Cmd(“name1”), Cmd(“name2”) … Cmd(“name100”)というメッセージがアクターに投げられたとする。
この際、journalだけだと、復元方法が

  • Evt(“name1”)をアクターに投げて、name = “name1″になる。
  • Evt(“name2”)をアクターに投げて、name = “name2″になる。
  • Evt(“name100”)をアクターに投げて、name = “name100″になる。

となって復元するのに100回処理が行われます。

この対策としてSnapshotがあります。上の例だとEvt(“name71”)が記録された後、saveSnapShot(name) を呼ぶと、復元の手順が

  • snapshotからリカバリーして、name = “name71″になる。
  • Evt(“name72”)をアクターに投げて、name = “name72″になる。
  • Evt(“name100”)をアクターに投げて、name = “name100″になる。

となり、復元までの処理が30回に軽減されます。

具体的には以下のようなコードを書いて、snapshotに状態を保存します。

リカバリー方法

journal, snapshotを使ってPersistentActorが受け取ったイベントや状態を保存していることはわかりました。では、次にどのように復元するのか見てみましょう。

「メッセージを受け取る & snapshotに状態を保存 → クラッシュ → リカバリー」の一例:

  1. Cmd(“name1”)をSampleActorに投げる。
  2. saveSnapshotする。
  3. Cmd(“name2”)をSampleActorに投げる。
  4. saveSnapshotする。
  5. Cmd(“name3”)をSampleActorに投げる。
  6. Cmd(“name4”)をSampleActorに投げる。
  7. クラッシュしてリスタートがかかるとする。
  8. 最新のsnapshotを復元して name = “name2” が入る
  9. snapshotを記録した時点からのjournalに記録された次のイベントはEvt(“name3”)なので、
    name = “name3” が入る
  10. 次にjournalに記録したイベントはEvt(“name4”)なので、
    name = “name4″となる。
  11. もう記録されたイベントがこれ以上ないので、リカバリー完了
  12. RecoveryCompletedが投げられ、
    println(“リカバリー完了したらすぐにここが呼ばれる。”)
    が実行される。

全体のサンプルコード

実装方法をサンプルコードを交えて説明していきます。

最後に

var変数を持ったアクターを作らないといけない状況になった場合は、是非PersistentActorを使って安全に状態を扱いましょう!
(ちなみに今回は主題から少し外れるので割愛しましたが、PersistentActorからメッセージを送るときはAtLeastOnceDeliveryというtraitを使うとメッセージが届くことが保証されてRemote Actorを使っている場合は特にオススメなので、次回に説明するかもしれません。)