Play2.0のtarget/startとその停止方法について調べたこと

評価/本番環境へデプロイする過程で調べたことについて書きます。

  • バックグラウンドでPlayを起動する(target/start)
  • 起動時にDB設定などを指定する
  • target/startで起動したサーバーを停止する

環境

  • Play2.0.4
  • Scala2.9.2

バックグラウンドでPlayを起動する(target/start)

Playを起動するにはいくつかのコマンドがあります。
こちらから説明を引用します。

play run

run コマンドは開発モードのためだけに用意されたものなので、本番環境では絶対に利用しないでください。開発モードの場合、リクエスト毎に sbt が全てのソースファイルの更新チェックを行ってしまいます。

この理由から、デプロイコマンドとしてrunは不適切です。

play start

詳細は先のリンクを見て頂くことにして、問題となるのは次の部分。

プロセスを切り離すために Ctrl+D が必要だということ

Ctrl+Dでプロセスを切り離してもサーバーは生き続けますが、
手作業が必要になるのはよろしくありません。
そこで、次のtarget/startを使うことになります。

target/start

まず「play clean compile stage」を実行します。「sbt clean compile stage」でも構いません。
そうすると以下のものが作られます。

  • target/stagedディレクトリとコンパイルされたモジュール
  • target/startという起動コマンド

そしたら「target/start」で起動出来ます。
これをさらにバックグラウンドで起動するには次のようにします。

$ ./target/start >/dev/null 2>&1 &

起動時にDB設定などを指定する

./target/startを見ると分かりますが、中身はただのjavaコマンド呼び出しです。
そのため、-Dオプションを用いればシステムプロパティを指定することができます。
例えば以下のような設定ができます。

項目 プロパティ名
起動時のPIDファイル出力先 pidfile.path -Dpidfile.path=/tmp/play_pid
HTTPポート http.port -Dhttp.port=8888
HTTPアドレス http.address -Dhttp.address=192.x.x.x
DBドライバ db.default.driver -Ddb.default.driver=com.mysql.jdbc.Driver
DBのURL db.default.url -Ddb.default.url=jdbc:mysql://localhost/sample
DBユーザー db.default.user -Ddb.default.user=hoge
DBパスワード db.default.password -Ddb.default.password=passpass

target/startで起動したサーバーを停止する

ここまでの設定で起動までは出来ましたが、停止はどうすればいいでしょうか。
2つのアプローチがあります。

1. PID指定してkill

先ほどのリンクには次のように書かれています。

サーバの起動時にはサーバのプロセス ID が表示されると同時に、 RUNNING_PID というファイルに書き込まれます。起動中の Play サーバを停止させるためには、単にそのプロセスに SIGTERM シグナルを送信してアプリケーションを終了すればよいでしょう。

説明は「play start」項に書かれているものですが、RUNNING_PIDが生成されるのは「target/start」でも同じです。
(※-Dpidfile.pathで指定していればRUNNING_PIDではなくてそのファイル名となる)

とすると、RUNNING_PIDに書かれているPIDを使ってkillするしかないのでしょうか。
公式のWikiでそう書かれているので、非推奨な方法というわけではなさそうです。

2. play stop

「play stop」というコマンドがあります。これを実行しても上記1と同じ結果になります。
なお、「play stop」は通常のコンソールから実行します。
playコンソールにはstopというコマンドはありません。

ところでplay stopは内部的に何をしているのでしょう。
https://github.com/playframework/Play20/blob/2.0.4/playを見ると、結局上記1と同じことだと分かります。
RUNNING_PIDからPIDを取得してkillしているだけです。

  if test "$1" = "stop"; then
    if [ -f RUNNING_PID ]; then
      echo "[info] Stopping application (with PID `cat RUNNING_PID`)..."
      kill `cat RUNNING_PID`

      RESULT=$?

      if test "$RESULT" = 0; then
        echo "[info] Done!"
        exit 0
      else
        echo "[\033[31merror\033[0m] Failed ($RESULT)"
        exit $RESULT
      fi
    else
      echo "[\033[31merror\033[0m] No RUNNING_PID file. Is this application running?"
      exit 1
    fi
  fi

※コードから分かるようにファイルパスが「RUNNING_PID」というべた書きのため、-Dpidfile.pathで別パス指定にしていると正常に動きません。

ここでちょっとした疑問が浮かびます。

「killで急に落としたらシステム不整合になりかねないんじゃ」

これに関しては、2.0.3を境に少し異なります。
play.core.server.NettyServerのcreateServerメソッドによってサーバーが立ち上がりますので、
ここのコードを見てみます。
まずは2.0.4から。

    try {
      val server = new NettyServer(
        new StaticApplication(applicationPath),
        Option(System.getProperty("http.port")).map(Integer.parseInt(_)).getOrElse(9000),
        Option(System.getProperty("http.address")).getOrElse("0.0.0.0"))

      Runtime.getRuntime.addShutdownHook(new Thread {
        override def run {
          server.stop()
        }
      })

      Some(server)
    } catch {
      case e => {
        println("Oops, cannot start the server.")
        e.printStackTrace()
        None
      }
    }

Runtime.getRuntime.addShutdownHookに処理が登録されています。
addShutdownHookはJVM終了時にフックさせる処理を追加するものです。
参考:FIO14-J. プログラムの終了時には適切なクリーンアップを行う
参考:Runtime (Java Platform SE 6)

ここでNettyServerのstopが呼ばれています。
stopメソッドではPlayのstopを呼んだりその他クローズ処理を行います。

なので、「addShutdownHookが動いてくれれば」大丈夫そうかなと思います。
ただしJavadocにあるように「保証はされない」のがちょっと心配。

まれなケースとして、仮想マシンが「異常終了」することがあります
(略)
たとえば、Unix の場合の SIGKILL シグナルまたは、Microsoft Windows の場合の TerminateProcess の呼び出しがその例です。
(略)
仮想マシンが異常終了するとシャットダウンフックの実行は保証できません。

簡単なコードで確認してみましたが、確かに「kill {pid}」で殺してもちゃんとフックが動いてました。
ただ「kill -KILL {pid}」ではフック動きませんでした。
まあ「play stop」はただの「kill {pid}」だから大丈夫。

そしてこのaddShutdownHookは2.0.3で追加されたものですので、2.0.2以前は対応していません。
以下は2.0.2のcreateServerの同じ処理部分です。

    try {
      Some(new NettyServer(
        new StaticApplication(applicationPath),
        Option(System.getProperty("http.port")).map(Integer.parseInt(_)).getOrElse(9000),
        Option(System.getProperty("http.address")).getOrElse("0.0.0.0")))
    } catch {
      case e => {
        println("Oops, cannot start the server.")
        e.printStackTrace()
        None
      }
    }

関連する話題や変更履歴はこの辺です。
Google グループ
merge fix Global.onStop is not called when netty exists · 3525058 · playframework/Play20 · GitHub