Scala + finagle + MongoDB + Herokuで2chクローンを作る(連載第3回)

11/3に、@xuwei_k さんの誕生祝 & Scalaハッカソン!に参加した。とりあえず、Hello Finagleを見ながら、Finagle製のWebサーバを立てることを目指す。まずは、静的なHTMLファイル返答するところまで作ろう。

むむむ…sbtの標準ディレクトリ構成がわからん。sbtのディレクトリ構成はMaven由来らしいので、Mavenのディレクトリ構成を見る。

どうやら、src/main/webappというディレクトリを掘ればいいらしい。ホントにこれでいいのかな。「sbt resources 」でGoogle検索。そのものずばりな、What are “resources” folders in SBT projects for?という質問を発見。そこのリンクから、さらにhow to get a resource within scalatest w/ sbtを見る。

どうやら、リソースファイルのパスprefixはgetClass.getResource()で取得するのがJavaでは普通で、Scalaでもそうするらしい。Javaに詳しくないことがバレてしまう。staticに配信するHTMLファイルはsrc/main/resourcesかsrc/main/webappかどちらに置いたほうがいいのか、よくわからなくなってきたぜ。とりあえずwebappに置くことにする。

finagleを用いたWebサーバのひな形(あきこ)がある。どうやら、Nettyが用意するHTTPハンドラを使っているらしい。このひな形をベースに、

  • ^/$, ^/css, ^/js -> staticなファイルを返す、mime-typeは拡張子からマッピングする
  • それ以外は、それぞれのcaseクラスにマッピングしてハンドリングする
みたいなことを書いていけばいいだろう。しかも、パスからのdispatchで、matchとか使えばScalaっぽくなるっしょ!?

まずは、HTTP Methodのハンドリングだな。NettyのHTTPRequestのメソッドであるgetMethod()でパターンマッチするのが常道か。「"getMethod match"」でGoogle検索。それでひっかかった、PunkというWebフレームワークがサイズ小さそうなので読んでみる。

ソースを読む上で、vimのsyntax highlightが欲しい。

sbaz install scala-tool-support
cp -R /usr/local/Cellar/scala/2.9.1/libexec/misc/scala-tool-support/vim/ ~/.vim
という風にしたら、syntax highlightがついた。いえー。

Punkでは、静的ファイルをどのように読み込み、クライアントに返しているのか。

ack File
でgrep。src/main/scala/punk/PunkFilter.scalaにreadFile()というメソッドがあるらしい。
    private def readFile(url: URL) = {
      scala.io.Source.fromURL(url).mkString
    }
うーむ。scala.io.Source.fromURL(url).mkStringってその場で文字列作るから、ファイルI/O待ってブロックするんじゃねーの? FinagleのイベントループにFile I/Oも載せられるような何かはないのかなー。初心者のクセに欲張りかもしらんけど。

と思ったらあったー、Finagleのドキュメント内、Using Future Pools

FinagleはFutureという型をよく使うが、一種の遅延評価みたいなもんだろう。上記のサンプルに、importを補うとこんな感じか。

import com.twitter.finagle.Service
import com.twitter.util.FuturePool
import java.util.concurrent.Executors

class ThriftFileReader extends Service[String, Array[Byte]] { val diskIoFuturePool = FuturePool(Executors.newFixedThreadPool(4))

def apply(path: String) = { val blockingOperation = { scala.Source.fromFile(path) // potential to block } // give this blockingOperation to the future pool to execute diskIoFuturePool(blockingOperation) // returns immediately while the future pool executes the operation on a different thread } }

このThriftFileReaderを実行してみると、型エラーが出た。fromFileはscala.io.BufferedSourceを返すが、FuturePool.apply()は、Array[Byte]を受け取る。scala.io.BufferedSourceからArray[Byte]に変換する方法を調べよう…と思ったけど、面倒なのでやめる。toArrayあたりでいいのかな?

とりあえず、同期でもいいからファイルをHTTP経由で返すところまでもっていこう。

response.setContent(copiedBuffer(scala.io.Source.fromFile("src/main/webapp/index.html").mkString, UTF_8))
うむ。ブラウザからアクセスしたところ、index.htmlが表示された。しかし、このcopiedBufferをはさむところがなんかダサいな。

responseはNettyのHttpResponseのインスタンス。setContentはバッファを受け取る。fromFile()はscala.io.BufferedSourceを返す。setContentが受け取るのはorg.jboss.netty.buffer.ChannelBufferインターフェース互換のもの。うまくやれば直接変換できそうな予感。そうすれば、FuturePoolでラップしなくても、実際にI/Oが必要となったときにFile -> Networkに直に転送してくれたりできそうな気配がする。けど、これもあまり深追いしないでおく。

pathによってきちんと返すファイルを変えよう。

response.setContent(copiedBuffer(scala.io.Source.fromFile("src/main/webapp" + request.getUri).mkString, UTF_8))
動作した。

上記のサンプルは、「/」へのアクセスでindex.htmlを返してくれない。また、ありがちな「../../../etc/passwd」的なものに弱い。前者について、HttpRequestのパスが「/」の場合には「/index.html」と解釈するようにしよう。後者について、パスを作ったあとパスの正規化をして、そのprefixをチェックする、っていうのが一般的な処理だろう。今回は、Scalaの正規表現を勉強したいので、とりあえず「..」という部分文字列があったら例外を投げるようにしてみる。

こんな感じでよかんべ。「..」にマッチする正規表現をソースコードに埋め込むのは面倒だ。最初は、"..“と書いたが、”“”..“”“とも書けるようだ。

  val tentenRegex = """..""".r
  val path = request.getUri

val filePath = "src/main/webapp/" + path match { case tentenRegex() => throw new Exception case "/" => "/index.html" case _ => path }

初match!どきどき…あれれ、動作しない。ルートにアクセスしても、/index.htmlの内容を返してくれない。なんでだろ?調べてみると、pathが「/」の際に、2個目のcaseにマッチしていないようだ。なんでだ。俺たちはまだ、青春知らずさ。

ためしにifでマッチするかどうか試してみるか。

  val path = request.getUri

if (path == "/") { println("match de-su") }

match de-suって表示された。ifだとうまくいくようだ。なんでだろう。matchへの理解不足だな。とりあえず、スタンドアロンのmatch検証プログラムを書いて動作させるか。道のりは遠い。

今回の分はコミットしていないですが、githubでソースコードを公開し始めましたニコニコ生放送でライブコーディング(という名のつぶやき放送)もやっております