2012-04-05

Play 2.0 コードリーディング - リクエストを受ける(前)

一人Play 2.0 コードリーディングの第二弾です。

今回はリクエストを受けとった時にどのような処理をするかを追ってみました。
そもそも発端はPlay2.0ではリクエストをどのように処理させているのか気になり、
意図的に例外を発生させてみました。

すると、出力されたスタックトレースは以下のようなものだったのです。

play.core.ActionInvoker$$anonfun$receive$1$$anon$1: Execution exception [[Exception: null]]
        at play.core.ActionInvoker$$anonfun$receive$1.apply( ...
        at play.core.ActionInvoker$$anonfun$receive$1.apply( ...
        at akka.actor.Actor$class.apply( ...
        at play.core.ActionInvoker.apply( ...
        at akka.actor.ActorCell.invoke( ...
        at akka.dispatch.Mailbox.processMailbox( ...
Caused by: java.lang.Exception: null
        at controllers.Application$$anonfun$index$1.apply( ...
        at controllers.Application$$anonfun$index$1.apply( ...
        at play.api.mvc.Action$$anonfun$apply$4.apply( ...
        at play.api.mvc.Action$$anonfun$apply$4.apply( ...
        at play.api.mvc.Action$$anon$1.apply( ...
        at play.core.ActionInvoker$$anonfun$receive$1$$anonfun$4.ap ...

大本がakka.dispatch.Mailbox.processMailboxになっており、
Play2.0本体のスタックがほとんど出力されていません。
こりゃどういうことだ?と気になってソースを追ったのがこのエントリーです。

結論的を先に書いてしまうと、
Play2.0は以下の流れでリクエストを処理していました。

  1. リクエストを受けとるとNettyの仕組みを使って play.core.server.netty.PlayDefaultUpstreamHandlerがハンドリングする
  2. play.core.Serverを経由してplay.core.Router.RoutesがActionを決定する
  3. AkkaにActionを実行する為のメッセージが登録される
  4. AkkaからActionを実行する

これらの処理を順を追って説明したいと思います。
大半は自分のためのメモのため、ダラダラと長くなってしまったので前後編に分けます。


さて、Play2.0は内部的にはNettyを使用して動いています。
Play2.0自体はNettyのラッパーと言っても過言ではありません。
そのため、リクエストを受けとるとorg.jboss.netty.channel.SimpleChannelUpstreamHandlerのサブクラスでハンドリングします。

つまりそれは、play.core.server.netty.PlayDefaultUpstreamHandlerのことです。
リクエストを受けとるとPlayDefaultUpstreamHandlerのmessageReceivedメソッドが実行されます。

private[server] class PlayDefaultUpstreamHandler(server: Server, allChannels: DefaultChannelGroup) 
    extends SimpleChannelUpstreamHandler with Helpers 
    with WebSocketHandler with RequestBodyHandler {
  // Play2.0本体で、リクエストを受けとった時に一番最初に実行されるメソッド
  override def messageReceived(
      ctx: ChannelHandlerContext,
      e: MessageEvent) {
    e.getMessage match {
      case nettyHttpRequest: HttpRequest => {
        val keepAlive = isKeepAlive(nettyHttpRequest)
        val websocketableRequest = websocketable(nettyHttpRequest)
        var version = nettyHttpRequest.getProtocolVersion
        val nettyUri = new QueryStringDecoder(nettyHttpRequest.getUri)
        val parameters = Map.empty[String, Seq[String]] 
              ++ nettyUri.getParameters.asScala.mapValues(_.asScala)

        ...

        // Actionの取得
        val handler = server.getHandlerFor(requestHeader)

messageReceivedではリクエストヘッダーの解析等を行なった後、
server.getHandlerForでActionを取得する処理が行なわれます。

serverのクラスはplay.core.server.NettyServerクラスで、
getHandlerForの返り値の型はEither[Result, (Handler, Application)]です。
つまり、正常にActionが決定した場合rightでHandlerとApplicationのタプルを、
エラーが発生した場合はleftでResultを返すということです。
HandlerはActionがmix-inしているトレイトでメンバーを一切もたない、
マーカーの為の型です。


続いてgetHandleForの中身も見てみます。getHandleForの定義はplay.core.server.Serverトレイトにあります。

  def getHandlerFor(request: RequestHeader): Either[Result, (Handler, Application)] = {

    def sendHandler: Either[Throwable, (Handler, Application)] = {
      applicationProvider.get.right.map { application =>
        val maybeAction = application.global.onRouteRequest(request)

        // Actionが見つからなかった場合はNotFound用のHandlerを返す。
        (
          maybeAction.getOrElse(
            Action(BodyParsers.parse.empty)
              (_ => application.global.onHandlerNotFound(request))), 
            application
        )
    }
    def logExceptionAndGetResult(e: Throwable) = {
      // エラー発生時のハンドリング
      DefaultGlobal.onError(request, e)
    }

    sendHandler.left.map(logExceptionAndGetResult)
  }

getHandlerForのポイントは以下の三つです。

  1. play.core.ApplicationProviderからApplicationを取得しています。
  2. Actionの取得処理をapplication.global.onRouteRequest()に移譲しています。
  3. エラーのハンドリングをgetHandlerForで行なっています。Actionが見付からなかった場合はNotFoundを返すHandlerを返し、途中で例外が発生した場合はDefaultGlobal.onErrorを使ってInternalServerErrorのResultを返します。

applicationProviderの実際の型はPlayを実行しているコンテキストから、以下の三つのサブクラスの中から一つが選ばれます。

  1. StaticApplication - 本番稼動などで使用するApplicationProvider
  2. TestApplication - テスト用のApplicationProvider
  3. ReloadableApplication - Dev Mode用のリロードが出来るApplicationProvider

application.global.onRouteRequestではconfig/routesをもとに、
実際にActionを取得する処理を行なっていますが、続きは後編で解説します。

2012-03-27

Play 2.0 コードリーディング - プラグインの読み込み

最近はボルダリングに嵌っています。

提示された課題に対して、自身が持つリソースを使っていかに解決するかを
考えるのはプログラミングに近い楽しさだなぁ、とは思っていません。


先日、「Play Framework 2.0 ソースコードリーディングの会」でなんか話せって言われました。

http://partake.in/events/310d72cb-b5b2-4503-98c7-df5887582c27

話す内容はまだ決っていません。

そんなわけでPlay 2.0のコードをぼちぼち読み始めているのですが、
折角なのでその内容を残しておこうと思いました。
一人Play Framework 2.0 ソースコードリーディングの会結成の瞬間です。

記念すべき第一回はModule(Plugin)の読み込みです。
いきなりマニアックな部分から始まっているのは僕がたまたま興味があったからです。

Moduleを自作する方法についてはfitsさんの記事など参照してください。

http://d.hatena.ne.jp/fits/20120324/1332584629


さて、Moduleの読み込みはplay.api.Applicationが行なわれます。
まず始めに、Application.pluginClassesメソッドを使用して設定ファイルを読み込みます。

class Application {
  private[api] def pluginClasses: Seq[String] = {

    import scalax.file._
    import scalax.io.JavaConverters._
    import scala.collection.JavaConverters._

    val PluginDeclaration = """([0-9_]+):(.*)""".r

    val pluginFiles = 
      classloader.getResources("play.plugins").asScala.toList ++ 
      classloader.getResources("conf/play.plugins").asScala.toList

    pluginFiles.distinct.map { plugins =>
      (plugins.asInput.slurpString.split("\n").map(_.trim))
          .filterNot(_.isEmpty).map {
        
        case PluginDeclaration(priority, className) => 
          (priority.toInt, className)
      }
    }.flatten.sortBy(_._1).map(_._2)
  }

設定ファイルのフォーマットは 450:plugins.SamplePlugin のように、
読み込み順のプライオリティとクラス名をコロン区切りにしたもので、
正規表現を使ってこれをパースし、プライオリティ順に並びかえています。
実際にpluginClassesを使っているのは、同じくApplicationのpluginsを定義する所です。

class Application {
  val plugins: Seq[Plugin] = Threads.withContextClassLoader(classloader) {
    pluginClasses.map { className =>
      try {
        val plugin = classloader.loadClass(className)
              .getConstructor(classOf[Application])
              .newInstance(this).asInstanceOf[Plugin]
        if (plugin.enabled)
          Some(plugin)
      } catch {
        case e: java.lang.NoSuchMethodException => {
          val plugin = classloader.loadClass(className)
                .getConstructor(classOf[play.Application])
                .newInstance(new play.Application(this))
                .asInstanceOf[Plugin]
          if (plugin.enabled)
            Some(plugin)
        }
      }
    }.flatten
  }
}

pluginClassesで作ったクラス名のインスタンスを
ClassLoaderとリフレクションを使って順番に生成しています。

NoSuchMethodExceptionが発生した時は、
引数をplay.Applicationに変更して再実行していますが、
play.Applicationはjavaで書かれており、
恐らくjavaで書かれたModuleを読み込むためのものだと思います。


ここまででModuleの読み込みは完了です。

後は実際に、 play.api.Play.start や play.api.Play.stop によるハンドラの実行や、

object Play {
  def start(app: Application) {
    // First stop previous app if exists
    stop()

    _currentApp = app

    app.plugins.foreach(_.onStart)

    app.mode match {
      case Mode.Test =>
      case mode => 
        Logger("play").info("Application started (" + mode + ")")
    }
  }

  def stop() {
    Option(_currentApp).map {
      _.plugins.foreach { p =>
        try { p.onStop } catch { case _ => }
      }
    }
    _currentApp = null
  }
}

play.api.Application.plugin を使ったインスタンスの取得が行なわれます。

class Application {
  def plugin[T](implicit m: Manifest[T]): Option[T] =
    plugin(m.erasure).asInstanceOf[Option[T]]

  def plugin[T](pluginClass: Class[T]): Option[T] =
    plugins
      .find(p => pluginClass.isAssignableFrom(p.getClass))
      .map(_.asInstanceOf[T])
}
2012-01-26

Play2.0でmoreStylesとmoreScripts

環境

  • Play framework 2.0 RC1(trunk)

Scala Templateの練習。
機能が用意されていなくてもScalaコードが書けるので簡単に実現できますね。

javaScripts.scala.html

@(scripts: Seq[String])

@scripts.map { script =>
  <script src="@routes.Assets.at(script)" type="text/javascript"></script>
}

styleSheets.scala.html

@(styleSheets: Seq[(String, String)])

@styleSheets.map { case (media, styleSheet) =>
  <link rel="stylesheet" media="@media" href="@routes.Assets.at(styleSheet)">
}

main.scala.html

@(moreStyles: Seq[(String, String)] = Nil, moreScripts: Seq[String] = Nil)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>sample</title>
    <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">

    @styleSheets(
      List(
        ("screen", "stylesheets/main.css")
      ) ++ moreStyles
    )

    @javaScripts(
      List(
        "javascripts/jquery-1.7.1.min.js"
      ) ++ moreScripts
    )
  </head>
  ...

index.scala.html

@main(
  moreScripts = List("javascripts/moreScript.js")
) {
  hello!
}

デザイナにとっては見た目がやさしくないかもしれないけど便利。

2012-01-24

Play framework + Scala でリクエストが返らずに嵌った

環境

  • Play framework 1.2.4
  • Scala Module 0.9.1 (Scala 2.8.1)

ある時を境にどのURLにリクエストを投げても、
ずっと読み込み中のままいつまでたってもレスポンスがかえってこなくなった。

ログを見ても以下で止ったままで何も出力されない。

...

Traversing /home/kiris/project/sample1/tmp/generated/views.defaults.html.welcome.scala
Traversing /home/kiris/project/sample1/tmp/generated/views.html.main.scala
API phase took : 1.759 s

原因がさっぱりわからないのでThread Dumpだしてみた。

% jps
19264 Jps
18730 Server

% jstac 18730
"Thread-3" prio=10 tid=0x00007fe8d87ff000 nid=0x6ba9 waiting for monitor entry [0x00007fe8dc3d7000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at sbt.BufferedLogger.doBufferableIf(BufferedLogger.scala:87)
	- waiting to lock <0x00000000f6ec6e78> (a sbt.Logger$$anon$1)
	at sbt.BufferedLogger.doBufferable(BufferedLogger.scala:85)
	at sbt.BufferedLogger.log(BufferedLogger.scala:75)
	at sbt.Logger$class.error(Logger.scala:74)
	at sbt.AbstractLogger.error(Logger.scala:8)
	at sbt.BasicIO$$anonfun$processErrFully$1.apply(ProcessImpl.scala:51)
	at sbt.BasicIO$$anonfun$processErrFully$1.apply(ProcessImpl.scala:51)
	at sbt.BasicIO$.readFully$1(ProcessImpl.scala:73)
    ...

どうもsbtからエラーログを出力する所で止ってしまっているっぽい。
出力しようとしている文字列がわかれば原因が特定できそうなので、
Eclipse PluginのMemory Analyzer(http://eclipse.org/mat/)を使ってヒープ解析。

f:id:kiris60:20120124131221p:plain
Thread-3で検索かけて調べてみると、どうもJPAとAnorm共存させていたせいか、
JPA側のアノテーションプロセッサの処理でコケてたっぽい。怪しい行を削除したら動いた。

まとめ

  • PlayでEnsimeのデバッガーを動かせるようしておくべき
  • あけましておめでとう

2011-12-16

Scala 2.9.1 + sbt 0.11.2 でpicture-showをインストール

picture-showについてはこちらを参照。

[error] {file:/home/iwanaga/repos/picture-show.git/}PictureShow Conscript/*:update: sbt.ResolveException: unresolved dependency: org.scala-tools.sbt#launcher-interface;0.7.4: not found
[error] Total time: 31 s, completed 2011/12/16 12:16:11

のように怒られて、picture-showがインストールできない人向け。
ローカルで依存関係を解決しても、conscriptでコケるのでforkしてしまった。

% git clone git://github.com/kiris/picture-show.git
% cd picture-show
% sbt publish-local
% cs kiris/picture-show
2011-12-12

Javaでシングルトンの遅延初期化

もっとも簡単なシングルトンの例

@ThreadSafe
final class Singleton {
  public static final Singleton INSTANCE = new Singleton();
  private Singleton() {}; 
}

staticフィールドの初期化は、そのクラスに初めてアクセスされた(≠クラスがロードされた)時に行なわれる。

そこで次の例では、インスタンスが必要になる時まで生成を遅らせている。

@ThreadSafe
final class Singleton {
  private static Singleton instance;
  private Singleton() {}; 
  public static synchronized Singleton getInstance() {
    if(instance == null) { 
      instance = new Singleton();
    }
    return instance;
  } 
}

確かにインスタンスの生成は遅らせされたが、getInstanceを実行する度にスレッドの同期が行なわれるのはうれしくない。

そこで登場するのが悪名高きdouble-checked locking pattern。
アウトオブオーダー書き込みのせいで、不完全なインスタンスが別スレッドに公開されることがあるので使ってはいけない。

@NotThreadSafe
final class Singleton {
  private static Singleton instance;
  private Singleton() {}; 
  public static Singleton getInstance() {
    if(instance == null) { 
      synchronized(Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

Java 5.0からは変数にvolatile修飾子を付けることでこれを防げる。
以下はJava 5.0以降でのみ安全なdouble-checked locking pattern。

@ThreadSafe
final class Singleton {
  private static volatile Singleton instance;
  private Singleton() {}; 
  public static Singleton getInstance() {
    if(instance == null) { 
      synchronized(Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

そして、最後に示すのがInitialization-on-demand holder idiom。
LazyHolderの初期化は、getInstance()が実行されるまで行なわれないので、
スレッドセーフかつ高速なシングルトンの遅延初期化が実現できる。

@ThreadSafe
public class Singleton {
  private Singleton() {}
 
  private static class LazyHolder {
    public static final Singleton INSTANCE = new Singleton();
  }
 
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

refs

2011-12-11

Scala 2.9.1 + Ensime 0.7.6 + SBT 0.11.0 でensime-sbtが動かない

エラー内容

        ...

	==== Scala-Tools Maven2 Snapshots Repository: tried

	  http://scala-tools.org/repo-snapshots/org/scala-tools/sbt/sbt_2.9.1/0.7.4/sbt_2.9.1-0.7.4.pom

		::::::::::::::::::::::::::::::::::::::::::::::

		::          UNRESOLVED DEPENDENCIES         ::

		::::::::::::::::::::::::::::::::::::::::::::::

		:: org.scala-tools.sbt#sbt_2.9.1;0.7.4: not found

		::::::::::::::::::::::::::::::::::::::::::::::



:: USE VERBOSE OR DEBUG MESSAGE LEVEL FOR MORE DETAILS
unresolved dependency: org.scala-tools.sbt#sbt_2.9.1;0.7.4: not found
Error during sbt execution: Error retrieving required libraries
  (see /Users/euler/.sbt/boot/update.log for complete log)
Error: Could not retrieve sbt 0.7.4

Process sbt exited abnormally with code 1

ensime-sbt-project-dir-pに"/project"をセットするか、空の/project/build.propertiesを作成すれば良いっぽい。

ref https://github.com/aemoncannon/ensime/issues/193