diff --git a/lib/DBIx/Class/Storage/TxnScopeGuard.pm b/lib/DBIx/Class/Storage/TxnScopeGuard.pm index edf72057f..1575a8c4d 100644 --- a/lib/DBIx/Class/Storage/TxnScopeGuard.pm +++ b/lib/DBIx/Class/Storage/TxnScopeGuard.pm @@ -49,6 +49,11 @@ sub commit { $self->{inactivated} = 1; } +sub force_inactivate { + my $self = shift; + $self->{inactivated} = 1; +} + sub DESTROY { return if &detected_reinvoked_destructor; @@ -136,6 +141,15 @@ DBIx::Class::Storage::TxnScopeGuard - Scope-based transaction handling An object that behaves much like L, but hardcoded to do the right thing with transactions in DBIx::Class. +If you get the urge to call a C method on the guard object, you're +advised to instead wrap your scoped transaction using C<< L >> +or L and throw an exception with C<< L >>. +Explicit rollbacks don't compose (or nest) nicely without unwinding the scope +via an exception. + +A warning is emitted if the guard goes out of scope without being first +inactivated by L or an exception. + =head1 METHODS =head2 new @@ -150,6 +164,12 @@ Commit the transaction, and stop guarding the scope. If this method is not called and this object goes out of scope (e.g. an exception is thrown) then the transaction is rolled back, via L +=head2 force_inactivate + +Forcibly inactivate the guard, causing it to stop guarding the scope without +committing. You're advised not to use this and to throw an exception if you +want to abort the transaction. See the L. + =cut =head1 SEE ALSO diff --git a/t/storage/txn_scope_guard.t b/t/storage/txn_scope_guard.t index afe8c8e46..9d449499f 100644 --- a/t/storage/txn_scope_guard.t +++ b/t/storage/txn_scope_guard.t @@ -242,4 +242,19 @@ for my $post_poison (0,1) { ; } +# force_inactivate actually inactivates and doesn't emit a warning +{ + my $s = DBICTest::Schema->connect('dbi:SQLite::memory:'); + + my @warnings; + local $SIG{__WARN__} = sub { + push @warnings, @_; + }; + { + my $g = $s->txn_scope_guard; + $g->force_inactivate; + } + is scalar @warnings, 0, "no warning"; +} + done_testing; diff --git a/t/storage/txn_scope_guard_global_destruction.t b/t/storage/txn_scope_guard_global_destruction.t new file mode 100644 index 000000000..3a539ac8b --- /dev/null +++ b/t/storage/txn_scope_guard_global_destruction.t @@ -0,0 +1,49 @@ +use strict; +use warnings; +use Test::More; + +sub run_snippet { + my $output = `$^X -Mlib=t/lib -MDBICTest -e '$_[0]' 2>&1`; + chomp $output; + return $output; +} + +{ + my $output = run_snippet(' + my $s = DBICTest->init_schema; + eval { + my $g = $s->txn_scope_guard; + die "normal destruction"; + }; + '); + unlike $output, + qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error\. Rolling back\./, + 'Out of scope warning not detected'; + is $output, "", "No other output"; +} + +{ + my $output = run_snippet(' + my $s = DBICTest->init_schema; + my $g = $s->txn_scope_guard; + die "global destruction"; + '); + unlike $output, + qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error\. Rolling back\./, + 'Out of scope warning not detected'; + like $output, + qr/global destruction/, + 'Fatal exception detected'; +} + +{ + my $output = run_snippet(' + my $s = DBICTest->init_schema; + my $g = $s->txn_scope_guard; + '); + like $output, + qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error\. Rolling back\./, + 'Out of scope warning detected'; +} + +done_testing;