« 2013年5月 | メイン | 2013年7月 »

2013年6月28日

Test::mysqldのcopy_data_fromでテストが更に捗る話

少し前ですがTest::mysqld 0.17からは copy_data_fromというオプションが加わっています。

これは、Test::mysqld起動時にコピー元のdataディレクトリを指定できるもので MySQLの起動時間を節約することができます。テスト開始時にDBに大量のデータを 入れておきたい場合に特に有効です。

特にゲームなどの場合は、大量のマスタデータもコードの一部と言えるので、ちゃんと 全部流し込んでからテストを実施したいという要件があるので重宝します。

さて、そのdataディレクトリをどうやって作ればよいかという話になるのですが、 それも、Test::mysqldに事前に作らせてどこかに配置しておけば良いでしょう。

手順としては例えば以下のようになります。

  1. ‘tmp/test_mysqld_data’ をdatadirにしてTest::mysqldを起動
  2. DDLとマスタデータの流しこみを行う

上記を事前にやっておき、実際にテストを実施する時は、’tmp/test_mysql_data’. からcopy_data_fromします。

コードは以下の様な感じになるでしょう。

use Path::Tiny qw/path/;
use Test::mysqld;
sub prepare_mysqld_copy_data {
    my $datadir = 'tmp/test_mysqld_data';
    path($datadir)->rmtree;

    my $mysqld = Test::mysqld->new(
        base_dir => 'tmp/test_mysqld',
        my_cnf   => {
            datadir => $datadir, # $datadirを指定
            'skip-networking' => '',
        },
    ) or die $Test::mysqld::errstr;

    # データ流し込み
    deploy_test_database($mysqld->dsn);

    # ここで一旦処理終了
}

deploy_test_databaseの中見は以下の様な具合です。 スキーマの流しこみと、これまたリポジトリにコミットしてcsv管理してある マスターデータを流し込んでいます。 先日書いたDBIx::FixtureLoderが活躍していますね(ステマ)。

use DBI;
use DBIx::FixtureLoder;
sub deploy_test_database {
    my $dsn = shift;
    my $dbh = DBI->connect($dsn);

    # スキーマ流し込み
    my $source = path('sql/myapp_ddl.sql')->slurp;
    for my $stmt (split /;/, $source) {
        next unless $stmt =~ /\S/;
        $dbh->do($stmt) or die $dbh->errstr;
    }

    # マスタデータ流し込み
    my $loader = DBIx::FixtureLoder->new(dbh => $dbh);
    $loader->load_fixture('data/master/item.csv');
    $loader->load_fixture('data/master/job.csv');
    $loader->load_fixture('data/master/skill.csv');
}

これで、’tmp/test_mysqld_data’ 以下にデータが作られます。 以降は各テスト実施時に以下のようにTest::mysqldを起動すれば良いだけです。

my $mysqld = Test::mysqld->new(
    copy_data_from => 'tmp/test_mysqld_data',
    my_cnf         => { 'skip-networking' => ''},
) or die $Test::mysqld::errstr;

テストファイル毎にTest::mysqldを起動する形になりますが、 マスタデータが多い場合、

  • Test::mysqldのプロセスを使い回して起動時間を節約するよりも、データを流し込む時間を節約するほうが効果が高い
  • テストケース毎の競合を考える必要がない
  • テストでデッドロックなどが発生することを心配する必要がなく並列度を上げやすい
  • 各テストで初期化処理やクリーンアップ処理を複雑に考えずに済む

といったメリットもあるので、この形を採用するに至りました。

実際今やっている案件だと、素直にTest::mysqldを起動してデータを流し込んだ場合、10秒程度時間がかかりますが、copy_data_fromを使った場合は1秒程度に短縮されますtempfs的なものを使えばもっと速くなるでしょう。

OSXでtmpfs的なことをするのは村瀬大先生のエントリーが参考になります。

ちなみに、本来は

  • datadirがない場合のケア
  • マスタデータやDDLが更新された場合のdatadirの作り直しの処理

などが必要なので、それも考慮した形だともうちょっとコードは複雑になります。その辺の話はそのうち書くかも。また、このdatadirを元に開発用のMySQLを立てたりもしていますがその辺の話もまたそのうち。

実はこれと同様のことを自力で頑張っていたりしたので、これは神機能来たなーと、リリース直後にkazuhoさんがアナウンスする前にChanges読んで思っていたのですが、使ってみたらやっぱり神だったのでBlogろうと常々思っていてやっとblogった次第。

ちなみに最初に自力で頑張っていたのは僕ではなくて、テスト番長の335先生で、copy_data_fromがなかった時代の苦労が綴られております

彼も今年は、perl な web application のためのテスト情報という形でYAPCのトークに応募しているので、非常に楽しみですね。

01:36

2013年6月23日

CPANで意図しない名前空間の取得を防ぐために

だいたいこのへんで教えてもらった話のまとめです。

http://lingr.com/room/perl_jp/archives/2013/04/03

CPANで名前空間を取るのは簡単です。今ならCPANに上げるコードベースの「どこか」に package Hoge; と書けば、CPAN Indexerにインデックスされていとも簡単にHoge名前空間のオーナーになれます。 (執筆時現在Hogeのオーナーはいません)

これはlib/以下の.pmファイルやファイル先頭のpackage宣言だけに限った話ではありません。 例えば、example/MyApp.pmとかも対象です。

ちなみに誰がどの名前空間を持っているかは以下を見ることでわかります。

http://www.cpan.org/modules/02packages.details.txt

多くの場合この挙動に困ることはありませんが、以下の様な場合に困ることがあります。

  1. example codeを同梱してその中にpackage宣言を含む場合 eg. package ExampleApplication;
  2. 自分が持っていない他人の名前空間のpackage宣言をする場合 eg. package Plack;

1の場合は、特に欲しくもないExampleApplicationという名前空間を取得してしまいま す。あまりかっこよくありませんし、もし他の人がその名前空間を欲しがったら困るでしょう。

つい最近だと、Mozilla::DOMのexampleにMinilla.pmが同梱されていて、名前空間がす でに取られていたので、Minillaでは交渉して名前空間を開けてもらったみたいな話もあったりし ました。

2の場合は「権限のない名前空間を取ろうとするんじゃねーよ!」ってCPAN Indexerに怒られます。 そして無常にも ** UNAUTHORIZED RELEASE ** の烙印を押されます。

そういうことを起こさないために以下のような対応が必要です。

  • 適切にnoindexを設定する、またはnoindexをケアしてくれるauthorizing toolを使う
  • packageの後に改行を入れる # hide from PAUSE ハックを使う(あまりやるべきではない)

適切にno_indexを設定する、またはno_indexをケアしてくれるauthorizing toolを使う

CPAN::Meta::Specのno_indexの項 に書いてあるように、インデックスしてほしくない項目をno_indexで指定することができます。 Makefile.PLやBuild.PLに適切な設定項目があります。

一応、t/やinc/などはIndexerはskipしてくれるようになっていますが、そういう挙動 に期待せずに、ちゃんと指定した方が良いです。

また、MinillaやMillaだと、

directory => ['t', 'xt', 'inc', 'share', 'eg', 'examples', 'author']

を自動的にno_indexに追加してくれているので、その辺気にしなくて良いので楽チンです。

packageの後に改行を入れる # hide from PAUSE ハックを使う(あまりやるべきではない)

他人のモジュールに対して、モンキーパッチしたいとか、メソッドを追加したいそうい うそもそもお行儀の良くない要件がごくたまにありますが、そういう時に、素直 にpackage宣言を書いてしまうと、名前空間を取ろうとしてしまうので良くありません。

そういう時は以下のように改行とコメントを入れることでインデックスをさせないこと が可能です。

package # hide from PAUSE
    Plack;

こういう他人のモジュールに動的に手を入れたいという要件の時のみに使うべきで、 そもそもそういうお行儀の良くないことはあまりやらないほうがいいでしょう。

逆に、ファイルの先頭に書かないinner packageなどで、package宣言の後に改行を入れて しまっているケースをたまに見かけますがこれはあまり良くありません。

なぜかというと、サブパッケージであっても、その名前空間を取得しておかないと、 他の人にその名前空間を取られてしまうかもしれなくて悲しいからです。


余談ですが、CPANモジュールはlib/ 以下にモジュールを配置するのが一般的な形ですが、 あくまで慣例であって、別にそう決まっているわけではありません。 また、Inner Packageなどもあるので、ファイル名とパッケージ名が一対一対応しているとも 限りません。

なので涙ぐましいパース処理をIndexerは行なっています。

その様子は https://github.com/andk/pause/tree/master/lib/PAUSE 辺りのソース を読むと良いでしょう。

02:02

2013年6月22日

勝手に続:DBIx::Tracerを使って流れているSQLのテストをしてみた話

http://shibayu36.hatenablog.com/entry/2013/01/22/080049

このへん見て、もっと汎用的に手軽に使える感じにしたいなーとか常々思ってたので以下の様なユーティリティーをプロジェクトのt::Utilに追加した。

sub trace_sqls(&) {
    my $code = shift;
    my @sqls;
    require DBIx::Tracer;
    my $tracer = DBIx::Tracer->new(sub {
        my %args = @_;
        push @sqls, $args{sql};
    });
    $code->();
    @sqls;
}

以下のように使う。

my @sqls = trace_sqls {
    $dbh->do('UPDATE ...');
    ...
};

@sqlsに流れたSQLが入ってきます。便利。

みたいなことを社内IRCで発言したら、soh335先生に「あーDBIx::QueryLog版だけど同じようなの半年前に作ってたわー」って言われて涙目であった。

なんか最近やっとテストを楽に書く方法とかそういうのを色々考えられるようになってきて、テスト番長であるsoh335先生の背中が見えてきた感じがする。

19:21

2013年6月17日

DBIx::FixtureLoaderってのを書きました

https://metacpan.org/module/DBIx::FixtureLoader

これもまたどこも同じようなの作ってるんでしょうけど、テストとかでさくっとfixture 読み込んでInsertして欲しい的な要件で作りました。使い方は以下の様な感じ。

use DBI;
use DBIx::FixtureLoader;

my $dbh = DBI->connect(...);
my $loader = DBIx::FixtureLoader->new(dbh => $dbh);
$loader->load_fixture('item.csv');
$loader->load_fixture('item-2.yaml', update => 1); # ON DUPLICATE KEY UPDATE

ORM非依存で$dbhを渡すだけで簡単にお使いいただけます。

CSV、YAML、JSONのfixtureをよしなに読み込んでくれます。ファイル名からテーブル名と フォーマットを解決してくれるゆるふわな感じになっております。

CSVはヘッダ行付きのCSV、YAMLとJSONはArray<Hash> もしくは、ActiveRecordみ たいなHash<Hash>形式のデータを受け付けます。一応Test::Fixture::DBI形式のYAMLも 受け付けるようになっております。

基本的にテストコードで使うことが前提ですが、開発環境のDBをサクッと作りたい的な 場合にも有用なんじゃないでしょうか。

ちなむと最近はGoogle Spreadsheetでテストデータ作って、それをローカルにサクッと取り込んでリポジトリ管理とかそういうことが多いです。

依存少なめで作る予定だったのですが、結局SQL::MakerとMooとか使いました。

SQL::Makerマジ便利ですね。いつの間にかPlugin::InsertOnDuplicateもある。

Mooもやっぱこれくらいは使いたいな!って感じで、個人的にもMouseとの違いとかにも 慣れてきたので、CPANモジュールには積極採用していきたい。

プロジェクトでは引き続きMouse使うけどね。

00:52

2013年6月 7日

サーバーマシンのコア数に応じてworker数を調整する方法

PSGI/Plackアプリケーションの起動方法いろいろと本番環境アレコレ

便乗ポスト。最近は、上記内の「シェルスクリプトでラップする方法」で運用していることが多いです。その場合のone more tips.

appサーバーごとにマシンスペックが違う場合がたまにあって、その場合マシンごとに worker数を調整したいけど、deployの都合上サーバー起動スクリプトは同じやつを使いた いってことがあります。

そこでおすすめなのが、CPUコア数に応じてworker数を計算する方法です。

シェルスクリプトの場合、

% cat app.sh
#/bin/sh
NCPU=`getconf _NPROCESSORS_ONLN`
WORKERS=$(expr $NCPU \* 5)
exec plackup -E production -s Starlet --max-workers=$WORKERS

となります。この場合だと、コア数x5のworkerが起動するはずです。 この方法はfujiwara氏に教えてもらいました。

もちろん、CPUコア数のみにworker数を比例させるのはいささか乱暴なので、 ちゃんとチューニングしたいのであれば、サーバーごとに調整したほうが良いのは 言うまでもありません。

17:31

Redis::LeaderBoardっての書いてた

RedisのSorted Setがランキング作るのとかに便利だよーってのは今や多くの人に知られるところですが、 同率問題とかがめんどくさかったりするので、その辺解決したやつを書いてみました。 というか、このへんみなさん個別に書いてると思うんですけど、色々めんどくさくなってカッと なってCPANに上げました。Synopsis丸コピですが、以下のような感じで使います。

use Redis;
use Redis::LeaderBoard;
my $redis = Redis->new;
my $lb = Redis::LeaderBoard->new(
    redis => $redis,
    key   => 'leader_board:1',
    order => 'asc', # asc/desc, desc as default
);
$lb->set_score('one' => 100');
$lb->set_score('two' =>  50');
my ($rank, $score) = $lb->get_rank_with_score('one');

# memmber object (オブジェクトが欲しい人のため)
my $member = $lb->find_member('two');
$member->score(90);
my $rank2 = $member->rank;

$rankings = $lb->rankings(offset => 0, limit 10);
  • 同率問題を考慮したランキングが取れる
  • ランキング一覧が取れる
  • 値が昇順の場合でも降順の場合でも同じようなインターフェースで扱える

と言った感じになっています。

Redis2.8で同点問題解消したやつが入るってことで、それを待とうかと思ってたん ですが、以下のissueを見ると、今提案されているパッチがそのまま取り込まれる 訳ではなく再設計みたいな流れになっているようで、もう少し時間がかかりそうだったので、 取り急ぎユニークランキングの実装が必要だったのでCPANに上げた次第。

https://github.com/antirez/redis/issues/943

ご利用下さい。

11:09

2013年6月 5日

cpanfileのrequiresにURLを直接指定するのは好ましくない

cpanfileでは、requires "git:..."; のように、gitやtarballのURL等を指定すれば cpanm --installdeps . でモジュールがインストールできる「裏ワザ」があることが 結構知られているようです。しかし、これは好ましくありません。そもそも CPAN::Meta::Spec準拠ではありません。

これはあくまでcpanm側が第1引数にgitやtarballのURLなどを指定すればよしなに インストールしてくれるから現状は「偶然」そういう動きになっているだけです。

(cpanm git:...でインストールできるので、requires "git:..."; って書いてあれば 入れてくれてしまうってことです。)

あくまでインストールにcpanmを使った場合に動くってだけの限定的な話で、しかも今後は 動かなくなる可能性が高いです。

実際問題、requires $any_url; としてしまうと、モジュールのバージョン比較とか ができずに、毎回愚直にdownloadしてきてインストールし直すようになってしまうの で、これは色々嬉しくない。

じゃあどうするのか

現状、requires "Module::Name", "1.01", git => "git:...";などと書けるように する仕様が話しあわれています。

なので、そのあたりの仕様が固まるまで待ちましょう。

待ってられないなら、OrePAN2とかでcpan mirrorを立てよう

とは言えそんなに悠長に待ってられないって話もあるでしょう。実際問題、CPANに上がって ないけど、gitから入れたいってことはままあるかとおもいます。

その場合は、CPANに上がっていないモジュールをOrePAN2でcpan mirror運用しましょう。 OrePAN2の使い方はググって下さい。

OrePAN2を使うと、git URLから簡単にcpan mirror形式のディレクトリを作れるので、 それをそのままローカルで使ってもいいし、どっかで静的ファイル配信してもいい。 gitで共有してもいい。楽チンです。

cpan mirrorを使ってモジュールをインストールする方法はcpanmとcartonでそれぞれ 以下の様な感じです。なんかmirrorのみ指定だと依存モジュールとかが上手く入らない ようです。

% cpanm --mirror=http://cpan.metacpan.org/ --mirror=http://oreore/orepan/ --installdeps .
% PERL_CARTON_MIRROR=http://cpan.metacpan.org/,http://oreore/orepan/ carton install

今の職場だと、Arkをそういう感じで運用していたりします。OrePANで今年運用 始めたんだけどそしたらいきなりOrePAN2が出てきてだいぶタイミング悪かった感ある。

Arkのrepoの.travis.yml を見るとそのへん色々頑張ってるな!っていうのが伺えます。

そういや、Arkのリポジトリをいつの間にか移動させてました。しかしそろそろCPANにあげたいですね。クリスマス辺りには上がるんじゃないですかね。

23:15