Workaround for double-call of destructors (based on 3d56e026 and e1d9e578)
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / TxnScopeGuard.pm
CommitLineData
6936e902 1package DBIx::Class::Storage::TxnScopeGuard;
1bc193ac 2
3use strict;
4use warnings;
ed7ab0f4 5use Try::Tiny;
d63c9e64 6use Scalar::Util qw(weaken blessed refaddr);
b4ad8a74 7use DBIx::Class;
d63c9e64 8use DBIx::Class::_Util qw(is_exception detected_reinvoked_destructor);
70c28808 9use DBIx::Class::Carp;
9c1700e3 10use namespace::clean;
11
1bc193ac 12sub new {
13 my ($class, $storage) = @_;
14
f62c5724 15 my $guard = {
16 inactivated => 0,
17 storage => $storage,
18 };
19
20 # we are starting with an already set $@ - in order for things to work we need to
21 # be able to recognize it upon destruction - store its weakref
22 # recording it before doing the txn_begin stuff
6e102c8f 23 #
24 # FIXME FRAGILE - any eval that fails but *does not* rethrow between here
25 # and the unwind will trample over $@ and invalidate the entire mechanism
26 # There got to be a saner way of doing this...
841efcb3 27 if (is_exception $@) {
6e102c8f 28 weaken(
841efcb3 29 $guard->{existing_exception_ref} = (ref($@) eq '') ? \$@ : $@
6e102c8f 30 );
f62c5724 31 }
32
1bc193ac 33 $storage->txn_begin;
1f870d5a 34
6e102c8f 35 weaken( $guard->{dbh} = $storage->_dbh );
f62c5724 36
37 bless $guard, ref $class || $class;
1f870d5a 38
7d216b10 39 $guard;
1bc193ac 40}
41
42sub commit {
43 my $self = shift;
44
f62c5724 45 $self->{storage}->throw_exception("Refusing to execute multiple commits on scope guard $self")
46 if $self->{inactivated};
47
48 $self->{storage}->txn_commit;
49 $self->{inactivated} = 1;
1bc193ac 50}
51
52sub DESTROY {
d63c9e64 53 return if &detected_reinvoked_destructor;
54
f62c5724 55 my $self = shift;
1bc193ac 56
f62c5724 57 return if $self->{inactivated};
3b7f3eac 58
f62c5724 59 # if our dbh is not ours anymore, the $dbh weakref will go undef
0d8817bc 60 $self->{storage}->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
f62c5724 61 return unless $self->{dbh};
7d216b10 62
f62c5724 63 my $exception = $@ if (
841efcb3 64 is_exception $@
f62c5724 65 and
66 (
67 ! defined $self->{existing_exception_ref}
68 or
841efcb3 69 refaddr( ref($@) eq '' ? \$@ : $@ ) != refaddr($self->{existing_exception_ref})
f62c5724 70 )
71 );
c6e27318 72
a778f387 73 {
74 local $@;
36099e8c 75
76 carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back.'
f62c5724 77 unless defined $exception;
36099e8c 78
ed7ab0f4 79 my $rollback_exception;
7d216b10 80 # do minimal connectivity check due to weird shit like
81 # https://rt.cpan.org/Public/Bug/Display.html?id=62370
f62c5724 82 try { $self->{storage}->_seems_connected && $self->{storage}->txn_rollback }
ed7ab0f4 83 catch { $rollback_exception = shift };
c6e27318 84
90d7422f 85 if ( $rollback_exception and (
86 ! defined blessed $rollback_exception
87 or
88 ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
89 ) ) {
1f870d5a 90 # append our text - THIS IS A TEMPORARY FIXUP!
91 # a real stackable exception object is in the works
92 if (ref $exception eq 'DBIx::Class::Exception') {
93 $exception->{msg} = "Transaction aborted: $exception->{msg} "
94 ."Rollback failed: ${rollback_exception}";
95 }
96 elsif ($exception) {
36099e8c 97 $exception = "Transaction aborted: ${exception} "
c6e27318 98 ."Rollback failed: ${rollback_exception}";
36099e8c 99 }
100 else {
101 carp (join ' ',
102 "********************* ROLLBACK FAILED!!! ********************",
103 "\nA rollback operation failed after the guard went out of scope.",
104 'This is potentially a disastrous situation, check your data for',
105 "consistency: $rollback_exception"
106 );
107 }
c6e27318 108 }
3b7f3eac 109 }
36099e8c 110
5815ffb0 111 $@ = $exception;
1bc193ac 112}
113
1141;
115
116__END__
117
118=head1 NAME
119
6936e902 120DBIx::Class::Storage::TxnScopeGuard - Scope-based transaction handling
1bc193ac 121
122=head1 SYNOPSIS
123
124 sub foo {
125 my ($self, $schema) = @_;
126
127 my $guard = $schema->txn_scope_guard;
128
129 # Multiple database operations here
130
131 $guard->commit;
132 }
133
134=head1 DESCRIPTION
135
136An object that behaves much like L<Scope::Guard>, but hardcoded to do the
fd323bf1 137right thing with transactions in DBIx::Class.
1bc193ac 138
139=head1 METHODS
140
141=head2 new
142
6936e902 143Creating an instance of this class will start a new transaction (by
144implicitly calling L<DBIx::Class::Storage/txn_begin>. Expects a
1bc193ac 145L<DBIx::Class::Storage> object as its only argument.
146
147=head2 commit
148
149Commit the transaction, and stop guarding the scope. If this method is not
48580715 150called and this object goes out of scope (e.g. an exception is thrown) then
6936e902 151the transaction is rolled back, via L<DBIx::Class::Storage/txn_rollback>
1bc193ac 152
153=cut
154
155=head1 SEE ALSO
156
157L<DBIx::Class::Schema/txn_scope_guard>.
158
a2bd3796 159L<Scope::Guard> by chocolateboy (inspiration for this module)
1bc193ac 160
a2bd3796 161=head1 FURTHER QUESTIONS?
1bc193ac 162
a2bd3796 163Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
1bc193ac 164
a2bd3796 165=head1 COPYRIGHT AND LICENSE
1bc193ac 166
a2bd3796 167This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
168by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
169redistribute it and/or modify it under the same terms as the
170L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.