2012年12月25日火曜日

ScalaでMMOのサーバを書くための技術

この記事は、Play or Scala Advent Calendar 2012の25日めです。

Looking back 2012

2012年は、

など、個人的にはPlayとScalaが身近な世界で躍進した年でした。そんな年にアドベントカレンダーの最終日を担当するというのは、何か感慨深いものがあります!

最近はPlayやScalaが実践で使われ始めた影響か、バイナリ互換性やビルドツール、習得面などの実践的な課題が色々と話題になっています。課題に対しては来年移行も粛々と対応をしていき、Scalaの今後の発展に寄与していきたいと思います

さて、本題に入ります。先日、AkkaでMMOのサーバ(ほんの小さなものですが)を書きました。その時に使ったScala関連の技術をいくつかご紹介したいと思います。(ゲームロジックの詳細は今回は省きます)

Why

そもそも何故いまMMOのサーバなのか!身も蓋もない言い方をすると、「前々から書いてみたかった」んです。

I love WebDev

軽く自己紹介をさせていただきますが、僕は新卒からWeb業界で働き初めてはや5年くらいになる20代Webエンジニアです。なぜWeb業界にいるのかというと、

  • Webのコモディティ化が著しい昨今、これからのエンジニアはWebができて当たり前、WebエンジニアリングはWebエンジニアにとってのそろばんなのではないか!
  • 流行りものだからやるなら今だろう!
  • お金になりそう!
  • 日々すごい技術が生まれては消えていき、さらには教育機関と連携したりしちゃって日々自然言語処理を活用した新しいサービスが生まれているのではないか!
などというキラッキラした期待があったからです。

5年くらい働いてみて、僕にとってのWebはこのキラッキラしたものとは無縁だなーというのが実感です。黙ってたらみんな古い技術使い続けるし、流行り過ぎてエンジニアの市場価値としてはどうなんだ、お金は基本的に儲からない、教育機関と連携した先端技術もなにそれ美味しいの状態だし研究開発は縮小する一方って感じですし。

それでも僕の考えるWeb開発やプログラミングが楽しく、それでいてエンジニアとしての今後の成長を視野にいれて、微力ながらScalaやPlayを普及する活動を公私にわたって行なってきました。Web業界を輝かせるために1エンジニアとしてできることを考えた結果が今、です。

I love GameDev

で、今回はMMOのサーバを書きました。これは、上記のWeb業界をエンジニアリングの立場から盛り上げたり底上げするみたいな大それた考えとは一切関係なく、僕のもっと個人的な趣味であり、夢です。

僕の子供時代といえば1990年代。ファミコンやスーパーファミコンが全盛期で、僕はマリオ、くにおくん、ドラクエ、FF、聖剣伝説に育てられたといっても過言ではありません。

そんな僕が小学生時代にPC98 CanBeとVisual Basic 2.0を与えられて、ゲームプログラマになりたがらないわけがないですよね?学校サボってDiabloとかLineageとかUltima Onlineやってたら、ネトゲ開発者になりたがらないわけがないですよね?でも諸々の理由でサラリーマンとしてゲームプログラマーになりたいとは思わなかったので、仕事以外の時間を使ってライフワークとしてやっていこうと思ってます。ネトゲをつくりたいなーとは以前から考えていたので、今回の試みはそのステップ1です。

最近色々な勉強会で同業の方々とお話する機会があるのですが、ソフトウェアエンジニアになったキッカケは「ゲーム!ゲーム作りたい!」だった、という人って結構います。そういった方にゲームづくりに使える技術がこんなところにあるんだよー、ということが伝わるといいなーという思いもあります。

Scala, Akka, STMs for hobbysts

また、アドベントカレンダー的には、「なんと趣味でネトゲのサーバをさっくりと書けるような技術が個人の手に入るようになっていました!それがScalaとAkkaとSTMでした!」という点もポイントです(・∀・)

Technology topics

今回のサーバアプリケーションですが、目標としたのは以下の3点です。

  • エレガントに。泥臭くスレッドプログラミングをせずに可用性、並列性が高く高速なネットワークアプリケーションを構築する
  • 安定性。マルチスレッド環境で安定して動くものにする(共有メモリの問題をちゃんと考える)
  • 開発速度。ゲームロジック以外のところは極力ありものを使って時間を節約する(アドベントカレンダーに間に合わせる)
スケーラビリティとかゲーム性とかは後で改善しやすいような設計にはしておくものの、今回は実現しなくてもOK。こんな目標設定の元、色々な技術を選定しました。トピックは以下のとおりです。

サーバはScala + Akka、クライアントはUnity 4 + C# 4で実装

通信にはTCP/IP、マーシャリングにはApache Thrift(のプロトコル部分のみ)を利用

ワールドの状態保持のため、ScalaSTMとAkka Agentによる共有メモリを利用

Akka IOとIterateeを利用した関数型のソケットプログラミング

今回はPlay・Scalaアドベントカレンダーなので、サーバをScalaとAkkaで実装するにあたってのポイントをいくつかご紹介したいと思います。

Marshallling messages between Scala and C#, with Apache Thrift

Apache Thriftは、複数のプログラミング言語をまたがるようなシステムや、いわゆるRPCを活用したアプリケーションを開発するためのフレームワークです。

Thriftにはサーバの機能も含まれているのですが、今回はサーバにAkkaとAkka IOを使ったTCP/IPをしゃべるサーバを自前で実装することにしたのでそれは使いません。単にメッセージをプログラミング言語間でやりとりするためにマーシャリングする手段として使います。

なお、マーシャリングに使えるシリアライズ方式としてGoogleのProtocol BufferやJSONも考えられましたが、今回はほどほどの速度と各言語向けのバインディングを公式に用意しているという点を両立しているのがThriftだったので、Thriftを採用しました。

Thriftではインタフェース定義を.thriftという拡張子のファイルに独自のDSLで記述します。この場合、インタフェース定義というのは、プログラミング言語をまたいで送受信するメッセージの形式を定めたものです。そして、.thriftファイルをThriftのジェネレータに渡すと、複数のプログラミング言語向けのコードを一括生成してくれます。今回はScalaとC#間でメッセージをやり取りしたかったので、うってつけですね。

例えば、今回はネットワークゲームへプレイヤーが参加するときのJoinメッセージは以下のように記述しました。

struct Join {
  1: string name,

}
structとあるように、Thriftでは独自の構造体を自由に定義できます。1: stringとありますが、これは一番目の要素がstring型であるということを意味しています。このように番号をつけておくことで、要素が削除されてもそれを欠番にすることで前方互換性を保つことができます。

なお、これをジェネレータにかけるとかなりやばい行数のソースが生成されるので、工数削減したった感が出ます!

これでインタフェース定義に基づいたDTOとプロトコルの実装が各言語向けに生成されるので、それを応用してマーシャリングを行います。なお、Scala向けの公式のジェネレータは存在しないので、今回はJava向けのジェネレータを使ってJavaソースを生成、それをScalaから呼び出すことにしました。こういう時にJavaとの親和性が高いのは助かりますね。

例えば、Scalaでは以下のようなコードになります。

アンマーシャリング(バイトデータ=>メッセージ)

import java.io.ByteArrayInputStream
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.transport.TIOStreamTransport

val bais = new ByteArrayInputStream(bytes)
val transport = new TIOStreamTransport(bais)
val protocol = new TBinaryProtocol(transport)

val join = new message.Join()
join.read(protocol)

マーシャリング(メッセージ=>バイトデータ)

val baos = new java.io.ByteArrayOutputStream()
val transport = new TIOStreamTransport(baos)
val protocol = new TBinaryProtocol(transport)

val join = new message.Join("mumoshu")
protocol.write(join)
baos.toByteArray

Writing a MMO server with Akka IO and Iteratee's

Akka IOは、低レベルのIO処理を詳細に気を取られずにAkkaと使って非同期IOを伴う計算を実装するためのモジュールです。内部的にはJava NIOをラップした機能が色々と揃っていて、ソケットにも対応しています。今回は手続き的なソケットプログラミングの代わりに、Akka IOを使って関数型プログラミング的に通信部分を書きました。

サーバのコードを要約すると、以下の様な感じになります。(実際のコードはもっと汚いので心してお読みください( ^ω^ ))

// サーバのインスタンス(Actor)を返す
def createServer(port: Int = 1234) = actor(new Act with ActorLogging {

  // ゲーム世界の状態を保持するAkka Agent(後述)
  // 状態を保持するために単にvarを使わないのは、synchronizedやロックによる排他制御を行わず、もっと安全で高速な実装にするためです。
  val world = Agent(new World)

  // IOを処理するIterateeを保持するMapです。今回は1ソケットにつき1つのIterateeが割り当てられます
  val state = IO.IterateeRef.Map.async[IO.Handle]()(context.dispatcher)

  val address = new InetSocketAddress(port)
  
  // IOの詳細はAkkaのIOManagerが見てくれます
  val socket = IOManager(context.system) listen address

  // クライアントから送信されてきたバイト列を大まかに分割します。
  // 今回は「フレーム」という単位でメッセージが梱包されたものがバイトデータにマーシャリングされて送られてくるものとします。
  // フレームには、ヘッダとボディがあり、ヘッダにはボディのバイト長、ボディにはThriftでマーシャリングされたメッセージのバイトデータが含まれます。
  // ここでは、バイト列をフレームに戻してあげます
  val FrameDecoder: IO.Iteratee[ByteString] = for {
    bodyLenBytes <- IO.take(4) // まずヘッダを読み取り
    bodyLen = bodyLenBytes.iterator.getInt // ボディの長さを計算
    bodyBytes <- IO.take(bodyLen) // 長さのぶんだけバイトデータを読み取りボディとする
  } yield {
    bodyBytes // ボディを返す
  }

  // お待ちかねのIterateeです。
  // クライアントから送信されてきたバイトデータをとにかくこのIterateeに食わせます。
  // すると、上記のFrameDecoderによりフレームのボディ部分だけが1つずつ返ってくるので、ボディをThriftでアンマーシャリングして処理します
  def processBytes(handle: IO.SocketHandle): IO.Iteratee[Unit] = {
    IO repeat {
      for {
        bodyBytes <- FrameDecoder
      } yield {
        processBodyBytes(handle, bodyBytes)
      }
    }
  }

  // IO.SocketHandleはソケットの参照で、接続先を識別するIDを得たり、バイトデータを読み書きするために使えます。
  // bytesは読み込んだチャンクです。1チャンクにシリアライズされた1個以上のメッセージが含まれます。
  def processBodyBytes(handle: IO.SocketHandle, bodyBytes: ByteString) {
      val id = handleToId(handle)
      val message = deserialize(bodyBytes)
      atomic { txn =>
        message foreach {
          // 新規プレイヤーの参加
          // SEND(クライアントから一方的に通知するだけ) の通信例です。
          case m: message.Join =>
            // ゲームに参加中のプレイヤーが増える、つまりゲーム世界の状態が書き換わります
            world send {
              _.join(new Player(id = id, name = m.name))
            }
            // 接続中の他のプレイヤーに、新規プレイヤー参加を通知します(詳細は省略)
            publish(m)
          // 任意のプレイヤーの位置確認を行うためのメッセージ。
          // REQUEST(クライアントが要求して)・RESPONSE(サーバが返答する)の通信例です。
          case m: message.GetPosition =>
            // まず要求を理解して
            val targetId = strToId(m.id)
            world.get().getPositionById(targetId) foreach { p =>
              // 返答をバイト列にマーシャリングしてソケットに書き込みます
              val rep: thrift.Position = new serializers.thrift.Position(targetId.str, p.x.toFloat, p.z.toFloat)
              val repBytes = protocol.serialize(rep)
              handle.asWritable.write(FrameEncoder(repBytes))
            }
        }
      }
    }

   // ここまでが通信部分の実装でした。バックエンドがTCP/IPであることを意識させないコードになっていたと思います。

   // ここからActorの振る舞いを記述します。
   become {
     // 新しい接続要求がきたら、それを受け付けます。
     case IO.NewClient(server) ⇒
       val socket = server.accept()
       socket.write(FrameEncoder(ByteString("You are connected!")))
       // このクライアントとの通信は先ほど定義したprocessBytes Iterateeが担当します
       state(socket) flatMap (_ => processBytes(socket))
     // クライアントからバイトデータのチャンクが送られてきたら
     case IO.Read(handle, bytes) ⇒
       // Iterateeに食わせます。後のことはIterateeさんにまかせた。
       state(handle)(IO Chunk bytes)
     // ソケットが閉じたら
     case IO.Closed(socket, cause) =>
       log.debug("Socket closed: " + cause)
       // Iterateeを止めます
       state(socket)(IO EOF)
       state -= socket
     case unexpected =>
       log.debug("Unexpected message: " + unexpected)
   }

} // おつかれさまでした

What is ScalaSTM

STMとはSoftware Transactional Memoryの略で、その名の通りミドルウェアの機能に頼らず言語側で共有メモリに対するアトミックな操作を可能とするための技術です。Scalaでは今回使ったScalaSTMが有名です。AkkaのSTM絡みの機能もScalaSTMを使って実現されていたります。

アトミックな操作を実現するためには、共有メモリへの変更が競合するときにロールバックする、という考え方が必要になります。ScalaSTMでは、共有メモリとなる変数を独自のコンテナでラップして、そのコンテナの操作を記録、競合が発生しない場合にのみ記録した操作をコミットする、というアイデアが使われているようです。(ツッコミお待ちしてます!)

ScalaSTMは他のライブラリに一切依存しない、単一のJARからなるライブラリです。

ScalaSTMには以下の機能があります。

  1. 複数のSTM実装をサポートするような統一的なAPI
  2. CCSTMをベースとしたSTMのリファレンス実装
  3. スケーラビリティと並列性に優れたSetとMap(高速なスナップショット機能を含む)
  4. Refというデータ構造を用意。イミュータブルなオブジェクトとRefを利用すると、
    • 複数スレッドやアクターからアクセス可能な共有メモリを
    • synchronizedなし
    • デッドロックやレースコンディションなし
    • いい感じのスケーラビリティ
    で実現できます。

また、JavaのwaitやnotifyAllより安全な代替え機能が提供されています。

ScalaSTM公式サイトより抜粋・翻訳

使い方はこんな感じです。
import scala.concurrent.stm._

val count = Ref(0)

atomic { implicit txn =>
  // read
  val c = count()
  // write
  count() = c + 1
}

Refでラップした共有メモリを、atomicブロック内で読み書きするだけというお手軽さです。

もう少し詳しいコード例は公式のSyntax Cheat Sheetを参照してください。

Reactive, asynchronous shared memory manipulation with Akka Agent

AgentはScalaSTMをベースに、共有メモリに非同期処理の機能を持たせたものです。

実用上は、Ref同様にアトミックな読み書きができるだけでなく、書き込みをキューイングさせて非同期的に処理したり、書き込みを待ってから読み込みするといった機能があります。トランザクション中にメインスレッドで行いたくないような重い計算やIOを行う場合はAgentを使うといいしょう。

使い方もScalaSTMとそうかわりなく、
import akka.actor.ActorSystem
import akka.agent.Agent

implicit val system = ActorSystem("app")

val counter = Agent(0)

// 読み書き
counter send { _ + 1 }

//  読み
val c = count()

sendで関数として表現されたアトミックな操作をAgentに送る。送った後はAgentがそれを処理するのに任せる、という考え方です。

読み込みはRefと同じですね。

なお、STMはトランザクション外での書き込みができませんが、Agentはトランザクション外でも書き込み可能です。atomicブロック外なら暗黙的にトランザクションを開始し、内であれば既にあるトランザクションに参加してくれるだけです。一度に一箇所の共有メモリをatomicに操作できればいいだけであれば、Agentのほうが手軽です。また、非同期処理の機能はAkkaと非常に相性がいいですね。

Conclusion

Scala + AkkaでMMOのサーバを記述するための技術要素を抜粋して解説しました。Scala、Akka、Thriftなどの既存技術を組み合わせることで、意外と組めてしまいそうな感じが伝わったでしょうか?

また、今回解説してみて気づきましたが、このレベルであれば、MMOのサーバに限らずリアルタイムなネットワークアプリケーションであれば普通に必要な機能ですね。今後リアルタイムWebが来るのであれば、Web業界でこれら技術が普通に使われる日も来るかもしれません。

この記事ではカバーできていませんが、実サービスになるとさらに、

  • シャーディング(Ultima Onlineの時代から有名な、エリア毎にマップを分ける方式)
  • クラスタリング(ゲーム世界の各部分を複数のノードが合同で受け持ち、可用性を高くする)
  • ゲームデータの保存の問題(どのタイミングで、何を保存するか)
といった面白い技術的課題も出てきます。 シャーディングやクラスタリングに関しては、Akka 2.1から追加されたAkka Clusterが使えるかもしれないので最近気になってます。ゲームデータの保存については、DQ10がOracleでぶん回しているという話がありましたが、スケールアップではなくスケールアウトを前提にしたDBを使うとどうなるのか考えてみるのも面白そうです。

以上、Play or Scala Advent Calendar 2012、25日めでした。

Thank you

最後に。本年はPlay、Scalaについて特に日本で大きな動きがあった一年だった思います。これも全国でPlay、Scalaのコミュニティを運営されている皆さん、勉強会を開催・参加している皆さん、twitter上で毎日のようにPlayについて熱い議論を交わしている皆さんをはじめ、日本の素晴らしいPlay or Scalaユーザの皆さんのお陰だと思います。

日本のPlay・Scalaユーザの皆さんに、
PlayやScalaを日々利用している一エンジニアとして、心より感謝申し上げます。

本年はお世話になりました!
来年もよろしくお願いいたします!!

P.S. 本当はデモを公開したかったのですが、Unity4にはまって間に合いそうにないので後日(´・ω:;.:...

ソースはこちら

2011年12月17日土曜日

Play 2.0 RC1 でControllerのテストを書く

この記事はPlay! framework Advent Calendar 2011 jpの17日目です。

来年の1月頃のリリースに向けて活発な開発が続いているPlay 2.0ですが、コミットログを追っていると、最近テスト関係の変更が少ないのでそろそろfixかと思い、勉強ついでにControllerのテストを書いてみました。

一応、Controllerに対するテストの書き方はPlay 2.0 RC1の公式ドキュメントにも記載されています。
https://github.com/playframework/Play20/wiki/Scalatest

テストはspecs2による記述が推奨されているようです。
具体的には、specs2にPlayのヘルパーメソッドを併用します。公式ドキュメントから抜粋したコードが以下です。

object ApplicationSpec extends Specification {

  "an Application" should {
    "execute index" in {
        withApplication(Nil, MockData.dataSource) {
          val action = controllers.Application.index
          val result = action.apply(new FakeRequest)
          ...
        }
     }
   }
}

コントローラごとに*Specを定義します。そして、
"テスト対象" should {
  "期待する振る舞い" in {
    テストコード
  }
}
という形で仕様を記述していきます。

withApplicationはアプリケーションのモックを作成して、そのモック上でコントローラなどのコンポーネントを動かしてくれます。Playに依存したコードのテストは、基本的にはwithApplicationを使った方が楽だと思われます。

MockData.dataSourceというのはただのMap[String, String]で、application.confの代わりです。次のように定義されています。
/**
 * provides standard mock data
 */
object MockData {
  val dataSource = Map("application.name" -> "mock app",
    "db.default.driver" -> "org.h2.Driver",
    "db.default.url" -> "jdbc:h2:mem:play",
    "ebean.default" -> "models.*",
    "db.default.disableJMX" -> "true",
    "mock" -> "true")
  def dataSourceAsJava = dataSource.asJava

}
デフォルト設定では、テストにインメモリデータベースを使うことがわかります。

controllers.Application.indexは、アクションを取得しています。アクションを実行しているわけではないことに注意してください。Play 1系まではコントローラのメソッドがアクションそのものでしたが、2.0のコントローラのメソッドはアクションを返すだけです。
コントローラが返したアクションをフレームワークが実際には実行するわけですが、テストにおいては自分でアクションにHTTPリクエストなどを渡して、アクションを実行する必要があります。
それが、
val result = action.apply(new FakeRequest)
の部分です。

さて、以上で公式ドキュメントの内容はおさらいできましたが、実際のテストを書くために知っておきたいことがいくつか足りていません。
例えば、リクエストメソッドやリクエストパラメータの指定、データベースの初期データの投入方法など、肝心なところが説明されていません。

というわけで、今回はPlay 2.0のzentasksサンプルの一部にテストを書いてみました。
テスト対象はApplicationコントローラ。zentasksへのログイン・ログアウトに関するアクションが定義されています。routesファイルからは次のようにマッピングされています。

# Authentication
GET     /login                              controllers.Application.login
POST    /login                              controllers.Application.authenticate
GET     /logout                             controllers.Application.logout

今回は、login, authenticate, logoutの3つのアクションの仕様を確認しつつ、テストフレームワークに足りない部分を実装しつつ、specs2の記述に落とし込んでみました。
コードはGitHubで公開してあります。

test/ApplicationSpec.scala at master from mumoshu/play20-zentasks-tests - GitHub

個別に説明していきます。まずは、loginアクションのspecを見てみてください。

"an Application" should {

    /**
     * GET /login へアクセスすると、ログインフォームが表示される。
     */
    "show login page" in {
      withEvolutions {
        withApplication(Nil, MockData.dataSource) {
          // Controllerに対するspecは、
          // 1. requestをテスト対象のactionへ渡して、resultを得る
          // 2. resultからレスポンス内容をextractする
          // 3. レスポンス内容をspecs2のMatcherで検証する
          // という流れになります。
          val action = controllers.Application.login
          val result = action.apply(FakeRequest("GET", "/login", body = AnyContentAsEmpty))
          val extracted = Extract.from(result)
          // ステータスコード
          extracted._1 must equalTo (200)
          // レスポンスヘッダ
          extracted._2.toString mustEqual ("Map(Content-Type -> text/html; charset=utf-8)")
          // レスポンスボディ
          extracted._3 must contain ("/login")
        }
      }
    }

公式ドキュメントに書いてない部分は、withEvolutionsとFakeRequest、Extractの3つですね。

まず、withEvolutionsについて説明します。そもそも、Scala版のPlayにはevolutionsという機能があります。データベースが最新の状態でないときにエラー画面でDDLの実行を促すというものです。
この便利機能が、今回は罠になりました。なんと、withApplicationでモックアプリケーションを起動するときに、evolutionsにより「データベースが最新の状態ではない」ということで例外を投げて、テストが必ず失敗してしまうのです・・・。

というわけで、テスト中にevolutionsの例外をcatchしたら、DDLを実行してから再度テストを走らせるという力技をかけました。それがwithEvolutionsです。コードが次の通りです。

private def withEvolutions[T](spec: => T): T = {
    try {
      spec
    } catch {
      case e: InvalidDatabaseRevision => {
        play.api.Logger.info("Applying all evolution scripts.")
        applyAllEvolutionScripts()
        spec
      }
    }
  }

specがwithApplicationから始まるテスト本体です。InvalidDatabaseRevisionというevolutionsの例外発生時にapplyAllEvolutionScripts()により全てのDDLを適用しています。コードは次の通りです。

private def applyAllEvolutionScripts() {
    val app = play.api.Play.current
    val api = app.plugin[DBPlugin].map(_.api).getOrElse(throw new Exception("there should be a database plugin registered at this point but looks like it's not available, so evolution won't work. Please make sure you register a db plugin properly"))
    val db = "default"
    val appPath = new File(".")
    val scripts = Evolutions.evolutionScript(api, appPath, db)
    Evolutions.applyScript(api, db, scripts)
  }

Playに組み込まれているEvolutionsPluginが行っているのと同じ方法で、"default"という名前のデータベースに必要な全てのDDLを取得・実行しています。

気づかれた方もいるかもしれませんが、正直なところ、なぜこれで動くのかわかっていない所があります・・・。本来、withApplicationでアプリケーションが作り直されるたびにインメモリデータベースも初期化されるべきだと思うのですが、どうやらされていないようです。というより、されていないから、evolutions実行後にwithApplicationをもう一度実行すると、evolutionがエラーを出さないのだと思います。@kitora_naokiさんの記事にもあるようにh2dbを使った方が良いのかもしれません。

さて、3つめのFakeRequestですが、これも今回独自に実装した、テスト用のリクエストを簡単に作成するためのクラスです。Play本体にあって欲しいところ・・・。
実装については特筆すべきところはないので説明は省いて、使い方だけ書きます。

// クエリパラメータなしのGETリクエスト
FakeRequest("GET", "/login", body = AnyContentAsEmpty)

// クエリパラメータありのGETリクエスト
FakeRequest("GET", "/login", params = Map("key" -> Seq("value")))

// POSTリクエスト
FakeRequest("POST", "/login", body = AnyContentAsUrlFormEncoded(Map(
  "email" -> Seq("mumoshu@sample.com"),
  "password" -> Seq("secret")
)))

3つめのExtractについて説明します。ExtractはPlay組み込みのクラスです。
Extract.from(result)
は、resultから
  • HTTPステータスコード
  • レスポンスヘッダ
  • レスポンスボディ
を抽出して、Tuple3で返します。

基本的にアクションのテストは、このようにresultからExtract.fromで上記3つを取り出し、specs2のmatcherで検証すれば良いでしょう。

最後に、データベースにテスト用データを投入する方法です。withApplication内で普通にModelを呼び出してinsertしましょう。
withEvolutions {
        withApplication(Nil, config) {

          User.create(User("mumoshu@sample.com", "mumoshu", "secret"))

          val action = controllers.Application.authenticate
          // つづく...

まとめ


Play 2.0 RC1でのControllerに対するテストの書き方を、公式ドキュメントより少し踏み込んで説明しました。
個人的にここで紹介した以外にも、
  • Web APIを呼び出すアクションのテスト。APIをモックする?
  • HTTP Chunkingに対応したアクションのテスト。チャンクを一つずつ取得して検証する?
  • Web Socketコントローラのテスト。
など、どうやってテストするのか気になるところが残っていますが、それはまた別の機会に。

明日はPlay! framework Advent Calendar 2011 jp 18日目、@garbagetownさんのご担当です!
よろしくお願いします。

2011年8月6日土曜日

Play framework + WebSocket + Growlで遊ぶ

第1回「Play framework勉強会」IN 関西@tan_go238さんが、NettyとGrowlを組み合わせて、指定ポートのソケットに書き込まれたメッセージをGrowlに飛ばすアプリを披露されていました。
それにインスパイアされて、Play + WebSocket + Growlを組あわせて、HTTPやWebSocket経由で送信されたメッセージをサーバのGrowlや、他のWebSocket接続ユーザへ飛ばすアプリを書いてみました。

ソースコードはこちらで公開しています。
本文には特に重要なコードしか掲載していませんので、ざっと記事を読んでいただいたあと、全体のコードを眺めて復習してみてください。
mumoshu/play-websocket-growl-sample at master - GitHub

GrowlやWebSocketでメッセージを送信する


public class Notifier {
    /* 略 */

    public static void notify(String message) {
        Logger.info("Notify: %s", message);
        growl().notify("system", "Play feat. Growl", message);
        NotificationStream.publish(message);
    }
}

HTTPやWebSocketから送信されたメッセージは、Notifierというクラス経由で他のWebSocketクライアントやサーバのGrowlへ飛ばします。
このコードでは、
growl().notify(...)
でサーバへGrowlによる通知を送信しつつ、
NotificationStream.publish(message)
により接続中のWebSocketクライアントへメッセージを送信します。

これを、HTTP POSTされたときやWebSocketクライアントからのメッセージを受信した際に呼び出します。

HTTP経由


HTTPやWebSocketからはそれぞれこんな感じで受け取ったメッセージをNotifierに渡します。

public class Application extends Controller {
 
    public static void growlHTTP() {
        render();
    }

    public static void growlWS() {
        render();
    }

    public static void notifyWithGrowl(String message) {
        Notifier.notify(message);
    }
}

growlHTTPがHTTP経由で、growlWSがWebSocket経由でメッセージをサーバへ送信するページを表示するアクションです。
HTTP経由のメッセージ送信は、notifyWithGrowlへHTTP POSTを送信することで行います。

WebSocket経由


WebSocketクライアントからのメッセージ送受信はこのように行います。

public class Notifications extends Controller {
    public static void index() {
        render();
    }

    public static class WebSocket extends WebSocketController {

        public static void connect() {
            Logger.info("Connected.");

            F.EventStream eventStream = NotificationStream.stream();

            while(inbound.isOpen()) {
                F.Either receivedEvent = await(F.Promise.waitEither(
                        inbound.nextEvent(),
                        // 以下のようにするとNotificationが無限に返ってきてアプリが応答しなくなります。
                        // F.EventStream eventStream = NotificationStream.stream();
                        eventStream.nextEvent()
                ));

                // WebSocketから受け取ったメッセージをGrowlや他のWebSocketコネクションにブロードキャストする。
                for (String message : TextFrame.match(receivedEvent._1)) {
                    Notifier.notify(message);
                    Logger.info("Message received: %s", message);
                }

                // 他のWebSocket接続から受け取ったメッセージを、現在のWebSocket接続先に送る。
                for (Notification notification : ClassOf(Notification.class).match(receivedEvent._2)) {
                    Logger.info("Matched to Notification class.");
                    outbound.send(notification.message);
                }

                for (Http.WebSocketClose close : SocketClosed.match(receivedEvent._1)) {
                    disconnect();
                }
            }
        }
    }
}

この例ではEventStreamは、PlayアプリからWebSocketクライアントと間接的にメッセージ交換をするためのパイプの役割を持ちます。
Playアプリと全WebSocketクライアントが通信するための「バス」もしくは「ハブ」とも言えます。

通信とデータの流れとしてはこうです。

Application.notifyWithGrowl()のリクエスト
または
Notifications.WebSocket.connect()でWebSocketのメッセージ受信
↓
Notifier.notify(message)
↓
ArchivedEventStream#publish(new Notification(message))
↓
Notificationが「接続中の全WebSocketクライアントの」
Notifications.WebSocket.connect()におけるeventStream.nextEvent()から返ってくる。

あるHTTP/WebSocketクライアントのリクエストやジョブから、別のコネクションを貼っているWebSocketクライアントにメッセージを送る場合、EventStreamを経由する以外に方法はありません。

まとめ

  • HTTPやWebSocket経由でメッセージを受信しました
  • 受信したメッセージをサーバへGrowl通知しつつ、他のWebSocketクライアントへ送信しました。
基本的には誰得でございますが、
スライド発表中にこういうアプリでリアルタイムにコメントをもらうと面白いかもしれませんw

参考


Growl notifications in Java with script engine and AppleScript | Jayway Team Blog - Sharing Experience

Play frameworkのchatサンプル

2011年7月25日月曜日

Scala で Functional Dependency

Functional Dependencies in Scala
の自分なりの解釈をメモ。

Functional Dependencyとは?


Haskellで標準的に使われているパターンらしい。

Functional dependencies are used to constrain the parameters of type classes. They let you state that in a multi-parameter type class, one of the parameters can be determined from the others, so that the parameter determined by the others can, for example, be the return type but none of the argument types of some of the methods.

Quoted from: Functional Dependencies

Functional Dependencyは、型クラスのパラメータに制約をかけるために利用します。
例えば、複数の型パラメータを持つ型クラスについて、あるパラメータによって別のパラメータが決まる、という制約を表現することができます。
これを応用してメソッドを定義すると、メソッドの引数に無い型を戻り型にする、ということができます。

Java屋さん的に言えば、型クラスを、型安全にDependency Injectionする仕組みです。

Scalaでのコード例


Functoinal Dependencyは長いので、ここではfundepと略して説明します。

// 型クラスのパラメータとなる型
trait Matrix
trait Vector

// fundepの説明における"型クラスのパラメータに制約をかける"部分に対応します.
// MultiDepという型クラスのパタメータA, B, Cに制約をかけます.
trait MultDep[A, B, C]
// trait MultDep[-A, -B, +C]    // for Scala <= 2.9.0-1 these variance
//                              // annotations are required

// fundepの説明における
//  "複数の型パラメータを持つ型クラスについて、あるパラメータによって別のパラメータが決まる、とう制約を表現"
// しているのがここです.
//
// パラメータA, Bによって, 別のパラメータCが決まる, という制約を表現していると考えてください.
//
// 逆に言えば, ここで記述されていないパラメータの組み合わせは許さないのでコンパイルエラーにしたい, ということになります.

// A, BがMatrixなら, CはMatrixである
implicit object mmm extends MultDep[Matrix, Matrix, Matrix]
// AがMatrix, BがVectorなら、CはVectorである
implicit object mvv extends MultDep[Matrix, Vector, Vector]
// 以下、同様に。
implicit object mim extends MultDep[Matrix, Int, Matrix]
implicit object imm extends MultDep[Int, Matrix, Matrix]

// 利用例

implicitly[MultDep[Matrix, Matrix, Matrix]]
// ABCが制約を満たしていれば何らかのオブジェクトが返ってくる。
// 満たしていなければコンパイルエラー

// fundepの説明 "メソッドの引数に無い型を戻り型にする" に対応する例が以下です.
// メソッドの引数A, B以外の型Cを戻り型に指定しています.
// A, B, Cの組み合わせが上記のimplicit objectで表現されていないとコンパイルエラーになるわけです.
def mult[A, B, C](a : A, b : B)(implicit instance : MultDep[A, B, C]) : C = /** instanceのメソッドを呼び出すコード */

// これはコンパイル成功する
val r1 : Matrix = mult(new Matrix {}, new Matrix{})
val r2 : Vector = mult(new Matrix {}, new Vector{})
val r3 : Matrix = mult(new Matrix {}, 2)
val r4 : Matrix = mult(2, new Matrix {})

// これはコンパイル失敗する
// implicit object mvm extends MultDep[Matrix, Vector, Matrix]
// のようなimplicit objectが定義されていないので,
// 「MultiDep[Matrix, Vector, Matrix]のようなimplicit objectが見つかりません」というコンパイルエラーが出力される.
val r5 : Matrix = mult(new Matrix {}, new Vector{})

fundepの適用範囲

Functional Dependecyを利用すると、関数に対して、複数のパラメータを持つ型クラスを型安全にDIできるわけですが、実際にどういった実装に応用していくか?
関数の型パラメータによって処理が異なったり、型パラメータの組み合わせに制約がある場合に応用するとよさそうです。

関数の型パラメータの制約を、関数内の条件分岐として実装して、実行時に制約違反を発見するか、もしくはコンパイル時にコンパイルエラーとして出力するか?
コンパイル時にコンパイルエラーとして出力するほうが良いですよね。
Functional dependecyが使えます。

関数の型パラメータの組み合わせによる処理の違いを、関数内に実装するか、関数外の型クラスのinsntanceどちらに実装すると見通しや拡張性が良いか?
型クラスのinstanceですよね。
ここでもFunctional dependecyが使えるということです。

まとめ

  • Functional DependecyはHaskellで標準的に使われるパターン
  • 型クラスの型パラメータに制約をかけることができる
  • 関数の型パラメータによって処理が異なるとき、異なる部分を型クラスのインスタンスのメソッドとして抜き出すことで、コードの見通しを良くできる

参考


Thanks to:

Blogger でSyntax Highlighterを使う

Syntax Highlighter v3を Blogger で使いたかったので、以下のサイトにあるジェネレータを利用させていただきました。

How to Add Syntax Highlighter(v3) to Blogger Blogs ~ Blogger Widgets | Tips | Trick | Hacks | Help! ~ Way2Blogging

テーマと対応する言語をチェックボックスで選んで、「Get Code」ボタンを押し、表示されたHTMLコードをコピーしてください。
それから、Bloggerのテンプレートのhead閉じタグの直前にペーストするだけで設定完了です。
後は、投稿の編集中に、



を記述すれば利用できます。