cpan module を並列でインストールする
先日行われた Gotanda.pm #5 でも発表させていただいたが、まさしく experimental な cpan client cpm を作った。
Why new?
cpan module の install にはいつも cpanm を使っており そのシンプルさと安定性には感心するばかりである。
が、cpanmにも不満がないわけではない。 perlである程度のことをやろうとすればすぐにcpan moduleの依存は 100を超える。 そういった場合、cpanmは直列でcpan moduleをインストールしていくため かなりの時間がかかる。
これをなんとかしたい。
Features of cpm
cpm は如何に速くcpan module をインストールするか、しか考えていない。速くするために以下のようにしている。
- cpanm を並列に使って cpan module をインストールする。
- distributionのprovidesはAPIから取得する。cf: https://cpanmetadb-provides.herokuapp.com/
How to install
cpanm 2.0 (Menlo) に依存しているので以下のようにしてインストール。
> cpanm -nq git://github.com/miyagawa/cpanminus.git@menlo > cpanm -nq git://github.com/shoichikaji/cpm.git
本当に速くなるのか
実際速くなった。
例えば Plack の場合、
> time cpanm -nq -L cpanm-local Plack ... Successfully installed Plack-1.0037 25 distributions installed real 0m47.705s > time cpm install -L cpm-local Plack ... 13613 DONE install Plack-1.0037 real 0m16.629s
47sec -> 16secになった。
また他の例として依存が190個(!)ぐらいある Dist::Milla は 7min14sec -> 1min54sec になった。 https://gist.github.com/shoichikaji/65816150effb9ea45dfe
Let's try cpm with your module!
misc
cpm install -v Plack
とすると途中経過も表示するようになる。- API server を heroku で立ててるのをなんとかしたい。
- gotanda.pm の発表から下記を変えた。
結論
perl があつい!
Plack::App::File などで 304 Not Modified を返させる
plackup -MPlack::App::Directory -e 'enable "ConditionalGET"; Plack::App::Directory->new->to_app'
How FatPacker Works
https://metacpan.org/pod/App::FatPacker について知るしかない!
What is FatPacker?
perlを実行したら
Can't locate Moo.pm in @INC...
などとでてくることは日常茶飯事である。
これは文字通りMoo.pmがないからであり、おそらく
cpanm Moo
とすれば解決する。
しかし
- めんどくさい
Can't locate Moo.pm in @INC
と出た時点で放棄する人が続出する
などの理由によりこれをなんとかしたい。
ここで出てくるのが FatPacker である。すなわちFatPackerはスクリプトに依存モジュールをすべて同封してくれる。
How FatPacker Works
例として script.pl は Hello module を require しているとしよう。
> cat script.pl #!/usr/bin/env perl use strict; use warnings; use Hello; print Hello::world; > cat Hello.pm package Hello; use strict; use warnings; sub world { "Hello, world\n" } 1;
このとき script.pl に Hello.pm を同封したい。
先のエントリーで @INC にはオブジェクトも入れられるといった。これを利用すればいい。 すなわち script.pl の先頭に以下のようにして Hello.pm をぶちこむ。
#!/usr/bin/env perl BEGIN { { package My::FatPacker; my $hello_content = <<'...'; package Hello; use strict; use warnings; sub world { "Hello, world\n" } 1; ... sub My::FatPacker::INC { my ($self, $module_filename) = @_; return if $module_filename ne "Hello.pm"; open my $fh, "<", \$hello_content; $fh; } } unshift @INC, bless {}, My::FatPacker; } use strict; use warnings; use Hello; print Hello::world;
これがまさにFatPackerがやっていることである。
まとめ
FatPacker を使わざるを得ない。
perl の require を知る
今、perlがあつい!よって require について知るしかない!
"普通" のrequire
require(もしくはuse)関数は Module をロードしたいときに使う。
例えば、
require Foo::Bar; # もしくは use Foo::Bar
とすると、perlは@INC
に入っているディレクトリ以下のFoo/Bar.pmファイルを探しload(eval)しようとする。
すなわち仮に@INCが
@INC = ("/path/to/dir1", "/path/to/dir2", "/path/to/dir3")
ならば、perlは
候補1: /path/to/dir1/Foo/Bar.pm 候補2: /path/to/dir2/Foo/Bar.pm 候補3: /path/to/dir3/Foo/Bar.pm
の順に Foo/Bar.pm を探し、見つかった時点でそれをloadし探索を終了する。 もし見つからなかったら、
Can't locate Foo/Bar.pm in @INC (you may need to install...
のメッセージとともにdieする。
よって自分の書いたモジュールMy::Moduleが lib/My/Module.pmに置いてあるなら @INC にlibディレクトリを突っ込むことで My::Module を使えるようになる。
BEGIN { unshit @INC, "lib" } use My::Module; # OK
@INCにはサブルーチンリファレンス、オブジェクトも入れられる
上記、"普通"のrequireの仕組みは特定のディレクトリにファイルとしてモジュールを配置することを強制する。
しかし実際のところ、@INCにはディレクトリだけでなく、サブルーチンレファレンス、オブジェクトも入れられる!すなわち
my $sub = sub { ... }; unshit @INC, $sub;
みたいにやってよろしい。このハックによってモジュールロードの仕方を自由に変えられる。
まず例を出すと、
BEGIN { my $sub = sub { my ($self_sub, $module_file) = @_; return if $module_file ne "My::Module"; my $module_content = q{ package My::Module; sub new { bless {}, shift } sub hello { print "hello!\n" } 1; }; return \$module_content; }; unshift @INC, $sub; } use My::Module; # OK!!
のようにすれば My::Module は $sub から読み込まれる。物理的なディレクトリやファイルの制約から解放された!
@INCに入れるサブルーチンリファレンス詳細
@INCに入れられる$subの詳細は以下である。
仮に @INC が
@INC = ($sub, "/path/to/dir1", "/path/to/dir2");
だとしよう。このとき require Foo::Bar
が実行されると
まず@INCの先頭の$subが実行され、それがFoo::Barを"load"させたなら
そこで終了。もし$subがFoo::Barを"load"しなかった場合は
"/path/to/dir1"から通常のディレクトリ探索が始まる。
さて$subは
第一引数:自分自身 ($sub) 第二引数:モジュールファイル名 (Foo/Bar.pm)
で実行される。そして$subはもしそれをloadさせたければ 以下の9種類のうちどれかを返り値として返せばよろしい(おそらく)。
1. $scalar_ref 2. ($scalar_ref, $fh) 3. ($scalar_ref, $fh, $filter_sub) 4. ($scalar_ref, $fh, $filter_sub, $filter_sub_arg) 5. $fh 6. ($fh, $filter_sub) 7. ($fh, $filter_sub, $filter_sub_arg) 8. $line_generator_sub 9. ($line_generator_sub, $line_generator_sub_arg)
もし、$subがそれをloadさせたくなければ return だけすればいい。
9種類の返り値のうち、1, 5 ぐらいしか見たことがないのでそれだけ説明する。
1.の $scalar_ref とはコード(文字列)のレファレンスである。つまり
my $code = <<'...'; use strict; use warnings; sub foo { ... }; sub bar { ... }; 1; ... my $scalar_ref = \$code;
のようなものである。これを返り値として返せば、それがFoo/Bar.pmのコードだとしてload(eval)される。
5.の $fh とはファイルハンドルである。例えば
open my $fh, "<", "/path/to/awesome/code.pm"; $fh;
などを返せば、$fhの中身がFoo/Bar.pmのコードだとしてload(eval)される。
@INCに入れるオブジェクト詳細
@INCにはオブジェクトも入れられる。動作はほぼサブルーチンの場合と一緒である。違うのはサブルーチンの場合はモジュール探索時単に実行されたが、 オブジェクトの場合はINCメソッドが実行される点である。
例えば以下のような感じ。
BEGIN { { package My::Hook; sub new { bless {}, shift } sub My::Hook::INC { my ($self, $module_filename) = @_; return if $module_filename ne "Foo/Bar.pm"; my $content = 'sub hello { warn "hello" } 1;'; open my $fh, "<", \$content; return $fh; } } my $hook = My::Hook->new; unshift @INC, $hook; } use Foo::Bar;
返すべき値はサブルーチンの場合と一緒である。
どんなハックができるか
FatPacker, Carmel, lib::xi あたりを見ざるを得ない。
SEE ALSO
- perldoc -f require
サブルーチンが返すべき値のところの文章がわかりにくい。よってこの説明があってるか微妙。
まとめ
perlがあつい!
perl プログラムが hang する
会社でperlでcrawler (prefork 型のプログラム) を書いたのだが、 数日動かしているといくつかのworkerプロセスが固まって動かなくなってしまうという事案が発生している。
とりあえず手許で再現はできたが原因・解決法はわかっていない。
もしわかる方いましたら教えてもらえると大変嬉しいです。
どういうところで固まるか
- fork を多用しているprogramで、
Sys::SigAction::timeout_call で重たい処理を囲み、
Sys::SigAction::timeout_call $sec, sub { do_heavy_work(); };
timeout が起こり、
Perl_sighandler
が signal 14 (SIGALRM) で呼ばれ- さらに
Perl_sighandler
の中で下記が呼ばれPerl_safesysmalloc
malloc
_L_lock_9503
__lll_lock_wait_private
- たまに固まる
手許での再現方法
https://github.com/shoichikaji/perl-hang にある hang.pl で再現可能。
perl hang.pl を実行すると、master process が30個のworkerをforkする。 そしてworkerは timeout_call をsetしつつ重い処理(ここではfibonacci数の計算)をやる。 通常、workerは数十秒で終了しあらたなworkerがforkされるはずである。
ところがこのプログラムを1時間ぐらい動かしていると、
skaji 3930 0.0 0.2 133096 5200 pts/1 S+ 11:19 0:03 | \_ perl hang.pl skaji 8916 0.0 0.2 133096 4228 pts/1 S+ 11:55 0:00 | \_ worker (2015-05-30 11:55:17) skaji 14191 11.4 0.2 133096 4212 pts/1 R+ 12:33 0:02 | \_ worker (2015-05-30 12:33:14) skaji 14209 10.5 0.2 133096 4212 pts/1 R+ 12:33 0:01 | \_ worker (2015-05-30 12:33:20) skaji 14210 10.6 0.2 133096 4212 pts/1 R+ 12:33 0:01 | \_ worker (2015-05-30 12:33:20)
となる。ここで pid=8916 のworkerは30分近く生きており固まっているとわかる。
(また、おそらくこのprocessが固まる時に Attempt to free unreferenced scalar: SV 0x1620660 at hang.pl line 21.
なる warning がでてきた。)
pid=8916 のback traceは以下のような感じ。
#0 0x00007fb96b78e05e in __lll_lock_wait_private () from /lib64/libc.so.6 #1 0x00007fb96b71316b in _L_lock_9503 () from /lib64/libc.so.6 #2 0x00007fb96b7106a6 in malloc () from /lib64/libc.so.6 #3 0x000000000048a675 in Perl_safesysmalloc (size=<value optimized out>) at util.c:130 #4 0x00000000004b7a28 in Perl_sv_grow (sv=0x16347b0, newlen=10) at sv.c:1593 #5 0x00000000004b4968 in Perl_sv_setsv_flags (dstr=0x16347b0, sstr=0x1410100, flags=<value optimized out>) at sv.c:4524 #6 0x00000000004b56bd in Perl_newSVsv (old=<value optimized out>) at sv.c:9325 #7 0x0000000000494ff2 in Perl_sighandler (sig=14, sip=0x7fff073c97b0, uap=0x7fff073c9680) at mg.c:3216 #8 <signal handler called> #9 0x00007fb96b70f17d in _int_malloc () from /lib64/libc.so.6 #10 0x00007fb96b7106b1 in malloc () from /lib64/libc.so.6 #11 0x000000000048a675 in Perl_safesysmalloc (size=<value optimized out>) at util.c:130 #12 0x00000000004a2650 in Perl_av_extend_guts (av=0x1634780, key=6, maxp=0x16cb400, allocp=0x16cb408, arrayp=<value optimized out>) at av.c:176 #13 0x00000000004a2bb8 in Perl_av_store (av=0x1634780, key=6, val=0x1634798) at av.c:345 #14 0x0000000000465abf in Perl_pad_push (padlist=0x16229d0, depth=36) at pad.c:2352 #15 0x00000000004a54aa in Perl_pp_entersub () at pp_hot.c:2671 #16 0x00000000004a36f3 in Perl_runops_standard () at run.c:42 #17 0x0000000000437fbe in S_run_body (my_perl=<value optimized out>) at perl.c:2451 #18 perl_run (my_perl=<value optimized out>) at perl.c:2372 #19 0x000000000041ce5c in main (argc=2, argv=0x7fff073ca0b8, env=0x7fff073ca0d0) at perlmain.c:114
なぜ固まるか?
misc
- fork が関係あるかもまだよくわからず。
- Sys::SigAction::timeout_call での timeout は使うべきでないのか。
cf: http://blogs.perl.org/users/leon_timmermans/2012/01/what-you-should-know-about-signal-based-timeouts.html - 今週中にこれを解決し、shibuya.pm でLTしたいと思っていたのだがw
Carmel を読む
5/20 に 吉祥寺.pmミニ Carton/Carmelのコードリーディング が行われるので事前にCarmelを読んどく。
結論
読むなら Carmel::Setup/Runtime あたり!
Carmel の基本的な使い方
ほとんど Carton と同じにように使える。つまり cpanfile に依存を書いて、carmel install でインストールし、carmel exec で実行。
$ echo "requires 'Plack';" > cpanfile $ carmel install ---> Installing new dependencies: Plack ... ---> Complete! 1 cpanfile dependencies. 20 modules installed. $ carmel exec perl -V ... @INC: Carmel::Runtime::FastINC=HASH(0x7fbe120a45a8) /Users/skaji/.carmel/5.20.2-darwin-2level/builds/Apache-LogFormat-Compiler-0.32/blib/lib /Users/skaji/.carmel/5.20.2-darwin-2level/builds/POSIX-strftime-Compiler-0.41/blib/lib ... $ carmel exec plackup --version Plack 1.0034
実際のところ
Carmel と Carton は module の保持の仕方、それをどのように使わせるかにおいてかなり違う。
Caton は local/ 以下に module をインストールし、実行時は単に PERL5LIB に local/ を追加していた。
一方 Carmel は中央 repository (~/.carmel/perl-version/builds) に module を "個別" に保持し、実行時に @INC を巧みにいじり、require させている。
Carmel は @INC をどのように扱っているのか
中央 repository に個別にmodule を保持しているためたくさんの依存があった場合は、その数だけ @INC に追加しなくてはいけない。
初期の Carmel は環境変数 PERL5LIB にこれを追加していた。 PERL5LIBは相当な長さになり、これはちょっと、という感じがあった。
現在の Carmel は .carmel/MySetup.pm という設定ファイルに @INC をはじめとする依存moduleの情報を保存し、実行時はそれを require するようになった。
そして、ここからが興味深いところで Carmel は その設定ファイル .carmel/MySetup.pm の情報を単に @INC に追加するのではなく、Carmel::Runtime::FastINC クラスのインスタンスを作り、それを @INC に差し込んでいる。
名前の通り、Carmel::Runtime::FastINC は素早くmoduleを読み込むための仕掛けである。 実装詳細はコードを見てもらえばわかるが、とにかくこのあたりがかっこいい!
その他
- 依存モジュールを recursive に解決するコードがかっこいい。
- binstub の意味は?
my($self, $arg) = @_
のmy
と(
の間にスペースを書かない流儀 は誰が発祥か知りたい。- carmel package で configure deps などがコピーされない。
- https://github.com/miyagawa/Carmel/blob/master/lib/Carmel/Runner.pm#L31
ここは PERL5OPT ではなくて PERL5LIB=local/lib/perl5 だと思うが自信がない。-> やはりそのままであってた。 - 開発環境での使い方は?
perl -MCarmel::Setup ...
がいい? - 本番環境での使い方は?
carmel rollout
してPERL5LIB に local を設定する、がいい? - 会社で HTML::Tidy module を使っているのだが、これは先にAlien::Tidyp module を入れておかないとインストールに失敗する。 このようなmoduleをcpanfileで管理するのに苦労している。 何かいい解決法はないか。