Vagrant + Chef でハマったところメモ
Vagrant で作る環境に、 anyenv を入れるのでちょっとハマったのでメモ。 使っている Vagrant は 1.7.2 で、 Vagrantfile には、
config.vm.provision "chef_zero" do |chef| chef.add_recipe "anyenv::ndenv" end
というようなことを書いている。
anyenv install でコケる
bash "install_ndenv" do user node['anyenv']['user'] group node['anyenv']['gname'] code <<-COMMAND export PATH="#{node['anyenv']['destination']}/bin:$PATH" eval "$(anyenv init -)" anyenv install ndenv COMMAND creates "#{node['anyenv']['destination']}/envs/ndenv" end
こんな感じに anyenv でなんとか env を入れようとすると、
anyenv-install: definition not found: ndenv
といったエラーでコケる。 anyenv の方のソースを見ると、
BUILTIN_DEFINITION_PATH="${ANYENV_ROOT}/share/anyenv-install/${DEFINITION}" if [ -e "$BUILTIN_DEFINITION_PATH" ]; then DEFINITION_PATH="$BUILTIN_DEFINITION_PATH" else echo "anyenv-install: definition not found: ${DEFINITION}" exit 2 fi
ということが書かれていたので、どうやら ANYENV_ROOT という環境変数が無いっぽい。
Bash の provisioning のドキュメントを見た感じ、 environment というのを使うと環境変数を設定できるようなので、
bash "install_ndenv" do user node['anyenv']['user'] group node['anyenv']['gname'] environment 'ANYENV_ROOT' => node['anyenv']['destination'], 'HOME' => node['anyenv']['home'] code <<-COMMAND export PATH="#{node['anyenv']['destination']}/bin:$PATH" eval "$(anyenv init -)" anyenv install ndenv COMMAND creates "#{node['anyenv']['destination']}/envs/ndenv" end
vagrant ユーザで chef-solo 使うと HOME は /home/vagrant になるところが、 Vagrantfile からプロビジョニングすると、 root ユーザで su vagrant したような感じになって、 HOME が /root になるため。
vagrant provision がちゃんと動かない
Shared folders that Chef requires are missing on the virtual machine. This is usually due to configuration changing after already booting the machine. The fix is to run a `vagrant reload` so that the proper shared folders will be prepared and mounted on the VM.
こんなエラーが出て chef が動かなくなったりした。ググったら、
rm .vagrant/machines/default/virtualbox/synced_folders vagrant reload --provision
これをすると直るというので、やったら直った。
Mojolicious のテストがコケた時に CLI にエラーを出す
Mojolicious アプリのテストをやっていると、 500 エラーになるようなコケ方をしたときに、 CLI にエラーが出てこなくて原因を調べるのがちょっとめんどくさいですが、こんな風にテストを動かすといい感じになります。
Mojolicious 5.39 以前
ちょっと前の Mojolicious では、コントローラでコケた際には Mojolicious::Controller::render_exception が呼ばれるようになっていたので、そのメソッドに monkey patch する。
use common::sense; use Mojolicious::Lite; use Test::Mojo; use Test::More; use Test::Pretty; is $Mojolicious::VERSION, '5.39'; get '/' => sub { die 'Hoge'; }; { no strict 'refs'; no warnings 'redefine'; *Mojolicious::Controller::render_exception = sub { my ($c, $e) = @_; print "\n\n", "*** Exception thrown:\n"; warn $e; print "\n\n"; $c->rendered(500); $c; }; }; my $t = Test::Mojo->new; $t->get_ok('/')->status_is(200); done_testing;
結果:
% carton exec -- perl -Ilib test.pl ✓ L7: is $Mojolicious::VERSION, '5.39'; *** Exception thrown: Hoge at test.pl line 9. ✓ GET / ✖ 200 OK # Failed test '200 OK' # at test.pl line 27. # got: '500' # expected: '200'
Mojolicious 5.40 以降
新しい方の Mojolicious では、 Mojolicious::Plugin::DefaultHelpers から追加される、 'reply.exception' というヘルパーが呼び出されるようになっていたので、テストでは上書きする。
use common::sense; use Mojolicious::Lite; use Test::Mojo; use Test::More; use Test::Pretty; is $Mojolicious::VERSION, '5.72'; get '/' => sub { die 'Hoge'; }; my $t = Test::Mojo->new; $t->app->helper( 'reply.exception' => sub { my ($c, $e) = @_; print "\n\n", "*** Exception thrown:\n"; warn $e; print "\n\n"; $c->rendered(500); $c; } ); $t->get_ok('/')->status_is(200); done_testing;
結果:
% perl test.pl ✓ L7: is $Mojolicious::VERSION, '5.72'; *** Exception thrown: Hoge at test.pl line 9. ✓ GET / ✖ 200 OK # Failed test '200 OK' # at test.pl line 24. # got: '500' # expected: '200'
Term::ANSIColor なんかを使って、いい色にするとなおわかりやすいと思います。
Mojolicious で Streaming
Mojolicious で、レスポンスをストリーミングで返すには、 Mojolicious::Controller::write_chunk を使います。
使い方は簡単で、 chunk ごとにレスポンスを書いていく要領です。
例: 1024 byte ずつ返す
use strict; use warnings; use Mojolicious::Lite; get '/streaming' => sub { my $c = shift; open my $fh, '< my-list.txt'; my $cb; $cb = sub { my $c = shift; my $length = sysread($fh, my $buffer, 1024); if (!$length) { close $fh; undef $cb; return $c->finish(''); } $c->write_chunk($buffer, $cb); }; $c->res->headers->content_type('text/plain'); $c->$cb; }; app->start;
例: 1行ずつ返す
下のコードではファイルの中身を1行ずつ書いてますが、 どんなイテレーションでもそのまま書き出せるとメモリ的に捗ります。
use strict; use warnings; use Mojolicious::Lite; get '/line-by-line' => sub { my $c = shift; open my $fh, '< my-list.txt'; my $cb; $cb = sub { my $c = shift; my $buffer = <$fh>; if (!$buffer) { close $fh; undef $cb; return $c->finish(''); } $c->write_chunk($buffer, $cb); }; $c->res->headers->content_type('text/plain'); $c->$cb; }; app->start;
OS X の "path_helper" と anyenv
anyenv を使うと、いろいろな言語の環境を揃えたり使い分けたりするのが捗りますが、 Mac OS X で GNU Screen を使うときには、環境変数の PATH まわりでちょっとうまくいかないです。
Mac OS X では、 /etc/profile にあるシステムワイドな設定で、
eval `/usr/libexec/path_helper -s`
が動いて、 PATH を整理するみたいです。
その結果、
/Users/yowcow/.anyenv/envs/rbenv/bin /Users/yowcow/.anyenv/envs/plenv/bin /Users/yowcow/.anyenv/envs/ndenv/bin /Users/yowcow/.anyenv/envs/rbenv/shims /Users/yowcow/.anyenv/envs/plenv/shims /Users/yowcow/.anyenv/envs/ndenv/shims /Users/yowcow/.anyenv/bin /usr/local/bin /usr/bin /bin /usr/sbin /sbin
という順に通っていてほしいところが、 GNU Screen を起動した後は、
/Users/yowcow/.anyenv/envs/rbenv/bin /Users/yowcow/.anyenv/envs/plenv/bin /Users/yowcow/.anyenv/envs/ndenv/bin /Users/yowcow/.anyenv/envs/rbenv/shims /Users/yowcow/.anyenv/bin /usr/local/bin /usr/bin /bin /usr/sbin /sbin /Users/yowcow/.anyenv/envs/rbenv/bin /Users/yowcow/.anyenv/envs/plenv/bin /Users/yowcow/.anyenv/envs/ndenv/bin /Users/yowcow/.anyenv/envs/rbenv/shims /Users/yowcow/.anyenv/envs/plenv/shims /Users/yowcow/.anyenv/envs/ndenv/shims /Users/yowcow/.anyenv/bin
こんな風に変わってしまいます。
rbenv 以外は .anyenv/envs/{env}/shims が後ろに行ってしまい、各 env で指定しているバージョンのものが使えなくなっています。
バッドノウハウ的ですが、ひとまず .zshenv に、
for env in $(find ~/.anyenv/envs -type d -d 1 -exec basename {} \;); do export PATH=~/.anyenv/envs/$env/shims:$PATH; done;
と設定して動くように設定しました。
MojoX::Session::Simple をリリースしました
Mojolicious を使ったことがあればわかると思いますが、 Mojolicious のセッション機構 Mojolicious::Sessions は、 Plack::Middleware で扱うセッションとは互換性がありません。
Plack::Builder で複数のアプリケーションをサーバにマウントする場合、セッションを簡単に共有できると便利と思っていた際に、 Plack::Middleware::Session::Simple という大変便利なモジュールがあったので、 Mojolicious と組み合わせて使いたいなあと思い、 MojoX::Session::Simple というモジュールを作ってみました。
このモジュールを使うと、従来の Mojolicious::Controller を継承した実装のまま、 セッション部分は Plack::Middleware::Session::Simple を通して保存したり復元したりすることができるようになります。
AnyEvent のお勉強
AnyEvent を使ってイベントループからコールバックを呼び出すようにすると、 非同期でコードを動かせるようになるというのは前から知っていたけど、 AnyEvent のドキュメントやコードを見てもちょっと難しくてピンとこないので、 実際に動くものを作ってみました。
IO::Socket::INET を使った簡単なサーバ
TCP とか Socket とか、全然わかっていないのですが、
use common::sense; use AnyEvent; use AnyEvent::Handle; use Data::Dumper; use IO::Socket::INET; my $sock = IO::Socket::INET->new( LocalPort => 5001, Listen => 5, Proto => 'tcp', ReuseAddr => 1, ) or die $!; printf "Socket created: %s\n", ref $sock; while (1) { local $| = 1; my $socket = $sock->accept; printf "Connected From %s:%s\n", $socket->peerhost, $socket->peerport; while (my $msg = $socket->getline) { $msg =~ s/\R//g; print "-> Got: ${msg}\n"; $socket->send("Got: ${msg}\015\012"); last if $msg =~ /^bye$/i; } $socket->close; print "Disconnected\n"; } 1;
コネクションを一つだけ受け付けるブロッキングなサーバは、こんな風に作れました。 これだと、複数の接続をさばけないので、あまり楽しくないです。
AnyEvent::Socket と AnyEvent::Handle を使ったイベント駆動サーバ
AnyEvent::Intro を読みながら、 Twiggyの実装例 や A simple chat server in AnyEvent の真似をしてみました。
use common::sense; use AnyEvent; use AnyEvent::Socket; use AnyEvent::Handle; my $cv = AnyEvent->condvar; my %clients; my %names; tcp_server undef, 5001, sub { my ( $fh, $host, $port ) = @_; print "Connection established: ${host}:${port}\n"; my $handle = AnyEvent::Handle->new( fh => $fh ); my $broadcast_cb = sub { my ( $fileno, $line ) = @_; $clients{$_}->{handle}->push_write($line) for grep { $_ ne $fileno } keys %clients; }; my $handle_disconnect_cb = sub { my $h = shift; my $fileno = fileno( $h->fh ); if ( my $client = delete $clients{ fileno( $h->fh ) } ) { my $name = $client->{name}; $broadcast_cb->( $fileno, "-> ${name} has left.\n", ); } $h->destroy; }; my $handle_connect_cb = sub { my $h = shift; my $fileno = fileno( $h->fh ); my $name = $clients{$fileno}->{name}; $h->push_write("-> Welcome, ${name}!\n"); $broadcast_cb->( $fileno, "-> ${name} has joined.\n" ); }; my $handle_read_cb = sub { my ( $h, $line ) = @_; my $fileno = fileno( $h->fh ); my $client = $clients{$fileno}; if ( !defined $client->{name} ) { if ( defined $names{$line} ) { $h->push_write("-> ${line} is taken, try again: "); } else { $names{$line} = 1; $client->{name} = $line; $handle_connect_cb->( $h, $client->{name} ); } } else { $broadcast_cb->( $fileno, $client->{name} . ": ${line}\n" ); } }; $handle->on_error($handle_disconnect_cb); $handle->on_eof($handle_disconnect_cb); $handle->on_read( sub { $_[0]->push_read( line => $handle_read_cb ) } ); #$handle->on_drain(sub { }); $handle->push_write("-> Your name is?: "); $clients{ fileno($fh) } = { name => undef, handle => $handle, }; }; $cv->recv;
こんな風に、 AnyEvent::Socket と AnyEvent::Handle を使ってできました。
どちらのモジュールについてもまだ全然わかっていないので、もっと触ってみようと思います。