Q4M を使ってる時のシグナル処理に注意B!

はじめに

今日は趣向を変えて、 tips さんを。

Q4M だけに限った話ではなく SELECT SLEEP(10); とかしてる時でも良いんですが、今回は Q4M を使ってる時の注意という話です。

Data::Model の Q4M 対応では、もちろん DBI を使って Q4M を使うわけですが、これとシグナルを組み合わせるとシグナルを送った直後にシグナルハンドラを呼ばないんですね。

例えばこんなの

    # こいつは、 Q4M の queue_wait が終わるまでシグナルをトラップしない
    local $SIG{INT} = sub { warn "int" };
    $queue->queue_running( queue_test => sub {}, timeout => 5 );

何故かというと perl のメインループは以下のような実装で

int
Perl_runops_standard(pTHX)
{
    dVAR;
    while ((PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX))) {
        PERL_ASYNC_CHECK();
    }

    TAINT_NOT;
    return 0;
}

一般的なケースだと (PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX)) してるところで、実際のオペコードを実行して PERL_ASYNC_CHECK() のとこで、オペコード実行中に受け取ったシグナルに対するハンドラを処理してるのです。

ようするに、オペコード実行中にシグナルを受け取っても即座に処理してないんですよね。

DBI をはじめとする XS だと、一度 XS のコードを呼ばれると処理が終わるまでは PERL_ASYNC_CHECK() が実行されないため、今回のように即座にハンドラが呼ばれないということなんです。

解決法

POSIX の sigaction を使えば Perl の世界のシグナル処理をすっ飛ばして、 low level なところでシグナルをトラップしてくれるのでなんとかなります。これは DBI のドキュメントにも書かれています。

    # DBI のドキュメントを参考にしたよ
    # これはトラップする
    use POSIX ':signal_h';

    # シグナル設定
    my $mask = POSIX::SigSet->new( SIGINT );
    my $action = POSIX::SigAction->new(sub { warn "int" }, $mask);

    # local $SIG{INT} = sub {} 相当の事をするので、もともとのシグナルを覚えとく
    my $oldaction = POSIX::SigAction->new();
    sigaction(SIGINT, $action, $oldaction);

    $queue->queue_running( queue_test => sub {}, timeout => 5 );

    # もともとのシグナルに戻す
    sigaction(SIGINT, $oldaction);

もうちょっとスマートに

上のコードは、スコープの中だけシグナルを変えようとして頑張ってるので、いろいろ処理が面倒です。

そういう時は Sys::SigAction を使うと良いでしょう。 POSIX::SigAction をうまいぐわいにラップして、 Sys::SigAction のオブジェクトの DESTROY が呼ばれるタイミングで sigaction(SIGINT, $oldaction); をやってくれます。

ほとんど local $SIG{INT} と同じ感じです。

    # せいかい
    use Sys::SigAction qw( set_sig_handler );
    my $h = set_sig_handler( 'INT', sub { warn "int" }, { flags => SA_RESTART });
    $queue->queue_running( queue_test => sub {}, timeout => 5 );

まとめ

今日は、長時間ブロックする SQL を扱いつつシグナルをハンドリングしたくなった時の tips を紹介しました。

明日は、またチュートリアル的な事をしましょう。