ref $coderef eq 'CODE' or $self->throw_exception
('$coderef must be a CODE reference');
- return $coderef->(@_) if $self->{transaction_depth} && ! $self->auto_savepoint;
-
local $self->{_in_dbh_do} = 1;
my @result;
try {
$self->txn_begin;
+ my $txn_start_depth = $self->transaction_depth;
if($want_array) {
@result = $coderef->(@$args);
}
else {
$coderef->(@$args);
}
- $self->txn_commit;
+
+ my $delta_txn = $txn_start_depth - $self->transaction_depth;
+ if ($delta_txn == 0) {
+ $self->txn_commit;
+ }
+ elsif ($delta_txn != 1) {
+ # an off-by-one would mean we fired a rollback
+ carp "Unexpected reduction of transaction depth by $delta_txn after execution of $coderef";
+ }
} catch {
$exception = $_;
};
if(! defined $exception) { return $want_array ? @result : $result[0] }
- if($tried++ || $self->connected) {
+ if($self->transaction_depth > 1 || $tried++ || $self->connected) {
my $rollback_exception;
try { $self->txn_rollback } catch { $rollback_exception = shift };
if(defined $rollback_exception) {
}
sub _svp_generate_name {
- my ($self) = @_;
-
- return 'savepoint_'.scalar(@{ $self->{'savepoints'} });
+ my ($self) = @_;
+ return 'savepoint_'.scalar(@{ $self->{'savepoints'} });
}
sub txn_begin {
# this means we have not yet connected and do not know the AC status
# (e.g. coderef $dbh)
- $self->ensure_connected if (! defined $self->_dbh_autocommit);
+ if (! defined $self->_dbh_autocommit) {
+ $self->ensure_connected;
+ }
+ # otherwise re-connect on pid changes, so
+ # that the txn_depth is adjusted properly
+ # the lightweight _get_dbh is good enoug here
+ # (only superficial handle check, no pings)
+ else {
+ $self->_get_dbh;
+ }
- if($self->{transaction_depth} == 0) {
+ if($self->transaction_depth == 0) {
$self->debugobj->txn_begin()
if $self->debug;
$self->_dbh_begin_work;
$self->svp_release
if $self->auto_savepoint;
}
+ else {
+ $self->throw_exception( 'Refusing to commit without a started transaction' );
+ }
}
sub _dbh_commit {
$num_children = 10;
}
-plan tests => $num_children + 6;
+plan tests => ($num_children*2) + 6;
use lib qw(t/lib);
$pid = $$;
- my $child_rs = $schema->resultset('CD')->search({ year => 1901 });
- my $row = $parent_rs->next;
- if($row && $row->get_column('artist') =~ /^(?:123|456)$/) {
- $schema->resultset('CD')->create({ title => "test success $pid", artist => $pid, year => scalar(@pids) });
+ my $work = sub {
+ my $child_rs = $schema->resultset('CD')->search({ year => 1901 });
+ my $row = $parent_rs->next;
+ $schema->resultset('CD')->create({ title => "test success $pid", artist => $pid, year => scalar(@pids) })
+ if($row && $row->get_column('artist') =~ /^(?:123|456)$/);
+ };
+
+ # try with and without transactions
+ if ((@pids % 3) == 1) {
+ my $guard = $schema->txn_scope_guard;
+ $work->();
+ $guard->commit;
}
+ elsif ((@pids % 3) == 2) {
+ $schema->txn_do ($work);
+ }
+ else {
+ $work->();
+ }
+
sleep(3);
- exit;
+ exit 0;
}
ok(1, "past forking");
-waitpid($_,0) for(@pids);
+for (@pids) {
+ waitpid($_,0);
+ ok (! $?, "Child $_ exitted cleanly");
+};
ok(1, "past waiting");
is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
}
+# test nested txn_begin on fresh connection
+{
+ my $schema = DBICTest->init_schema(sqlite_use_file => 1, no_deploy => 1);
+ $schema->storage->ensure_connected;
+
+ is ($schema->storage->transaction_depth, 0, 'Start outside txn');
+
+ my @pids;
+ for my $action (
+ sub {
+ my $s = shift;
+ die "$$ starts in txn!" if $s->storage->transaction_depth != 0;
+ $s->txn_do ( sub {
+ die "$$ not in txn!" if $s->storage->transaction_depth == 0;
+ $s->storage->dbh->do('SELECT 1') }
+ );
+ die "$$ did not finish txn!" if $s->storage->transaction_depth != 0;
+ },
+ sub {
+ $_[0]->txn_begin;
+ $_[0]->storage->dbh->do('SELECT 1');
+ $_[0]->txn_commit
+ },
+ sub {
+ my $guard = $_[0]->txn_scope_guard;
+ $_[0]->storage->dbh->do('SELECT 1');
+ $guard->commit
+ },
+ ) {
+ push @pids, fork();
+ die "Unable to fork: $!\n"
+ if ! defined $pids[-1];
+
+ if ($pids[-1]) {
+ next;
+ }
+
+ $action->($schema);
+ exit 0;
+ }
+
+ is ($schema->storage->transaction_depth, 0, 'Parent still outside txn');
+
+ for my $pid (@pids) {
+ waitpid ($pid, 0);
+ ok (! $?, "Child $pid exit ok");
+ }
+}
+
+# Test txn_do/scope_guard with forking: outer txn_do
+{
+ my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
+
+ for my $pass (1..2) {
+
+ # do something trying to destabilize the depth count
+ for (1..2) {
+ eval {
+ my $guard = $schema->txn_scope_guard;
+ $schema->txn_do( sub { die } );
+ };
+ $schema->txn_do( sub {
+ ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)");
+ });
+ }
+
+ for my $pid ( $schema->txn_do ( sub { _forking_action ($schema) } ) ) {
+ waitpid ($pid, 0);
+ ok (! $?, "Child $pid exit ok (pass $pass)");
+ isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row');
+ }
+ }
+}
+
+# same test with outer guard
+{
+ my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
+
+ for my $pass (1..2) {
+
+ # do something trying to destabilize the depth count
+ for (1..2) {
+ eval {
+ my $guard = $schema->txn_scope_guard;
+ $schema->txn_do( sub { die } );
+ };
+ $schema->txn_do( sub {
+ ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)");
+ });
+ }
+
+ my @pids;
+ my $guard = $schema->txn_scope_guard;
+ _forking_action ($schema);
+ $guard->commit;
+
+ for my $pid (@pids) {
+ waitpid ($pid, 0);
+ ok (! $?, "Child $pid exit ok (pass $pass)");
+ isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row');
+ }
+ }
+}
+
+sub _forking_action {
+ my $schema = shift;
+
+ my @pids;
+ while (@pids < 5) {
+
+ push @pids, fork();
+ die "Unable to fork: $!\n"
+ if ! defined $pids[-1];
+
+ if ($pids[-1]) {
+ next;
+ }
+
+ if (@pids % 2) {
+ $schema->txn_do (sub {
+ my $depth = $schema->storage->transaction_depth;
+ die "$$(txn_do)unexpected txn depth $depth!" if $depth != 1;
+ $schema->resultset ('Artist')->create ({ name => "forking action $$"});
+ });
+ }
+ else {
+ my $guard = $schema->txn_scope_guard;
+ my $depth = $schema->storage->transaction_depth;
+ die "$$(scope_guard) unexpected txn depth $depth!" if $depth != 1;
+ $schema->resultset ('Artist')->create ({ name => "forking action $$"});
+ $guard->commit;
+ }
+
+ exit 0;
+ }
+
+ return @pids;
+}
+
my $fail_code = sub {
my ($artist) = @_;
$artist->create_related('cds', {
# make sure the guard does not eat exceptions
{
- my $schema = DBICTest->init_schema();
+ my $schema = DBICTest->init_schema;
+
+ no strict 'refs';
+ no warnings 'redefine';
+ local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
+
throws_ok (sub {
my $guard = $schema->txn_scope_guard;
$schema->resultset ('Artist')->create ({ name => 'bohhoo'});
- $schema->storage->disconnect; # this should freak out the guard rollback
+ # this should freak out the guard rollback
+ # but it won't work because DBD::SQLite is buggy
+ # instead just install a toxic rollback above
+ #$schema->storage->_dbh( $schema->storage->_dbh->clone );
die 'Deliberate exception';
}, qr/Deliberate exception.+Rollback failed/s);
{
my $schema = DBICTest->init_schema();
- # something is really confusing Test::Warn here, no time to debug
+ no strict 'refs';
+ no warnings 'redefine';
+ local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
+
+#The warn from within a DESTROY callback freaks out Test::Warn, do it old-school
=begin
warnings_exist (
sub {
my $guard = $schema->txn_scope_guard;
$schema->resultset ('Artist')->create ({ name => 'bohhoo'});
- $schema->storage->disconnect; # this should freak out the guard rollback
+ # this should freak out the guard rollback
+ # but it won't work because DBD::SQLite is buggy
+ # instead just install a toxic rollback above
+ #$schema->storage->_dbh( $schema->storage->_dbh->clone );
},
[
qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
);
=cut
+# delete this once the above works properly (same test)
my @want = (
qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
{
my $guard = $schema->txn_scope_guard;
$schema->resultset ('Artist')->create ({ name => 'bohhoo'});
-
- $schema->storage->disconnect; # this should freak out the guard rollback
}
is (@w, 2, 'Both expected warnings found');