$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

DBIのconnect_cachedのいろいろ

 最近DBへの接続をリクエスト単位ではなくリクエストを処理するプロセス単位(Starmanのworkerプロセス単位)でキャッシュしたいということがあって、DBIのconnect_cachedを使うことになった。Scope::Container::DBIでももしかしたら出来るのかもしれないけど、とりあえずconnect_cachedで実装した。そこでいろいろはまったのでメモ。

connect_cachedについて

 perldocに

connect_cached is like "connect", except that the database handle returned is also stored in a hash associated with the given parameters. If another call is made to connect_cached with the same parameter values, then the corresponding cached $dbh will be returned if it is still valid. The cached database handle is replaced with a new connection if it has been disconnected or if the ping method fails.

とあるように、connect_cachedはDBへのコネクションを渡されたパラメータにもとづいてキャッシュしておいてくれるというもの。しかしいろいろな問題があって、みんなハマるということをしている気がする。

forkに関する問題

 これに関しては以下の2つの記事が詳しいので、そちらを参照すると良いと思う。AutoInactiveDestroyとか難しい。

transactionに関する問題

 これも続DBIとforkの関係 - Articles Advent Calendar 2011 Dbixの最後の方にかいてある。簡単に言うと接続がキャッシュされて使い回されるから、トランザクションを実行している間に呼び出した関数が内部でconnect_cached使ってSQLを発行するとおかしなことになるという話。

渡す引数に関する問題

 さて、ここからが最近はまったところのメモ。connect_cachedは渡されたパラメータに基づきキャッシュすると書いてある。で、connect_cachedに渡せるのはパラメータは以下の通り。

DBI->connect_cached($dsn, $username, $password, \%attr);

 で、気をつける点として%attrの中に含まれるリファレンス(ハッシュや関数など)の中身を考慮してキャッシュはしてくれないということがある*1

Callbacksに関する問題

 DBIにはオプションとしてCallbacksというものを渡せるのだが、これを指定した場合おかしなことになる。

sub dbh {
    my $dbh = DBI->connect_cached($dsn, $username, $password, {
        Callbacks => {
            connected => sub {
                my $dbh = shift;
                $dbh->do("SET NAMES utf8") or warn $dbh->errstr;
                return;
            }
        }
    });
}

 これを使うと、dbhを呼び出すたびに、毎回ハッシュリファレンスが新しく作られてconnect_cachedのCallbacksに渡される。しかしconnect_cachedがキャッシュのキーとして使うときには、ハッシュリファレンスの中身は考慮せずstringify化されたものをキーとして利用する。例えばHASH(0x7fc68c0060b8)みたいなやつ。
 しかし、これはハッシュリファレンスが作られるたびに毎回変わってしまう*2

 これらの理由により、今回のdbh関数を呼ぶと毎回別のコネクションと見なされ毎回接続された上に、そのコネクションが全てキャッシュされていくため、どこかのタイミングでToo many connectionsと言われ死ぬようになる。

 解決方法としてはCallbacksをグローバルに持っておく、もしくはサブクラスでconnectedメソッドを定義するなどがある。

Callbacksをグローバルに持っておく

 DBIのドキュメントにも書いてありますが、こんな感じ。

my $cb = {
    connected => sub {
        my $dbh = shift;
        $dbh->do("SET NAMES utf8") or warn $dbh->errstr;
    }
};
sub dbh {
    my $dbh = DBI->connect_cached($dsn, $username, $password, {
        Callbacks => $cb,
    });
}

 stateを使えば、subの中だけで完結できる。

sub dbh {
    state $cb = {
        connected => sub {
            my $dbh = shift;
            $dbh->do("SET NAMES utf8") or warn $dbh->errstr;
        }
    };

    my $dbh = DBI->connect_cached($dsn, $username, $password, {
        Callbacks => $cb,
    });
}
サブクラスでconnectedメソッドを定義する

connectedだけCallbacksを呼びたければDBI::dbのconnectedメソッドを上書きすれば良い。下のような感じ。

package My::DBI;
use strict;
use warnings;
use parent 'DBI';

package My::DBI::db;
use strict;
use warnings;
use parent -norequire => 'DBI::db';

sub connected {
    my ($dbh) = @_;
    $dbh->do("SET NAMES utf8") or WARN $dbh->errstr;
    $dbh->SUPER::connected(@_);
}

1;

あとはMy::DBI->connect_cachedを呼んであげるだけ。

my $dbh = My::DBI->connect_cached($dsn, $user, $pass, $attr);

最後に

 今回はDBIのconnect_cachedで自分がはまったところについてまとめてみました。今回はプロセス単位でconnectionをキャッシュしたかったのでconnect_cachedを使いましたが、リクエスト単位でキャッシュしたい場合は単純にScope::Container::DBIPlack::Middleware::Scope::Containerを使うと良いと思います。

追記

connect_cached使わないほうが良いという意見もあるので、良ければどのような理由があって使わないほうがいいか誰か教えて欲しいです!

*1:挙動だけ確認されて、その後DBIのコード眺めたらよく分からなかったので、もしかしたら意図は違うかもしれない

*2:データの格納位置が保存されているため