Perl: TengのRollbackにScopeGuardの活用

TengのRollbackはScopeGuardの活用に基づきます。
sub transaction {

    Carp::croak('scope is not set correctly') unless in_scope_container();
    my ($self, $code) = @_;

    #transactionのscope guard objectを作る
    #Begin Transaction
    my $db = $self->db('Master');
    my $txn = $db->txn_scope;

    #queryを実行する
    my $ret = $code->($self);

    #$dismissを1にする
    #Commit
    $txn->commit;

    return $ret;

    #transactionのscope guard objectがDestroyする
    #$dismissが1の場合は、何もしない、普通にreturnする
    #$dismissが2の場合は、rollbackする
}
Perlにも不用になったメモリを回収するgarbage collectionがあります。 オブジェクトに対する最後のリファレンスが消滅した時(またはプログラムの終了時)、そのオブジェクトは自動的に破棄されます。
ScopeGuardも同じ原理です。 ここのScopeGuardはオブジェクトが解放される直前に制御を横取って、DESTROYというメソッドを定義します。
$dismissが1の場合は普通に解放します。$dismissが0の場合はrollbackを追加します。 これで、Transactionが終わるまでに、commitrollbackが呼んでなければ、rollbackを実行します。
package DBIx::TransactionManager::ScopeGuard;
use Try::Tiny;
use Data::Dumper;

sub new {
    my($class, $obj, %args) = @_;
    my $caller = $args{caller} || [ caller(1) ];
    $obj->txn_begin( caller => $caller );
    bless [ 0, $obj, $caller, $$ ], $class;
}

sub commit {
    return if $_[0]->[0]; # do not run twice.
    $_[0]->[1]->txn_commit;
    $_[0]->[0] = 1;
}

sub DESTROY {
    my($dismiss, $obj, $caller, $pid) = @{ $_[0] };
    return if $dismiss;
    
    if ( $$ != $pid ) {
        return;
    }
    
    warn( "Transaction was aborted without calling an explicit commit or rollback." );

    try {
        $obj->txn_rollback;
    } catch {
        die "Rollback failed: $_";
    };
}
また、scope内でqueryを実行する時exceptionがあったら、 executeが中止して$sthの参照を失ってしまったと判別したから、DESTROYを呼びます。 Destroyの$callerはexceptionが投げる場所を表明するために設定しました。
package Teng;
our $SQL_COMMENT_LEVEL = 0;
sub execute {
    my ($self, $sql, $binds) = @_;
    if ($ENV{TENG_SQL_COMMENT} || $self->sql_comment) {
        my $i = $SQL_COMMENT_LEVEL; # optimize, as we would *NEVER* be called
        while ( my (@caller) = caller($i++) ) {
            next if ( $caller[0]->isa( __PACKAGE__ ) );
            next if $caller[0] =~ /^Teng::/; # skip Teng::Row, Teng::Plugin::* etc.
            my $comment = "$caller[1] at line $caller[2]";
            $comment =~ s/\*\// /g;
            $sql = "/* $comment */\n$sql";
            last;
        }
    }

    my $sth;
    eval {
        $sth = $self->dbh->prepare($sql);
        my $i = 1;
        for my $v ( @{ $binds || [] } ) {
            if (Scalar::Util::blessed($v) && ref($v) eq 'SQL::Maker::SQLType') {
               $sth->bind_param($i++, ${$v->value_ref}, $v->type);
            } else {
                # allow array ref for using pg_types. e.g. [ $value => { pg_type => PG_BYTEA } ]
                # ref. https://metacpan.org/pod/DBD::Pg#quote
                $sth->bind_param( $i++, ref($v) eq 'ARRAY' ? @$v : $v );
            }
        }
        $sth->execute();
    };

    if ($@) {
        $self->handle_error($sql, $binds, $@);
    }
    return $sth;
}
ちなみに、リファレンスとしての$_[0]は変更できないが、${$_[0]@{$_[0]}は変更できます。 このメソッドは適切な時期に自動的に呼び出され、クリーンアップを行うこともできます。
package DBIx::TransactionManager;
sub _txn_end {
    @{$_[0]->{active_transactions}} = ();
    $_[0]->{rollbacked_in_nested_transaction} = 0;
}
最後に注意するべきなことは、GuardScopeで処理したexceptionはエラー信号になれないです。 Controllerでcatchした信号はHandle_errorで投げたCroak::Carpです。

なぜScopeGuardを使いますか?

またGuard Scopeを使うの理由について考えました。やはりGuard Scopeを使うのはBeginとCommitというペアな関係性が大きいです。万一BeginとCommitの間に例外が発生するか不注意でreturnするか、Commitは実行されなくなって、中身のコードやデータがリークされるかもしれません。 それで、作者はResource Acquisition Is Initialization(RAII)のようなことに基づいて、Contructorでresourceを獲得するたびに、scopeの正常終了時か例外時にdestructorでresourceを解放する同時にrollbackという行動を判別します。 しかし、Guard Scopeはresourceの解放(Destroy)に依頼過ぎかもしれません。Scopeのobjectは常に解放するものですが、これはあまり柔軟ではないことです。例外が送出された場合はリソースを解放するが、送出されなかった場合にはリソースを解放したくない可能性があるかもしれません。それで、rollbackもできないし、commitもできないです。 以上の状況は循環参照と言います。そして、ここの対策はCommitのところにclean upを設定すること(Guard ScopeのところにblessしたTengのobjectが解放する)です。resource解放のことを確実に走らせるようにします。さらにPerlの場合は「 global destruction」というphaseがあります。このタイミングでこれらの循環参照されたobjectの解放が行われます。循環参照されているobjectだといってもresource解放はきちんとされるので、問題ないだと思います。 他にある考えるべきなことはrollbackを実行する時の例外かな。だが、そこもDestroyでtry catchしましたので、心配ないだと思います。

0 留言:

發佈留言