Minillaで複数のテストスイートを実行するようにする

今ちまちま書いてるRedisismというRedis向けのフレームワークでは、Redisとのコネクション周りとかクエリ投げたりするところのハンドリングを自前では持っていなくてほとんど同じインターフェースなので、RedisDBあるいはRedisに任せている。

そうなると、Redisあるいは、RedisDBの両方で同じテストスイートを実行するようにしたくなるので、Text::XslateのAnyMoose対応を参考にModule::Install::Targetを使ってやっていたのだけど、Minillaへ置きかえてみた

Minillaだと、minil.tomlに

[build]
build_class = "builder::MyBuilder"

を追加して、自前でModule::Buildを拡張したものを書く、という感じにできる。

Creating working directory: /Users/yoshimin/project/perl/Redisism/.build/rHpXPchM
Detecting project name from directory name.
Retrieving meta data from lib/Redisism.pm.
Name: Redisism
Abstract: framework for Redis based application.
Version: 0.01
Writing MANIFEST file
Writing release tests: xt/minimum_version.t
Writing release tests: xt/cpan_meta.t
Writing release tests: xt/pod.t
Writing release tests: xt/spelling.t
$ perl -I/Users/yoshimin/project/perl/Redisism/.build/rHpXPchM Build.PL
Created MYMETA.yml and MYMETA.json
Creating new 'Build' script for 'Redisism' version '0.01'
Merging cpanfile prereqs to MYMETA.yml
Merging cpanfile prereqs to MYMETA.json
$ perl -I/Users/yoshimin/project/perl/Redisism/.build/rHpXPchM Build build
Building Redisism
$ perl -I/Users/yoshimin/project/perl/Redisism/.build/rHpXPchM Build test
Testing: $ENV{REDIS_CLIENT} = "Redis"
t/000_load.t .......... ok
t/001_basic.t ......... ok
t/011_factory.t ....... ok
t/020_list.t .......... ok
t/030_set.t ........... ok
t/040_sorted_set.t .... ok
t/050_hash.t .......... ok
xt/cpan_meta.t ........ ok
xt/minimum_version.t .. ok
xt/pod.t .............. ok
xt/podcoverage.t ...... ok
xt/podsynopsis.t ...... ok
xt/spelling.t ......... Possible attempt to separate words with commas at xt/spelling.t line 12.
xt/spelling.t ......... skipped: no ~/.spellunker.en
All tests successful.
Files=13, Tests=57,  6 wallclock secs ( 0.10 usr  0.05 sys +  4.84 cusr  0.44 csys =  5.43 CPU)
Result: PASS
Testing: $ENV{REDIS_CLIENT} = "RedisDB"
t/000_load.t .......... ok
t/001_basic.t ......... ok
t/011_factory.t ....... ok
t/020_list.t .......... ok
t/030_set.t ........... ok
t/040_sorted_set.t .... ok
t/050_hash.t .......... ok
xt/cpan_meta.t ........ ok
xt/minimum_version.t .. ok
xt/pod.t .............. ok
xt/podcoverage.t ...... ok
xt/podsynopsis.t ...... ok
xt/spelling.t ......... Possible attempt to separate words with commas at xt/spelling.t line 12.
xt/spelling.t ......... skipped: no ~/.spellunker.en
All tests successful.
Files=13, Tests=57,  6 wallclock secs ( 0.07 usr  0.04 sys +  4.77 cusr  0.41 csys =  5.29 CPU)
Result: PASS

今の時点だと、別々に実行する必要のないところが2回走ってしまう(xt/とか)のがちょっとアレではあるけど、とりあえず良いということにした。

Module::Buildは色々覚えること多そうで移行するのめんどくさいなぁと思っていてまだModule::Install使っていたのだけど、なかなかMinillaは使いやすいし個人的にはもう移行してもよいかなという気になってきた。Module::Buildの拡張もいざやってみると拡張しやすくて使いやすい。Minilla本体ではまだ色々変更が入っているので、こういうニッチな部分はまだまだ仕様が代わったりしそうではある。

ActiveRecordのSQLの実行箇所をSQLのコメントに入れる

arproxyを使うと、SQLにフックして色々書きかえることができて非常に便利ですね。

module Arproxy
  class QueryLocationCommentAppender < Arproxy::Base

    WHITE_LIST_WORD_RE = %r{^[a-zA-Z0-9\-_/:\?]+$}

    def execute(sql, name=nil)
      if ENV["ARPROXY_QUERY_LOCATION"]
        # SQL Injection対策
        # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if ENV["ARPROXY_QUERY_LOCATION"].index("*/").nil?
          # 制御文字や、マルチバイトの文字を防ぐため、文字種を制限する
          if ENV["ARPROXY_QUERY_LOCATION"] =~ WHITE_LIST_WORD_RE
            substr = ENV["ARPROXY_QUERY_LOCATION"]
            substr = substr.size >= 100 ? substr.slice(0, 100) : substr
            sql += " /* called from \"#{substr}\" */ "
          end
        end
      end
      super(sql, name)
    end
  end
end

Arproxy.configure do |config|
  config.adapter = "mysql2"
  config.use Arproxy::QueryLocationCommentAppender
end

Arproxy.enable!

みたいなのをconfig/initializers/arproxy.rbに書いておいて、ApplicationControllerのbefore_filterあたりにでも、

  def set_query_comment_url
    ENV["ARPROXY_QUERY_LOCATION"] = request.path_info
  end

こんなのをしこんでおけば、実行したページのURL情報がわかるので、mysqlのslow queryのログなどを見て、どのURLのクエリが重たいのか判別できる。
(なおSQL Injection対策はmysql限定(それ以外は調べてないので、注意))

情報の受けわたしに環境変数をつかってしまったが、グローバル変数でよい気もする。

また、開発時には、URLではなくて、どのmodelや、controller、viewから発行されているか知りたくなるので、

    def execute(sql, name=nil)
      stack_trace = caller.select {|i| i.inspect =~ %r{app/(models|controllers|view)} }.join("\t")
      if stack_trace
      # SQL Injection対策
      # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if stack_trace.index("*/").nil?
          substr = stack_trace
          sql += " /* called from \"#{substr}\" */ "
        end
      end
      super(sql, name)
    end

こんな感じにしてdevelopment.logを見つつ、発行元を確認するとよい。(だいぶ見づらいけど。

arproxy++

isucon #2へ参加してきました

前回非常に楽しかったので、今回も参加したいなーと思っていたのですが、「くらげとみかん」チームで、なんとか参加できました。

走り書きだけど、考えたこと、やったことをメモっておく(あとでまとめなおすかもしれない)

まずは最初に全サーバーへsshの鍵を通して、screenで全サーバーへログインした。
前回ベンチマークを走らせるのが遅かった、という反省があったので、まずベンチマークを実行した。

まずappをいじるための環境を整備した。git reposを作ったり。(11:30)

次は、アプリケーションの概要を把握するために、全tableのschemaをshow create tableしたり、全テーブルの件数を把握して、stockテーブル以外は対した容量にならない、ということを把握した。

long_query_timeを0にして、ベンチを走らせて、全クエリをslowログへ出力し、mysqldumpslowで解析した。(この時点でpt-query-digest使っておけばよかった...)

前回は、ボトルネック以外のところへ時間を消費してしまった、という反省があったので、stockテーブルへ注力してチューニングを始めた。

チケットの席を発行する処理がぱっとみ遅そうだ、ということで、mysqldのINSERTまわりの設定をチューニング。

具体的には、

  • innodb_flush_log_at_trx_commitを0に。
  • log-binを無効に

してみると少しスコアがあがった。(通常のwebサービスでは真似しないように)

alter table stock change column seat_id seat_id varchar(6) ascii binary not null;

してseat_idのデータを減らす。

deployスクリプトをかいてなかったので、appをいじるためにdeployスクリプトを書く ( 13:00ごろ)

"order by rand"しているクエリがあったので、とりあえずオフにしてベンチしてみたら通ったので、そのままにした。

alter table stock ROW_FORMAT=Compressed

してみたら少しあがった。

mysqldumpslowが見づらいので、やっぱりpt-query-digestを落としてきて実行する。

stockテーブルのupdated_atは、最後のチケットCSVをダウンロードしているページでしか参照していなくて、order_requestにあっても同じ、ということでupdated_atを移動してやる (stockテーブルよりorder_requestの方が行数がすくない)

alter table order_request add column `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

alter table stock drop column updated_at

updateやJOINしているクエリで読む行数を減らせないかな、というので、

CREATE TABLE `stock_part` (
 &#160;`variation_id` int(10) unsigned NOT NULL,
 &#160;`seat_id` varchar(6) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
 &#160;`order_id` int(10) unsigned DEFAULT NULL,
 &#160;PRIMARY KEY `variation_seat` (`variation_id`,`seat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=40961 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED
PARTITION BY LINEAR HASH(variation_id)
PARTITIONS 10;
rename table stock to stock_bk, stock_part to stock;

してみた (14:43)

stockテーブルの"order_id IS NULL "や、"order_id IS NOT NULL"のクエリが重いので、
そこをなんとかしてなくせないかなーと考えていたら、updated_atと同じ容量で、order_requestへ移せばいいんじゃね?と思ったので、アプリケーションを安全に保ちつつ、徐々に移行していった。

具体的には、variation_id/seat_idをorder_requestへ追加して、二重にINSERTするようにした。

次はJOINしまくっているクエリで、stockテーブルを使っていたところをカラムを追加したことでorder_requestを代用できるようになったのでとても高速になった。 (16:12)

最後にCSVを出力する処理でもstockを使わずにorder_requestで返せるようになったので修正 (16:20 )

このごろになると、徐々にチューニングが効きはじめて、最初のボトルネックからはかわってきたので、slow queryを取得し、pt-query-digestを実行し、アプリを書きかえては、ベンチを実行して測る、というのを繰り返した

for文の中で、1クエリ実行している箇所があったため、INを使ったクエリへ書きかえた。 (17:00)

さらにfor文の中で、1クエリ実行している箇所があったのでINを使って書きかえた(17:16)

チケットの残り件数を表示している処理がボトルネックになっていたので、これを高速に返すために、チケットを売りあげたときにカウンタテーブルを更新し、チケットの残り件数は、
全チケット数 - 売りあげたチケット数、で算出する、という方式に変更
。(17:45)

ここまででタイムアップしました。

最終的にはチケットの売上枚数が少なかった(1887 ticket)ですが、スコア(260461, BEST: 235274)はけっこうよかったようです。
おそらくカウンタテーブルを用意したことで、購入処理は重くなったが、その一方でチケット残数表示は早くなったんじゃないかと思います。

他の方のまとめはこちら

http://blog.livedoor.jp/techblog/archives/67726806.html

Play Framework 2.0雑感

scala触ってみたいなぁ、とは前々から思っていつつ、なかなか触れていなかったので、えいやと最近よく聞く気がするPlay Framework 2.0のチュートリアルを触ってみたり、軽いアプリケーションを書いてみての感想。

  • APIはよくできていて、非常に綺麗なAPIをしているなと感じた。
  • エラー画面などはカッコいい
  • devモードは、書きかえるたびにコンパイルが走るので、遅い。
  • evolutionsは001.sql/002.sqlなどではダメで、1.sql/2.sqlとしなければいけないのがちょっと管理しにくそう
  • anormは、ORMではなく、SQLを直接書く感じのAPIで、JavaでDBさわるときにめんどうな型定義しまくりをしなくてよい、というのは非常に楽で便利だなと思った。
  • 一方で、今のところanormのSQLParserのAPIの文法がどういうsyntaxになっているのかが今のところさっぱりわからない。
  • テンプレートエンジンは、標準だと、Scalaの文法をそのまま埋めこめるエンジンになっていて、コンパイル時に関数としてコンパイルされ、変数の引き渡しがチェックできるのは新鮮に感じた。一方で、簡単にコンパイルエラーになるので、エンジニア以外が触るのは厳しそうだなーという印象を受けた
    • テンプレートエンジンのタグが、@から始まると、scalaのコードというようになっていて、わりとふつうの文章を書いていてもタグとして判別されるのがイケてないなと感じた。
  • Formは、optional/nonEmptyTextなどの指定が、どのモジュールで定義されているのか、自前で拡張したいときにどうすればいいのか、がイマイチわからなかった。
    • 例えば既に登録されているタイトルを入力できないようにするチェック、などのバリデーションをどう実装すればよいのかわからなかった。
  • wikiのページなどのようにURLに日本語を含むを作ろうとしてみたが、文字化けしてしまった。
    • OSXでよくある、'export JAVA_OPTS="-Dfile.encoding=UTF-8"'の問題なのかな、と思いつつ、よくわからなかった

Sphinx+Guardでファイルを変更したときにビルドしつつ、自動でブラウザをリロードする

色々な言語が混ぜまくりすぎなので、他の人も環境を用意するようなものであればあまりおすすめできない。

guardfile内でふつうにWEBRick起動させるコードを書くのがよいんかな。

source/buildするディレクトリを分ける設定であるのを前提としております。

guard-shellとguard-livereloadが必要。

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'shell' do
  watch(%r{source/.+\.rst}) {
    puts "invoke sphinx build"
    Process.spawn(%|make html >/dev/null|)
  }
end

guard 'shell' do
  watch(%r{app.psgi}) # dummy
  Process.spawn(%|plackup -MPlack::App::Directory -e 'Plack::App::Directory->new({root=>"build/html/"})->to_app' -p 3000|)
end

guard 'livereload' do
  watch(%r{build/html/.+\.html})
end


追記

WEBrickを使うようにしてみました

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'shell' do
  watch(%r{source/.+\.rst}) {
    puts "invoke sphinx build"
    Process.spawn(%|make html >/dev/null|)
  }
end

guard 'shell' do
  watch(%r{app.psgi}) # dummy
  Process.fork do
    require 'webrick'
    webrick = WEBrick::HTTPServer.new({
      :DocumentRoot => 'build/html/',
      :BindAddress => '127.0.0.1',
      :Port => 3000
    })
    trap("INT") { webrick.shutdown }
    webrick.start
  end
end

guard 'livereload' do
  watch(%r{build/html/.+\.html})
end

rrails v1.0.0 released.

以前公開した、rails/rakeコマンドを超早くするrrailsですが、
http://d.hatena.ne.jp/walf443/20120427/1335482420

あれから、色々な方に使っていただいているようで、パッチをいくつかもらったりしてupdateしたりしています。

最近quark-zjuさんという方からたくさんpatchをいただいて、今までできなかったことができるようになった & 今までと非互換なところがでてきた、ということで紹介します。

rails console/serverができるように

今まで子プロセスへPTYの引きつぎをしていなかったため、rails console/serverなどが実行しても、こちらの入力をrrailsへ教えることができなかったのですが、できるようになりました。

個人的にはrails consoleあんまり使わないので対応しなくてもいいかなーと思って放置していたところですが、使えるとまぁ便利かなとは思います。

rrails-serverコマンドは非推奨になり、rrails startで起動するように

guard-rrailsつかっているとあまり関係ないですが、rrails-serverコマンドはそのうちなくなると思われるので、rrails startを使うようにしてください。

TCPSocketではなく、UNIXDomainSocketをデフォルトで使うように

rrailsとの通信は今までTCPでの通信でしたが、デフォルトではUnixDomainSocketを使うようになりました。

これにより、複数のプロジェクトでのrrailsの立ちあげが楽になると思われます。
一方で、rrailsを実行する際に、対象のプロジェクトのディレクトリへcdしないと適切に実行できなくなっています。

まとめ

使い方が色々かわっている(なるべくは互換性残すようにはがんばったつもりだけど...)ので、今まで使っていた方はあらためてREADMEを読みなおすことを推奨します。
http://walf443.github.com/rrails/

https://github.com/walf443/rrails/blob/master/Changes.md

DCIについて思うこと

最近DCI(Data Context Interaction)アーキテクチャというのを良く耳にするので、ちょっと調べたり他の人のコード例などをもとにちょろっと書いてみたりしたところ、やっぱりよくわからないなーと思いつつ、つまりはこういうこと?というのをまとめてみる。

たぶん色々と大分間違っているんだろうなぁと思いつつ、間違いを指摘してもらえるとありがたいです。

  • DCIは、MVCを置きかえるものではない。MVCのModelの部分が肥大化し、管理が難しくなる傾向にある(Skinny Controller, Fat Model)ので、そこをうまく扱うにはこう考えて分割すればよろしい、という考え方。つまり、M(D-C-I) - Controller - Viewみたいに書くことになる。
  • 同じモデルでも、ユーザーの役割によって、振舞いが異なることがある
    • 例えば、管理者(Administrator)には見えてよいが、閲覧者(Viewer)には見えてはいけないコンテンツ(Content)
  • 振舞いが異なるものを1つのモデルにして管理しようとすると、混乱が生じる
    • 例えば、Contentに、AdministratorおよびViewerのものをごっちゃに記述すると、Administrator権限のものをViewerで呼んでしまうようなミスが発生しやすくなる
  • そこで、コントローラーからはそのコンテキストに応じたモデルを呼びだすようにしよう
    • 例えば、ViewingContentクラスへViewerがContentを扱う際の情報を記述し、ControllerからはViewerの文脈であれば、ViewingContentを呼びだすようにする。

DCIで検索してみて出てくるコードを参考に書いたりしつつ、疑問に思ったことなど。

  • DCIという名前は個人的にはわかりづらくてMVCほど直感的には感じない気がする
    • コード例をみてみるに、ContextはUseCase /InteractionはRoleと記述されていることが多くて、私もUseCase/Roleの方がわかりやすいなと思う。ただし、Roleのクラス名をうまく考え出すことが難しくてユーザーに関連するようなケース以外だとあまりロールなしでUseCaseだけでよいのではないかと感じた。
  • コード例をみると、UseCaseはModelとRoleを紐づけることだけに専念するように書かれているが、サンプルが簡単な例だからか無駄にレイヤーが増えているだけで個人的にはあまりわかりやすくなったようには感じない。
  • コンテキストというのはユーザーの役割に応じて発生するものと思われるので、誰に対しても同じような振舞いをするものはあえてコンテキストを書く必要はない気もする
    • 実際はそういうものが後々複雑になったりしがちなのがではあるので、一枚噛ませておくのはいいと思う。
  • こういうときに個人的にはDCIで書くのがよさそうだなーという例
    • Userモデル (同じユーザーでも文脈に応じて違うクラスとして振る舞うように書くと便利そう)
    • 公開/非公開などのビジネスロジックを持つモデル
      • 非公開のものを誤って公開することがないように、コンテキストをコードへうまく表現できるとミスが減る

とはいえ、もっとコード書いてみないと、なんともいえないなーという感じではあります。
DCIでバリバリ書かれている本格的なアプリケーションで良い例とかあったりするのかな...

参考: http://d.hatena.ne.jp/digitalsoul/20100131/1264925022