tkawachi Blog

Inside Play! Continuation – JavaFlow

Play! 1.x 系 では version 1.2 から continuation (継続)が導入されました。 これを使ってController処理を一時中断、終わったら再開して応答を返す、なんてことをしているみたいです。 これによって、HTTP処理スレッドは他のリクエストの処理にかかることができ、不要な待ち時間を少なくすることができるようです。 興味深いですね。

Continuation の実装として JavaFlow が使われています。 JavaFlow の日本語記事はほとんど見当たらない様子ですので、 Play! の continuation 理解を深めるためにも、JavaFlow を触ってみたいと思います。

JavaFlow の入手

Downloadページ を見てみると、現時点でリリース版は無いことがわかります。(なんと!) 「初版もリリースされていないライブラリを使っていたのか」と思う気持ちを押さえて svn co しましょう。

$ svn co http://svn.apache.org/repos/asf/commons/sandbox/javaflow/trunk/ javaflow

pom.xml が含まれているので Maven project ですね。依存関係を全て含んだ .jar を作りたかったので ここ を参考にして pom.xml を書き換えて mvn を実行しました。

$ svn diff pom.xmlIndex: pom.xml
===================================================================
--- pom.xml     (revision 1221696)
+++ pom.xml     (working copy)
@@ -128,6 +128,14 @@

   <build>
     <plugins>
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <configuration>
+          <descriptorRefs>
+                <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+        </configuration>
+      </plugin>
       <plugin>     
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>

$ mvn package assembly:single

target/commons-javaflow-1.0-SNAPSHOT-jar-with-dependencies.jar が生成されました。

JavaFlow を使う2つの方法

JavaFlow を使って continuation を実行するには bytecode を enhance する必要があります。これは推測ですが、Call stack を保存・復元する必要があり、Java に用意されている言語機能ではそれが実現不能だからではないでしょうか。

JavaFlow のドキュメントによると、2つの方法で bytecode を enhance できるようです。

  • JavaFlow の ant task を使ってコンパイル時に enhance する
  • org.apache.commons.javaflow.ContinuationClassLoader を使って class を load する。

Play! では後者に近い方法を利用しています。 play.classloading.enhancers.ContinuationEnhancer が JavaFlow の ContinuationClassLoader と同じようなことをしています。 (Play! には JavaFlow 及び依存ライブラリの .jar が含まれているのですが、ant task も ContinuationClassLoader も何故か .jar 中に存在しなかったため、ソースからコンパイルしたのでした。)

今回は ant task を使ってみようと思います。

Sample continuation

ここから JavaFlow の tutorial 相当のコードを動かしてみます。 compile できるようになるまで案外苦労しました。

build.xml はこんな感じ。

<?xml version="1.0" encoding="UTF-8"?>
<project name="JavaFlowTest" default="run">

  <!-- javaflow の jar file の位置に応じて変更してください -->
  <property name="javaflow-jar" value="lib/commons-javaflow-1.0-SNAPSHOT-jar-with-dependencies.jar" />

  <taskdef name="javaflow"
    classname="org.apache.commons.javaflow.ant.AntRewriteTask">
    <classpath>
      <path location="${javaflow-jar}" />
    </classpath>
  </taskdef>

  <target name="compile" depends="clean">
    <!--
    複数回 javaflow task で bytecode 変換しようとするとエラーになるようなので
    depends="clean" しておく。
    -->
    <mkdir dir="build/classes"/>
    <javac srcdir="src" destdir="build/classes" classpath="${javaflow-jar}"
      includeantruntime="false" />
    <javaflow srcdir="build/classes" destdir="build/classes">
      <include name="cont/**/*.class" />
    </javaflow>
  </target>

  <target name="run" depends="compile">
    <java fork="true" classname="Main">
      <classpath>
        <path location="${javaflow-jar}" />
        <path location="build/classes" />
      </classpath>
    </java>
  </target>

  <target name="clean">
    <delete dir="build" />
  </target>

</project>

JavaFlow のドキュメント には <javaflow /> task の attribute は srcdir および dstdir と書いてあるが、そのまま実行するとエラーとなった。dstdir は destdir が正しいようです。後ほどバグレポートしておこう。

src/cont/MyRunnable.java は 0 から 9 までを印字します。ただし1回ループを回るごとに Continuation.suspend() します。

package cont;
import org.apache.commons.javaflow.Continuation;

public class MyRunnable implements Runnable {
  public void run() {
    System.out.println("started!");
    for( int i=0; i<10; i++ )
      echo(i);
  }
  private void echo(int x) {
    System.out.println(x);
    Continuation.suspend();
  }
}

src/Main.javacont.MyRunnableContinuation として実行する。ただし5回実行した後の状態を取っておき、最後にもう一度実行しています。

import org.apache.commons.javaflow.Continuation;
import cont.MyRunnable;

public class Main {
  public static void main(String[] args) {
    Continuation beginContinuation = Continuation.startSuspendedWith(new MyRunnable());
    Continuation c = beginContinuation;
    System.out.println("Loop 5 times");
    for (int i = 0; i < 5; i++) {
      c = Continuation.continueWith(c);
    }
    Continuation fifthContinuation = c;
    System.out.println("Run the rest");
    while (c != null) {
      c = Continuation.continueWith(c);
    }
    System.out.println("Run from fifthContinuation again!");
    c = fifthContinuation;
    while (c != null) {
      c = Continuation.continueWith(c);
    }
  }
}

実行結果はこうなりました。

$ ant
…省略…
run: 
     [java] Loop 5 times
     [java] started!
     [java] 0
     [java] 1
     [java] 2
     [java] 3
     [java] 4
     [java] Run the rest
     [java] 5
     [java] 6
     [java] 7
     [java] 8
     [java] 9
     [java] Run from fifthContinuation again!
     [java] 5
     [java] 6
     [java] 7
     [java] 8
     [java] 9

BUILD SUCCESSFUL
Total time: 2 seconds

5回回した時点の Continuation オブジェクトをとっておいて後から好きなときに実行出来ることがわかります。

Play! では await() 実行時に Continuation.suspend() を呼び出しています。 Continuation object を得てから Job などの完了後に再開しているのでしょう。

java.lang.Error: Internal error が出るんですが…

await() は code coverage module の cobertura と一緒に使っちゃダメです。 両者共に bytecode を変更するのですが、秘孔を突くと起動時に妙な例外と共に落ちます(レポート済み)。v1.3 が修正 milestone とされていますがまだバグは健在のようです。

他の bytecode 変換モジュールを使ってエラーが起きた場合にも、ContinuationEnhancer の存在を頭の隅で覚えておくと良いかもしれません。

明日は

@i2key さんです。お楽しみに!

Comments