
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が終わるまでに、commit
かrollback
が呼んでなければ、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 留言:
發佈留言