2013年4月5日金曜日

PlayのFutureの注意点

Play frameworkでプログラミングするときは基本的にブロックする処理は避けるべきです。
しかし、通信処理や重い計算などでどうしても、一時的にブロックしてしまうことはあり得ます。そんなときはFutureを使えばよいですが、使い方にちょっと注意が必要らしいです。

次の例は5分ごとに"Hello"を返す WebSocket  サーバーの実装です。

package controllers

import play.api._
import play.api.mvc._
import play.api.libs.iteratee.Iteratee
import play.api.libs.iteratee.Enumerator
import scala.concurrent.Future
import play.api.libs.concurrent.Execution.Implicits._
import scala.language.reflectiveCalls

object Application extends Controller {
  
  def index = WebSocket.using {
    req =>
      val in = Iteratee.consume[String]()
    
      val out = Enumerator.repeatM {
        Future {
          create()
        }
      }

      (in, out)
  }
  
  def create () : String = {
    Thread.sleep(5 * 60 * 1000) //重い処理を表現
    "Hello"
  }
}

Future { } に 重い処理を渡していますが、このときに暗黙の引数としてExecutionContextが必要です。ググると「play.api.libs.concurrent.Execution.Implicits._」をimportするとよいみたいな記事が見つかったので、その通りにしたというところです。

しかし、このサーバーを実行した上で、このwebsocketサービスを利用するページをブラウザでたくさん(5~10)のタブでどんどん開くと、Playサーバーが応答しなくなってしまいます。他のサービスや、静的コンテンツなどもplayのものはすべてbusyになってしまいます。

問題の原因はおそらく、Futureでくるんだ処理が実行される環境(ExecutionContext)です。Playのドキュメント「Understanding Play thread pools」を読むとだいたい次のような事が書いてあります。
  • Playでブロックする処理や重い処理を書くときは、それがどのThread Poolで実行されるか意識する必要がある
  • Playには目的に応じていくつかのThread Poolがある
  • Thread Poolの設定はapplication.confでする
そこで、AkkaでThread Poolを用意してそこで重い処理をするように明示的に指定するという方法を取りました。修正後のコードは以下の通りです。
package controllers

import play.api._
import play.api.mvc._
import play.api.libs.iteratee.Iteratee
import play.api.libs.iteratee.Enumerator
import scala.concurrent.Future
import scala.language.reflectiveCalls
import play.api.libs.concurrent.Akka
import play.api.Play.current

object Application extends Controller {
  
  def index = WebSocket.using {
    req =>
      val in = Iteratee.consume[String]()
    
      val executionContext = Akka.system.dispatcher
      val out = Enumerator.repeatM {
        Future {
          create()
        } (executionContext)
      }

      (in, out)
  }
  
  def create () : String = {
    Thread.sleep(5 * 60 * 1000)
    "Hello"
  }
}




環境
  • Play 2.1.1
  • Scala 2.10.1