2011年1月30日日曜日

Subversion導入時の注意点(改行コード、実行権限)その1

Subversion導入後にファイルの改行コードおよび実行権限で色々問題が発生したので、その時の対応をまとめたいと思います。

開発者はLinux、デザイナーさんはMac OS、HTMLのコーダーはWindowsみたいな状況って普通にあることだと思うんですが、このようなOSが混在する状況で、ちょっと困った問題が発生しました。

皆がそれぞれの環境で作成したファイルを、Subversionにコミットしていたんですが、ある日、Linux上でCheckoutしたPerlスクリプトファイルを実行しようとしたところ、次のようなエラーが発生しました。

$ ./test.pl
-bash: ./test.pl: Permission denied


あれれ、実行できない。
lsで確認すると。

$ ls -la test.pl
-rw-rw-r-- 1 yt yt 172 Jan 28 18:08 test.pl


おや?実行権限が無い。。。。
まぁいっか、chmodで実行権限を付与して実行してみよう。

$ chmod 755 test.pl
$ ./test.pl
-bash: ./test.pl: /usr/bin/perl^M: bad interpreter: No such file or directory


今度は意味不明なエラーが発生。
そこで、このファイルをvimエディタで開いてみると、ステータスバーに以下の表示。

"test.pl" [noeol][dos] 9L, 172C

どうやらファイルの改行コードがWindows(DOS)形式になっていたことが原因で、エラーが発生していたようです。

そこで、こちらを参考にして、vimのコマンド set ff=unix を使って、改行コードをUNIX形式に変換しました。
すると、エラーが発生しなくなりました。

なぜ、こんなことになったのか?
ということで調べたところ、このファイルは元々Windows上で作成し、WindowsのSubversionクライアントのTortoiseSVN経由でリポジトリにaddされたことがわかりました。

そもそもWindowsというOSにはUNIX系OSでいうところの実行ビットという概念が存在しないですし、改行コードも異なるので、なるほどこんな問題が発生しても不思議ではありません。

この問題をどうにか解決できないのかを調べていたところ、Subversionの日本語マニュアルにたどり着きました。
それによれば、Subversionはファイルやディレクトリに対して属性というメタデータを設定することができて、そのメタデータ自体もバージョン管理されるということでした。
さらに属性には、Subversion自身が使用している特殊な属性があり、それを使うとこの問題を解決できることがわかりました。
具体的には、svn:executable と svn:eol-style という属性名なんですが、それぞれの意味は、

svn:executable
ファイルに対してこの属性名が設定されていると、OSが対応していれば実行ビットが有効になる。
属性値は無い。

svn:eol-style=native
Checkoutする際に、ファイルに含まれる改行コードをCheckout先のOSに応じて変換する。
つまり、Windowsの場合は CRLF に変換され、UNIX系OSの場合は LF に変換されます。
逆にaddやcommitなどのコマンドでリポジトリにファイルを格納するときには、 オペレーティングシステムにはよらず、正規化された改行コード(LF)に変換されてリポジトリに格納されます。


だいたい上記のような意味だと私は解釈しました。
早速テストしてみました。実行結果はこんな感じです。

$ ls -la test.pl
-rw-rw-r-- 1 yt yt 23 Jan 28 18:59 test.pl
$ svn ps svn:executable '' test.pl ←svn:executableの設定
property 'svn:executable' set on 'test.pl'
$ ls -la test.pl
-rwxrwxr-x 1 yt yt 23 Jan 28 18:59 test.pl ←実行権限が付与された!
$ ./test.pl
-bash: ./test.pl: /bin/bash^M: bad interpreter: No such file or directory
$ svn ps svn:eol-style 'native' test.pl ←svn:eol-style=nativeの設定
property 'svn:eol-style' set on 'test.pl'
$ ./test.pl ←svn:eol-style=nativeを設定しただけではEOLの変換は行われないのでエラーになる
-bash: ./test.pl: /bin/bash^M: bad interpreter: No such file or directory
$ svn commit ←commitの段階でファイルのEOLがLFで正規化されてリポジトリに保存される
Sending test.pl
Transmitting file data .
Committed revision 31.
$ ./test.pl ←エラーが無くなり、実行可能になった!!
hello


という訳で、OSが混在する環境でSubversionを使用している場合は、
・実行ビットが必要なファイルには、svn:executable 属性を設定。
・テキストファイルには、svn:eol-style 属性に native という値を設定
しておけばよいという結論になりました。

次回のエントリでは、そもそもそういった問題が発生しないためには何をすればよかったのか?について書きたいと思います。

参考にしたURL:
http://advweb.seesaa.net/article/3074705.html
http://demo.zeera.jp/files/cubo/cubo-demo/document/01.Cubo-Manual/html/subversion/svn.advanced.props.html
http://cesare.seesaa.net/article/44146340.html

2011年1月18日火曜日

データの直列化(Data::Dumper vs JSON::XS vs Storable)

Perlのデータ構造を直列化(Serialize)したくて、いくつかのモジュールを試してみたので、その結果をこのエントリに記録しておきます。

そもそも今回やりたかったことは、
  1. Perlのハッシュを直列化する
  2. ただし、ハッシュの値はスカラーまたは配列のどちらか
  3. ある事情により、ハッシュキーでソートしてから直列化したい 
だったのですが、データ構造が複雑ではなかったので、最初は自分で直列化していました。でも、ソースがいまいちスッキリしなくて気持ち悪かったので、CPANモジュールに置き換えることにしたんです。

とは言っても、どのモジュールを使えばベストなのかで迷ったので、ベンチマークを取って、一番速いモジュールを採用することにしました。

use strict;
use warnings;
use Data::Dumper;
use JSON::XS;
use Storable 'nfreeze';
$Storable::canonical = 1;
use Benchmark qw(timethese  cmpthese);

my $hash = {
    'z' => 'z',
    'a' => 'a',
    'c' => [ '2','1'],
};

sub my_serialize {
    my $params = $hash;
    my $str = "{";
    foreach my $name ( sort(keys %{$params}) ) {
        if ( ref($params->{$name}) eq 'ARRAY' ) {
            $str .= "'$name'=>[";
            foreach my $value ( @{$params->{$name}} ) {
                $str .= "'$value',";
            }  
            $str .= '],';
        } else {
            $str .= "'$name'=>'$params->{$name}',";
        }  
    }  
    return $str . "}";
}

sub data_dumper {
    my $d = Data::Dumper->new([$hash]);
    return $d->Terse(1)->Indent(0)->Sortkeys(1)->Dump;
}

sub json_xs {
        return JSON::XS->new->canonical->encode($hash);
}

sub storable {
    return nfreeze($hash);
}

my $count = 1000000;
my $comp = timethese(
    $count,
    {
        my_serialize => \&my_serialize,
        data_dumper => \&data_dumper,
        jspn_xs => \&json_xs,
        storable => \&storable,
    }
);

cmpthese $comp;

上記のようなベンチマーク用のコードを用意して、実行してみると、、、


Benchmark: timing 1000000 iterations of data_dumper, jspn_xs, my_serialize, storable...
data_dumper: 43 wallclock secs (41.97 usr +  0.08 sys = 42.05 CPU) @ 23781.21/s (n=1000000)
   jspn_xs:  3 wallclock secs ( 2.04 usr +  0.00 sys =  2.04 CPU) @ 490196.08/s (n=1000000)
my_serialize:  6 wallclock secs ( 5.69 usr +  0.00 sys =  5.69 CPU) @ 175746.92/s (n=1000000)
  storable: 42 wallclock secs (29.85 usr + 12.66 sys = 42.51 CPU) @ 23523.88/s (n=1000000)
                 Rate     storable  data_dumper my_serialize      jspn_xs
storable      23524/s           --          -1%         -87%         -95%
data_dumper   23781/s           1%           --         -86%         -95%
my_serialize 175747/s         647%         639%           --         -64%
jspn_xs      490196/s        1984%        1961%         179%           --

上記のような結果になりました。つまり、今回の条件下では、JSON::XSがData::DumperやStorableの20倍高速に動作することがわかりました。

という訳で、JSON::XSを使うことに決定。

2011年1月13日木曜日

DBIx::Class(DBIC)で既存のメソッドを上書きしたい。けどできない。

自分の開発しているWebアプリケーションは、DBIx::Class(DBIC)を使っているのですが、少しハマったので、調査したことを書きます。

そもそもやりたかったことは、、、
テーブルに対して、DBIC経由でUPDATEなどのDML文が発行された時に、
自動的にmemcachedに保存されているキャッシュを削除させたかったのです。
そうすることで、キャッシュの削除を透過的に行うことができて、嬉しいかなと思いました。

そこで、該当テーブルのupdateメソッドを上書きすることにしました。

package DBIC::DB1::Result::Table;

use strict;
use warnings;
use base 'DBIx::Class';

(中略)

sub update {
    my $self = shift;
    $self->next::method( @_ );
    $self->delete_cache();
}

1;


こんな感じでupdateメソッドを上書きしておけば、UPDATEのタイミングでdelete_cache()も実行されるハズと思っていたのですが、上手くいかない場合がありました。

#!/usr/bin/perl

$ENV{'DBIC_TRACE'} = 1;

use strict;
use warnings;
use lib '/MyAPP/lib/
use YAML::Syck;
use DBIC::DB1;
use utf8;

$YAML::Syck::ImplicitUnicode = 1;
my $configfile = '/MyAPP/MyAPP.yml';
my $c = YAML::Syck::LoadFile($configfile);

my $row = DBIC::DB1->connect(
    @{$c->{'Model::DB1'}->{connect_info}}
)->resultset('Table')->find(1);
$row->update({ col1 => '' });        #上書きしたメソッドが実行される

my $rows = DBIC::DB1->connect(
    @{$c->{'Model::DB1'}->{connect_info}}
)->resultset('Table')->search({ item_id => { 'in' => ['1','2'] } });
$rows->update({ col1 => '' });       #上書きしたメソッドが実行されない


上記のテストスクリプトを実行すると、findメソッドなどで取得した単一レコードに対してupdateを実行すると、上書きしたupdateメソッドが実行されるのですが、searchメソッドなどで取得した複数行のレコードに対してupdateメソッドを発行した場合、上書きしたupdateメソッド(DBIC::DB1::Result::Table内のupdateメソッド)が呼ばれていないことがわかりました。

そこで、findおよびsearchメソッドが返すオブジェクトを調べてみることにしました。
その結果、findが返すのは、DBIC::DB1::Result::Tableオブジェクト、一方、searchが返すのは、DBIx::Class::ResultSetでした。

そこで、DBIx::Class::ResultSetをGoogle検索したところ、DBIx::Class::Manual::Cookbook - 様々なレシピというページが見つかったので、ここを参考にして次のようなパッケージを新しく作成しました。

package DBIC::DB1::ResultSet::Table;

use strict;
use warnings;
use base 'DBIx::Class::ResultSet';

sub update {
    my $self = shift;
    $self->next::method( @_ );
    $self->delete_cache();
}

1;


そして先ほどのテストスクリプトを実行すると。。。出来た!うまくいきました。
つまり、単一レコード(Result)に対するupdateメソッドと複数レコード(ResultSet)に対するupdateメソッドは、それぞれ別に定義してあげないといけなかったのでした。

なお、先ほどのDBIx::Class::Manual::Cookbook - 様々なレシピというページには、
パッケージ DBIC::DB1::Result::Table に対して、以下のコードを追加するよう書かれていますが、これはなくてもOKでした。

__PACKAGE__->resultset_class('DBIC::DB1::ResultSet::Table');

最初は理由がわからなかったのですが、色々調べてみると自分の環境では、パッケージ DBIC::DB1において、

__PACKAGE__->load_namespaces;

しているからでした。
これをしておくと、DB1/Result、DB1/ResultSet 以下に配置したパッケージが自動的に認識されるみたいです。こりゃ便利。

以下参考にしたURLです。
http://www.mail-archive.com/dbix-class@lists.scsys.co.uk/msg04797.html
http://search.cpan.org/~arcanez/DBIx-Class-0.08126/lib/DBIx/Class/Schema.pm#load_namespaces
http://gihyo.jp/dev/serial/01/perl-hackers-hub/000303