Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

Play2を使うならAkkaも使っちゃおうぜな話

f:id:ko-shibata:20160405010726p:plain

並行処理してますか?(挨拶)

ScalaのWebフレームワークとして有名なPlay Frameworkには、標準でメッセージパッシング型並行処理ライブラリであるAkkaが添付されていて、Akkaのアクターを簡単に導入できるようになっています。 どんなものなのか、是非とも使ってみたいと思いませんか。思いますよね。思いましょう。

そういうわけで、Play FrameworkとAkkaを使って簡単なWebアプリケーションを作ってみましたので、解説する事にします。

注意

当記事内容につきまして、サンプル例がAkkaの用例としてはアンチパターンであるという旨のご指摘を頂きました。
「状態を持たない並行処理に対してAkka Actorを使うのはアンチパターン」という要旨です。

これに関しまして次回の記事で、当記事のサンプルがアンチパターンである事の解説を書きたいと思います。

目次

対象読者

この記事は、「Play Frameworkで簡単な掲示板やTwitterクローンっぽいものを作った事はあるよ」という人達を対象にしています。

「名前くらいは聞いた事があるよ」「Play Frameworkなんて触った事もないよ」「むしろScala自体知らないよ」という方はまずLightbendさんのページからactivatorなりsbtなり落としてみて触ってみてください。

拙いコードですが、この記事を通してアクターモデルを利用した並列処理、イベント駆動な設計に興味を持っていただき、より「Playらしさ」「Scalaらしさ」のあるアプリケーションを作成するきっかけになれば、と思います。

何を作ったのか

フォームから検索条件を入力すると、その条件に従って、大量のデータから必要なものを抽出してくれるようなシステムを作ろうと思いました。

といっても、まず検索対象の「大量のデータ」を用意するのが大変ですね。 ランダムなデータを大量に投入してみてもいいですが、味気ないです。 またデータが大きくなると、自前でデータベースを管理するのも大変になってきます。 ちょっとPlay+Akkaを触ってみたいだけなのに、SQL文をたくさん書くのは嫌ですね。

そこで、無料で利用できるWebAPIを利用しました。

TwitterAPIとかでも良かったのですが、今回は個人利用なら登録不要で使える「国立国会図書館サーチAPI」を使ってみました。

何種類かAPIが用意されていますが、GETメソッドとリクエストパラメータで条件を指定でき、XMLで結果を取得できるOpenSearchを利用するのが良さそうです。 JSONで結果を返してくれるAPIが無いのが残念ですね……。

※利用にあたっては「APIのご利用について」を確認しましょう。

本記事では、Akka・アクターの利用に焦点を当てて解説していきます。

なお、コード全体はGithubで公開していますので御覧ください。

github.com

環境

バージョン
MacOS 10.11.2
Play Framework 2.5.0-M2

裏話: 最初はWebSocketとかも使おうと思ったので、その辺りに色々変更の入った最新版の2.5系を入れたのですが、紙面の都合でなくなっちゃいました。せっかく(?)なのでPlayのバージョンはそのままです。

(何か2.5系に起因するエラーとかが起きて、それを解説できれば良いなぁ、と思ったのですが何も起きなかったです)

解説

アプリケーションの仕様は概ね以下の様なものです。

  • ルート(/)にアクセスするとページを表示する
  • タイトル、著者名、その他曖昧条件で資料を検索できる
  • 最大取得数を1件から200件まで指定できる

ページ(View)はHTMLファイル1枚だけで遷移等は行わず、データ取得はAjaxで処理する事としました。

Controller

package controllers

import akka.actor._
import akka.pattern.AskTimeoutException
import com.google.inject.name.Named
import com.google.inject.{Inject, Singleton}
import play.api._
import play.api.data.Form
import play.api.data.Forms._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.Json
import play.api.mvc._
import scala.concurrent.Future
import scalaz.\/.left
import scalaz.{-\/, \/-}
import actors.Library
import actors.Library.{RequestBooks, BookData}
import entities.Book
import forms.SearchForm

@Singleton
class Application @Inject()(system: ActorSystem,
                            @Named("ndl-client-actor") ndlClient: ActorRef)
  extends Controller {
  import akka.pattern.ask
  import scala.concurrent.duration._

  def index = Action(Ok(views.html.index("Simple OPAC")))

  val bookForm = Form(
    mapping(
      "title"  -> optional(text),
      "author" -> optional(text),
      "any"    -> optional(text),
      "count"  -> optional(number(min = 1, max = 200))
    )(SearchForm.apply)(SearchForm.unapply)
  )

  implicit val bookToJson = Json.writes[Book]
  implicit val timeout: akka.util.Timeout = 1 minute
  lazy val libraryActor = system.actorOf(Library.props)

  def books = Action.async { implicit req =>
    bookForm.bindFromRequest.fold(
      formWithError => {
        Future.successful(BadRequest("invalid request"))
      },
      {
        case validForm => (try {
          libraryActor.ask(RequestBooks(validForm, ndlClient)).mapTo[BookData].map(_.books)
        } catch {
          case e: AskTimeoutException => Future.successful(left(s"Server Error: \n$e"))
          case _ => Future.successful(left("Something wrong..."))
        }).map {
          case \/-(books) => Ok(Json.toJson(books))
          case -\/(msg) => InternalServerError(msg)
        }
      }
    )
  }
}
アクターの作成

このコード内では、アクターを2つの方法で作成し、利用しています。

  1. ActorSystem#actorOfメソッドで取得
  2. Guiceを使ってDI

ActorSystem#actorOfメソッドにPropsオブジェクトを与える事で、そのアクターを作成、取得する事ができます。

Propsオブジェクトというのは、Akkaアクターを初期化する為のオブジェクトで、基本的にAkkaのアクターはnewではなくPropsで取得する事になります。

ActorSystem#actorOfメソッドには第二引数としてアクターの名前を与える事ができます。与えなければ自動で付与されます。自分で名前を付ける場合、重複が無いように気をつけて下さい

またアクターはDIする事もできます。これについては後述します。

アクターとaskメソッド
{
  case validForm => (try {
    libraryActor.ask(RequestBooks(validForm, ndlClient)).mapTo[BookData]
  } catch {
    case e: Exception => Future.successful(BookData(left(s"Server Error: $e")))
  }).map {
    case BookData(\/-(books)) => Ok(Json.toJson(books))
    case BookData(-\/(msg)) => InternalServerError(msg)
  }
}

この部分で、Akkaアクターを利用しています。

今更ですが、アクターに関して説明します。アクターは「実行エンティティ」と呼ばれる存在で、それぞれ独立したコンテクストを持ち、処理を並行で実行する事ができます(並列で実行できるか否かは、アクターの実装に依ります。Akkaアクターの場合、並列に実行する事もあればしない事もありますが、これらはExecutionContextに依存します)。

独立して動くアクターに命令を出すには、「メッセージ」を送ります。メッセージはアクターが各々有している「メッセージボックス」(言語によってはメールボックスとも呼ばれます)に貯められ、アクターはボックスから1つずつメッセージを取り出し、メッセージに応じて処理を実行していきます。

アクターは基本的にメッセージ以外に依存しないので、アクターモデルでプログラムを書く事によって、自然とイベント駆動でリアクティブなロジックを組み立てる事ができます。

アクターにメッセージを送る方法としては、Erlang風の!メソッドが有名ですが、ここではaskメソッドを使用しています。

!メソッドは値を返しませんが、askメソッドは返り値を持つという違いがあります。返り値は、メッセージを受け取った側のアクターが、送信者に対して送り返すメッセージです。なので、受け取り側のアクター内に「送信者にメッセージを送り返す」という処理が実装されている必要があります。が、!メソッドでメッセージを送って別メソッドで待ち受けるより簡便でわかりやすいので見通しの良いコードになります。

actor ! someMessage // これだと返り値はunitで、別に返事を待ち受ける必要がある。

actor.ask(someMessage) // これならFuture[Any]型の返り値が受け取れる。勿論Futureなので非同期実行。

なお、askメソッドには?という別名があります。

actor ? someMessage

今回はRequestBooksというcase classをメッセージとしてlibraryActorに送っているわけですね。 RequestBooksは、

case class RequestBooks(search: SearchForm, client: ActorRef)

と定義されています。

ちなみにアクターにメッセージを送る際は、timeoutを決めておかなければなりません。 今回は、外部APIを使う兼ね合いもあり、ちょっと長めに1分程取っています。

implicit val timeout: akka.util.Timeout = 1 minute
アクターとaskメソッドと返り値

上で

Future[Any]型の返り値が受け取れる

と書いたとおり、askメソッドの返り値はAny型にアップキャストされてしまっています。 ですので、Future#mapToメソッド等で本来の型に戻してやらなければなりません。

この辺り型安全性が崩れてしまいますが、致し方ありません。

一応tryで囲んで例外が起きた時に捕捉するようにしてあります。 (エラーメッセージはあくまで開発用のものです)

libraryActor.ask((validForm, ndlClient)).mapTo[BookData]

BookDataの定義はこうです。

final case class BookData(books: String \/ Seq[Book])

scalazのEither型を利用しています。rightなら書誌情報のSeq、leftならエラーメッセージが入っています。 この戻り値を、

{
  case \/-(books) => Ok(Json.toJson(books))
  case -\/(msg) => InternalServerError(msg)
}

とパターンマッチして、rightの時は書誌情報一覧をJSON化して200で、leftの時はエラーメッセージを500で返すようにしています。

ちなみにこの時Future#mapを使っていますので、暗黙のExecutionContextが必要です。 これはコードの先頭でインポートしてあります。

import play.api.libs.concurrent.Execution.Implicits.defaultContext

scalaで並行処理・並列処理を取り扱う場合、このExecutionContextへの理解が不可欠です。

Library

次に、メッセージを受け取るアクターの処理を見ていきます。

package actors

import akka.actor._
import akka.pattern.AskTimeoutException
import entities.Book
import forms.SearchForm
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.languageFeature.postfixOps
import scala.xml.XML
import scalaz.\/
import scalaz.\/.{left, right}
import shapeless._
import shapeless.ops.record.{Keys, Fields}
import shapeless.syntax.std.traversable._
import shapeless.tag
import shapeless.tag.@@

class Library extends Actor {
  import Library._
  import akka.pattern.{ask, pipe}
  import NDLClient.QueryString

  override def receive = {
    case RequestBooks(SearchForm(None, None, None, _), _) => sender ! BookData(right(Seq.empty[Book]))
    case RequestBooks(search, ndlClient) => (try {
      ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse]
        .map { case NDLResponse(res) =>
          res.map { body =>
            for {
              item <- XML.loadString(body) \\ "item"
              book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from)
            } yield book
          }
        }.map(BookData)
    } catch {
      case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e"))
      case _ => Future.successful(left("Something wrong..."))
    }) pipeTo sender
  }
}

object Library {
  def props = Props[Library]
  implicit val timeout: akka.util.Timeout = 1 minute

  final case class RequestBooks(search: SearchForm, client: ActorRef)
  final case class BookData(books: String \/ Seq[Book])
  final case class NDLResponse(response: String \/ String)

  trait Cnt
  case class Search(title: Option[String],
                    creator: Option[String],
                    any: Option[String],
                    cnt: Option[Int @@ Cnt])

  implicit def toSearch(form: SearchForm) = {
    import form._
    Search(title, author, any, count.map(tag[Cnt](_)))
  }

  lazy val genBook = Generic[Book]
  lazy val lgenBook = LabelledGeneric[Book]

  lazy val bookFields = Keys[lgenBook.Repr].apply.toList.map(_.name)

  lazy val lgenSearch = LabelledGeneric[Search]

  object toQueryString extends Poly1 {
    implicit def caseString[T <: Symbol] = at[(T, Option[String])] {
      case (k, Some(v)) => Some(k.name -> v)
      case _ => None
    }
    implicit def caseCnt[T <: Symbol] = at[(T, Option[Int @@ Cnt])] {
      case (k, ov) => Some(k.name -> ov.getOrElse(20).toString)
    }
  }

  private[Library] def queryUrlBuilder(search: Search) =
    Fields[lgenSearch.Repr].apply(lgenSearch to search).map(toQueryString).toList.flatten
}
アクタークラス

アクタークラスはActorextendsして作ります。分かりやすくていいですね。

アクタークラスにはreceiveメソッドが無ければなりません。こいつはPartialFunctionを返すメソッドで、送られてくるメッセージに対して行う処理を記述します。 (他にもpreStartpostStopのようなフックメソッドで挙動をコントロールする事もできます)

今回は2種類のパターンが記述してありますね。まず、

case RequestBooks(SearchForm(None, None, None, _), _) => sender ! BookData(right(Seq.empty[Book]))

ですが、これは要するに、「検索条件が空なら空の情報を返す」という事です。 検索条件が存在しないのですから、APIを叩いても当然何も返ってきません。

ですので、APIを叩く事なくさっさと結果を返してあげます。 メッセージの「送り主」はsenderメソッドで取得できます。

その下の、

case RequestBooks(search, ndlClient) => (try {
      ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse]
        .map { case NDLResponse(res) =>
          res.map { body =>
            for {
              item <- XML.loadString(body) \\ "item"
              book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from)
            } yield book
          }
        }.map(BookData)
    } catch {
      case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e"))
      case _ => Future.successful(left("Something wrong..."))
    }) pipeTo sender

が、検索条件がある場合、つまり本来の場合の処理ですね。

ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse]
        .map { case NDLResponse(res) =>

再びaskメソッドが見えていますが、少し待って、まずはそのメソッドの引数を見てみましょう。

型消去

送っているのはQueryString型のメッセージです。

final case class QueryString(queryString: Seq[(String, String)])

これは単純にSeq[(String, String)]をラップするためのcase classですね。 何故こんな入れ物が必要なのかと言いますと、JVMでは型消去をしてしまうので、例えば

case stringSeq: Seq[(String, String)]

みたいに書いても、(String, String)の情報は消えてしまっていて、取れないのですね。 なので、Seq(1,2,3)とかでも受け取ってしまうのですが、当然これは実行時エラーを引き起こします。

その為、QueryStringクラスを定義しています。

またこういう状況ではなくても、1つのメッセージに対して1つのcase classを用意する事で、メッセージの意図を明確にする事ができますし、メッセージの形式を変更した場合でも、バグを起こす危険性が低くなるという助言を頂きました。

どういう事かと言いますと、メッセージ送信はどのような型でも行う事ができますが、ただし受信は、receiveメソッドで対応している型しか受け付けないので、例えば送信側のメッセージの型を変更し、受信側を変更し忘れた場合、コンパイルは通ってしまうのですが、送信側は永遠に返信の無いメッセージを送信し続ける事になる、という問題です。この手のバグは一般に発見し辛く、厄介です。しかしcase classでメッセージをラップすれば、メッセージの変更にはcase classの変更を伴う事になり、送信側と受信側での型の一致を保証する事ができます。

例を出します。以下の様なコードがあるとします。

// 送信側
val res = actor ? "Hello, world!"

...

// 受信側
class HelloActor extends Actor {
  override receive = {
    case message: String => sender ! s"I receive '$message'"
  }
}

文字列としてメッセージを受け取って、また文字列を返すだけのアクターです。

このアクターに、複数の文字列を受け取る事ができるように改良を加えたかったとします。

class HelloActor extends Actor {
  override receive = {
    case messages: Seq[String] => sender ! s"I receive '${ messages.mkString(",") }'"
  }
}

しかし忙しく、受信側のコードを変えた段階で、送信する側の修正は後回しにしよう、と思ってしまいました。 後は、お察しの通り、そんな修正があった事などすっかり忘れてしまいました。

さてこのコードをコンパイルし、動かすとどうなるでしょうか。

// 送信側
val res = actor ? "Hello, world!"

...

// 受信側
class HelloActor extends Actor {
  override receive = {
    case messages: Seq[String] => sender ! s"I receive '${ messages.mkString(",") }'"
  }
}

このコードは正しくコンパイルされますし、また実行されます。

しかし思った通りには動きません。メッセージを受け取るHelloActorクラスにString型に対する処理が記述されていないからです。

このように単純な、送信側と受信側だけのコードでしたら、片方を修正し忘れる事など無いかもしれません。

ですが現実のプロダクトコードは膨大で、かつ複雑です。1つのアクターが送るべきメッセージは無数にありますし、受け取るメッセージも多いでしょう。何種類ものアクターにメッセージを送ったり、逆に受け取ったりする事もあります。

そんな状況で、うっかり一部のコードを修正し忘れるという事は、よくある事です。

そういう時、このメッセージをcase classに入れて送れば、

case class Message(value: String)

// 送信側
val res = actor ? Message("Hello, world!")

...

// 受信側
class HelloActor extends Actor {
  override receive = {
    case Message(message) => sender ! s"I receive '$message'"
  }
}
...

メッセージの型を変えた時でも、

case class Message(value: Seq[String])

// 送信側
val res = actor ? Message("Hello, world!") // ここでコンパイルエラー! Message型のコンストラクタはStringを取らない!

...

// 受信側
class HelloActor extends Actor {
  override receive = {
    case Message(messages) => sender ! s"I receive '${ messages.mkString(",") }'"
  }
}

コンパイル時にエラーが起き、間違いを知る事ができます。

このようにして、バグを事前に、しかも分かりやすいエラーメッセージで知る事ができます。

さて、コードに戻りましょう。queryUrlBuilderメソッドは、SearchForm型の値を取り、List[(String, String)]型の値を返すメソッドです。

SearchForm(Some("春琴抄"),None,None,None)
SearchForm(None,Some("内田百閒"),None,None)
SearchForm(None,None,Some("Scala"),Some(40))

という入力から、

List(("title", "春琴抄")
List(("creator", "内田百閒"))
List(("any", "Scala"), ("cnt", "40"))

という出力を作り出します。

では、このメッセージを送りつけるもう一つのアクターを見てみましょう。

NDLClient

package actors

import actors.Library.NDLResponse
import akka.actor.Actor
import com.google.inject.{Inject, Singleton}
import play.api.Configuration
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.ws.WSClient
import scalaz.\/
import scalaz.\/.{left, right}

@Singleton
class NDLClient @Inject()(wsc: WSClient, config: Configuration) extends Actor {
  import NDLClient._
  import akka.pattern.pipe

  def receive = {
    case QueryString(queryString) =>
      wsc.url(ndlOpenSearchUrl(config)).withQueryString(queryString:_*).get().map { res =>
        NDLResponse(res.status match {
          case 200 => right(res.body)
          case _ => left(s"Connect failed.\n${res.body}")
        })
      } pipeTo sender
  }
}

object NDLClient {
  private[NDLClient] def ndlOpenSearchUrl(conf: Configuration) = conf.getString("settings.url.ndl.openSearch").get

  final case class QueryString(queryString: Seq[(String, String)])
}
アクターとDI

WSClientConfigurationをDIで注入したかったので、DI管理下のクラスになっています。

アクタークラスをDI管理下に置くためには、アクターインスタンスを作成する為のモジュールを独自に作らなければなりません。 幸いAkkaにはAkkaGuiceSupportが用意されているので、比較的簡単に書く事ができます。

Module

独自モジュールは次のようになっています。

package modules

import actors.{NDLClient => NDLClientActor}
import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport

class NDLClient extends AbstractModule with AkkaGuiceSupport {
  def configure = bindActor[NDLClientActor]("ndl-client-actor")
}

たったこれだけです。これを、application.confで、

play.modules.enabled += "modules.NDLClient"

と指定してやるだけで、適切なインスタンスがDIされたアクターを名前で取得する事ができます。 名前というのは、この場合"ndl-client-actor"の事です。 コントローラに書かれていた、

@Named("ndl-client-actor") ndlClient: ActorRef)

は、これを意味していたのですね。

WSライブラリ

処理に戻ります。

def receive = {
  case QueryString(queryString) =>
    wsc.url(ndlOpenSearchUrl(config)).withQueryString(queryString:_*).get().map { res =>

WSClientは機能豊富ですが、ここではurlメソッドでURLをセットし、withQueryStringメソッドでクエリストリングを付与した後、getメソッドでリクエストを行っています。非同期に実行され、結果はFutureで返されます。

NDLResponse(res.status match {
  case 200 => right(res.body)
  case _ => left(s"Connect failed.\n${res.body}")
})

レスポンスは、ステータスを見て、200ならbodyをrightで、それ以外ならエラーメッセージをleftで返しています。 (繰り返しになりますが、ここでのエラーメッセージはあくまで開発用のものです)

pipeTo

この結果を呼び出し元に返します。

} pipeTo sender

ここで、!ではなくpipeToを使っています。 pipeToは、その名の通りアクターからアクターへ処理をパイプするときに使うメソッドです。

この場合、返り値の型はFuture[NDLResponse]になるのですが、もしこれを!メソッドで返してしまうと、askメソッドの返り値がFutureでラップされる事から、二重にFutureでラップされてしまう事になります。 これを防ぐ為、flatMapを使うようなイメージで、pipeToで値を送り返してやります。

再びLibrary

さて、Libraryでの処理に戻ってきました。

取得したレスポンスを処理していきましょう。

.mapTo[NDLResponse]
  .map { case NDLResponse(res) =>
    res.map { body =>

まず返ってきた値からレスポンスボディを取り出します。 scalazのEither(\/)を使っているので、もし正しくないレスポンスだった場合、この以下の処理はスキップされ、leftがそのまま返されます。

for {
  item <- XML.loadString(body) \\ "item"
  book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from)
} yield book

そして bodyの文字列をXMLとして解析し、必要な情報を濾し取ってBook型に変換し、Seqとして返します。

後はこの結果を、

    }.map(BookData)
} catch {
  case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e"))
  case _ => Future.successful(left("Something wrong..."))
}) pipeTo sender

メッセージの送り主、即ちコントローラにpipeToで送り返してあげれば、コントローラは最終的に書誌情報が手に入るという事です。

ちょっと内部実装の話

さて、コードをベースに今まで語ってきましたが、最後に、Scalaの並行実行について少しだけ解説します。

Scalaの並行処理は、単に新しいスレッドを立ててそちらに処理を任せる、という単純なものではありません。

ではどのように動くかといいますと、それはExecutionContextによります。例えばサンプルコード等によく書いてあるscala.concurrent.ExecutionContext.Implicits.global だと、CPUの個数分しか並行実行しません。並列で実行できる最大限で並行処理をしようとしているわけですね。

ではそんなglobalを使って、例えば8個の並行処理を行おうとした場合どうなるか。こういう時は、4個のスレッドにそれぞれ2処理、いい感じに割り当てられます。さらに処理が増えた場合は、暇してるスレッドに優先的に割り振られていきます。

thread1 | 処理1 | 処理5 | 処理8 |
thread2 |   処理2   |  処理6  | 処理10(実行中)
thread3 |   処理3   | 処理7 | 処理9(実行中)
thread4 |         処理4(実行中)

新規処理: 処理11 <- こいつはthread1に割り振られる

これが何を意味しているかと言いますと、空いているスレッドが無ければ処理は実行されないという事です。 4コアCPUの環境で標準のglobalを使い、まず重い処理を4つ走らせたとしましょう。その後、軽くて優先度が高い処理がどれだけ発生しても、まず最初の4つの処理が終わらなければ後続の処理は何時までたっても始まりません。

thread1 | 処理1(実行中)
thread2 | 処理2(実行中)
thread3 | 処理3(実行中)
thread4 | 処理4(実行中)

新規処理: 処理5, 処理6, 処理7, 処理8, ...

なので、ブロッキングするような処理をどんどん並行で実行していると、並行処理しているはずなのに速度が出ない、という事態に陥ります。単に「重い処理」ではなく「ブロッキングを伴う処理」というのが肝です。重い処理はどうしようもない点がありますが、ブロッキングであれば別スレッドに分けるだけで回避ができます。

ですので、そういう処理には独自のExecutionContextを割り当てる等、対策を講じた方が良いでしょう。

ここで述べたような内容は、Scalaの並行処理(Futureとか)を扱った文脈では必ずと言っていい程ちゃんと解説がなされています。御存じの方も多かったでしょう。そんな事をここでも繰り返し述べたのは、それだけ重要だからです。

処理が詰まる可能性がある、という危険性は、Scalaで並行処理を行う上で必ず頭のどこかに留めておくべきです。

で、

それほど強く言ったのに、今回のサンプルコードではあまりそういう危険性に対する配慮を行っていませんね。ExecutionContextplay.api.libs.concurrent.Execution.Implicits.defaultContextを何も考えずに使っています。

これは面倒くさかったからあくまでサンプルなので簡潔さを優先したからです。 実用的なコードを書く際は、是非ともご注意下さい。

結び

Play FrameworkとAkkaを利用して作られた簡単なWebアプリケーションの、Akkaに関する解説を行いました。 アクターモデルで構築されたアプリケーションには以下の様な利点があります。

  • 並行性・並列性
  • 耐障害性
  • 疎結合

これらの利点は、いずれも大規模アプリケーションを作成する際にこそ強い影響を持つものですので、今回の例だと少し有り難みが薄かったかもしれません。 また、耐障害性に関しては、今回の例で分かりやすいサンプルを提示する事ができませんでした。

しかし、アクターモデルの「メッセージを介してアクター同士が処理を連鎖する」という動きは摑んでいただけたかと思います。

以上で簡単ですが、Akkaに関する解説とさせて頂きます。

アクターモデルの本領はもっとアクター数が増加した時に発揮されますので、是非ともAkkaを使ったアプリケーションを試してみてください。 また、業務でこれらの技術を使いたい場合はぜひ弊社にご連絡頂ければと思います。


更新履歴

  • 2016年4月22日 注意事項を追記