Skip to content

Commit

Permalink
Fix TxnScopeGuard misbehaving on externally set $@ without inner exce…
Browse files Browse the repository at this point in the history
…ptions

The guard is supposed to warn when it goes out of scope without a commit.
However this doesn't work if $@ was already set before the guard was created
and there were no eval{}s to clear it between definition and destruction.

Fixing this by storing a weakref to the current exception (if any) and
disregarding the contents of $@ if they match the ref we started with.
  • Loading branch information
ribasushi committed Nov 25, 2011
1 parent 45638ae commit f62c572
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 15 deletions.
2 changes: 2 additions & 0 deletions Changes
Expand Up @@ -15,6 +15,8 @@ Revision history for DBIx::Class
- The internal carp module now correctly skips CAG frames when
reporting a callsite
- Fix test failures on perl < 5.8.7 and new Package::Stash::XS
- Fix TxnScopeGuard not behaving correctly when $@ is set at the
time of $guard instantiation

* Misc
- No longer depend on Variable::Magic now that a pure-perl
Expand Down
54 changes: 41 additions & 13 deletions lib/DBIx/Class/Storage/TxnScopeGuard.pm
Expand Up @@ -3,7 +3,7 @@ package DBIx::Class::Storage::TxnScopeGuard;
use strict;
use warnings;
use Try::Tiny;
use Scalar::Util qw/weaken blessed/;
use Scalar::Util qw/weaken blessed refaddr/;
use DBIx::Class;
use DBIx::Class::Exception;
use DBIx::Class::Carp;
Expand All @@ -14,9 +14,25 @@ my ($guards_count, $compat_handler, $foreign_handler);
sub new {
my ($class, $storage) = @_;

my $guard = {
inactivated => 0,
storage => $storage,
};

# we are starting with an already set $@ - in order for things to work we need to
# be able to recognize it upon destruction - store its weakref
# recording it before doing the txn_begin stuff
if (defined $@ and $@ ne '') {
$guard->{existing_exception_ref} = (ref $@ ne '') ? $@ : \$@;
weaken $guard->{existing_exception_ref};
}

$storage->txn_begin;
my $guard = bless [ 0, $storage, $storage->_dbh ], ref $class || $class;

$guard->{dbh} = $storage->_dbh;
weaken $guard->{dbh};

bless $guard, ref $class || $class;

# install a callback carefully
if (DBIx::Class::_ENV_::INVISIBLE_DOLLAR_AT and !$guards_count) {
Expand Down Expand Up @@ -53,19 +69,21 @@ sub new {

$guards_count++;

weaken ($guard->[2]);
$guard;
}

sub commit {
my $self = shift;

$self->[1]->txn_commit;
$self->[0] = 1;
$self->{storage}->throw_exception("Refusing to execute multiple commits on scope guard $self")
if $self->{inactivated};

$self->{storage}->txn_commit;
$self->{inactivated} = 1;
}

sub DESTROY {
my ($dismiss, $storage) = @{$_[0]};
my $self = shift;

$guards_count--;

Expand All @@ -90,24 +108,34 @@ sub DESTROY {
undef $foreign_handler;
}

return if $dismiss;
return if $self->{inactivated};

# if our dbh is not ours anymore, the weakref will go undef
$storage->_verify_pid;
return unless $_[0]->[2];
# if our dbh is not ours anymore, the $dbh weakref will go undef
$self->{storage}->_verify_pid;
return unless $self->{dbh};

my $exception = $@;
my $exception = $@ if (
defined $@
and
$@ ne ''
and
(
! defined $self->{existing_exception_ref}
or
refaddr( ref $@ eq '' ? \$@ : $@ ) != refaddr($self->{existing_exception_ref})
)
);

{
local $@;

carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back.'
unless $exception;
unless defined $exception;

my $rollback_exception;
# do minimal connectivity check due to weird shit like
# https://rt.cpan.org/Public/Bug/Display.html?id=62370
try { $storage->_seems_connected && $storage->txn_rollback }
try { $self->{storage}->_seems_connected && $self->{storage}->txn_rollback }
catch { $rollback_exception = shift };

if ( $rollback_exception and (
Expand Down
7 changes: 5 additions & 2 deletions t/storage/txn_scope_guard.t
Expand Up @@ -116,7 +116,9 @@ use DBICTest;
}

# make sure it warns *big* on failed rollbacks
{
# test with and without a poisoned $@
for my $poison (0,1) {

my $schema = DBICTest->init_schema();

no strict 'refs';
Expand Down Expand Up @@ -160,11 +162,12 @@ use DBICTest;
}
};
{
eval { die 'GIFT!' if $poison };
my $guard = $schema->txn_scope_guard;
$schema->resultset ('Artist')->create ({ name => 'bohhoo'});
}

is (@w, 2, 'Both expected warnings found');
is (@w, 2, 'Both expected warnings found' . ($poison ? ' (after $@ poisoning)' : '') );

# just to mask off warning since we could not disconnect above
$schema->storage->_dbh->disconnect;
Expand Down

0 comments on commit f62c572

Please sign in to comment.