Another overhaul (hopefully one of the last ones) of the rollback handling
Peter Rabbitson [Wed, 27 Jan 2016 11:27:25 +0000 (12:27 +0100)]
This subsystem has seen several passes (most notably 90d7422fc), all of them
utterly misguided and of rather terrible quality :( Lay groundwork for a saner
system by consolidating the "rollback after exception, with potential rewrite"
mechanism. There should be no notably changes in behavior (aside from several
transaction texts), for that see next commit.

Contains a rather hideous piece of code propagated over several years. Not
tackling that yet, as too many things are in motion.

Read under -w

lib/DBIx/Class/Carp.pm
lib/DBIx/Class/Storage.pm
lib/DBIx/Class/Storage/BlockRunner.pm
lib/DBIx/Class/Storage/DBI.pm
lib/DBIx/Class/Storage/DBI/Replicated.pm
lib/DBIx/Class/Storage/TxnScopeGuard.pm
t/storage/txn.t

index 2456d02..49c75fb 100644 (file)
@@ -35,7 +35,7 @@ sub __find_caller {
 # Need a way to parameterize this for Carp::Skip
       $1 !~ /^(?: DBIx::Class::Storage::BlockRunner | Context::Preserve | Try::Tiny | Class::Accessor::Grouped | Class::C3::Componentised | Module::Runtime | Sub::Uplevel )$/x
         and
-      $2 !~ /^(?: throw_exception | carp | carp_unique | carp_once | dbh_do | txn_do | with_deferred_fk_checks)$/x
+      $2 !~ /^(?: throw_exception | carp | carp_unique | carp_once | dbh_do | txn_do | with_deferred_fk_checks | __delicate_rollback )$/x
 #############################
     ) ? $f[3] : undef;
 
index 132daef..9b7dbd9 100644 (file)
@@ -272,6 +272,95 @@ sub txn_rollback {
   }
 }
 
+# to be called by several internal stacked transaction handler codepaths
+# not for external consumption
+# *DOES NOT* throw exceptions, instead:
+#  - returns false on success
+#  - returns the exception on failed rollback
+sub __delicate_rollback {
+  my $self = shift;
+
+  if (
+    $self->transaction_depth > 1
+      and
+    # FIXME - the autosvp check here shouldn't be happening, it should be a role-ish thing
+    # The entire concept needs to be rethought with the storage layer... or something
+    ! $self->auto_savepoint
+      and
+    # the handle seems healthy, and there is nothing for us to do with it
+    # just go ahead and bow out, without triggering the txn_rollback() "nested exception"
+    # the unwind will eventually fail somewhere higher up if at all
+    # FIXME: a ::Storage::DBI-specific method, not a generic ::Storage one
+    $self->_seems_connected
+  ) {
+    # all above checks out - there is nothing to do on the $dbh itself
+    # just a plain soft-decrease of depth
+    $self->{transaction_depth}--;
+    return;
+  }
+
+  my $rbe;
+
+  local $@; # taking no chances
+  unless( eval { $self->txn_rollback; 1 } ) {
+
+    $rbe = $@;
+
+    # we were passed an existing exception to augment (think DESTROY stacks etc)
+    if (@_) {
+      my $exception = shift;
+
+      # append our text - THIS IS A TEMPORARY FIXUP!
+      #
+      # If the passed in exception is a reference, or an object we don't know
+      # how to augment - flattening it is just damn rude
+      if (
+        # FIXME - a better way, not liable to destroy an existing exception needs
+        # to be created. For the time being perpetuating the sin below in order
+        # to break the deadlock of which yak is being shaved first
+        0
+          and
+        length ref $$exception
+          and
+        (
+          ! defined blessed $$exception
+            or
+          ! $$exception->isa( 'DBIx::Class::Exception' )
+        )
+      ) {
+
+        ##################
+        ### FIXME - TODO
+        ##################
+
+      }
+      else {
+
+        # SUCH HIDEOUS, MUCH AUGH! (and double WOW on the s/// at the end below)
+        $rbe =~ s/ at .+? line \d+$//;
+
+        (
+          (
+            defined blessed $$exception
+              and
+            $$exception->isa( 'DBIx::Class::Exception' )
+          )
+            ? (
+              $$exception->{msg} =
+                "Transaction aborted: $$exception->{msg}. Rollback failed: $rbe"
+            )
+            : (
+              $$exception =
+                "Transaction aborted: $$exception. Rollback failed: $rbe"
+            )
+        ) =~ s/Transaction aborted: (?=Transaction aborted:)//;
+      }
+    }
+  }
+
+  return $rbe;
+}
+
 =head2 svp_begin
 
 Arguments: $savepoint_name?
index be29701..ad9cbf7 100644 (file)
@@ -158,22 +158,10 @@ sub _run {
     # something above threw an error (could be the begin, the code or the commit)
     if ( is_exception $run_err ) {
 
-      # attempt a rollback if we did begin in the first place
-      if ($txn_begin_ok) {
-        # some DBDs go crazy if there is nothing to roll back on, perform a soft-check
-        my $rollback_exception = $storage->_seems_connected
-          ? (! eval { $storage->txn_rollback; 1 }) ? $@ : ''
-          : 'lost connection to storage'
-        ;
-
-        if ( $rollback_exception and (
-          ! defined blessed $rollback_exception
-            or
-          ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
-        ) ) {
-          $run_err = "Transaction aborted: $run_err. Rollback failed: $rollback_exception";
-        }
-      }
+      # Attempt a rollback if we did begin in the first place
+      # Will append rollback error if possible
+      $storage->__delicate_rollback( \$run_err )
+        if $txn_begin_ok;
 
       push @{ $self->exception_stack }, $run_err;
 
index 0163dc0..0c388ed 100644 (file)
@@ -1636,7 +1636,9 @@ sub _exec_txn_commit {
 sub txn_rollback {
   my $self = shift;
 
-  $self->throw_exception("Unable to txn_rollback() on a disconnected storage")
+  # do a minimal connectivity check due to weird shit like
+  # https://rt.cpan.org/Public/Bug/Display.html?id=62370
+  $self->throw_exception("lost connection to storage")
     unless $self->_seems_connected;
 
   # esoteric case for folks using external $dbh handles
index 92a0e17..8328cf3 100644 (file)
@@ -333,6 +333,7 @@ my $method_dispatch = {
   unimplemented => [qw/
     _arm_global_destructor
     _verify_pid
+    __delicate_rollback
 
     get_use_dbms_capability
     set_use_dbms_capability
index e6885cc..583639f 100644 (file)
@@ -61,10 +61,8 @@ sub DESTROY {
 
   return if $_[0]->{inactivated};
 
-  # if our dbh is not ours anymore, the $dbh weakref will go undef
-  $_[0]->{storage}->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
-  return unless $_[0]->{dbh};
 
+  # grab it before we've done volatile stuff below
   my $current_exception = (
     is_exception $@
       and
@@ -78,47 +76,35 @@ sub DESTROY {
     : undef
   ;
 
-  {
-    local $@;
-
-    carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back.'
-      unless defined $current_exception;
-
-    my $rollback_exception;
-    # do minimal connectivity check due to weird shit like
-    # https://rt.cpan.org/Public/Bug/Display.html?id=62370
-    eval {
-      $_[0]->{storage}->_seems_connected && $_[0]->{storage}->txn_rollback;
-      1;
-    } or $rollback_exception = $@;
-
-    if ( $rollback_exception and (
-      ! defined blessed $rollback_exception
-          or
-      ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
-    ) ) {
-      # append our text - THIS IS A TEMPORARY FIXUP!
-      # a real stackable exception object is in the works
-      if (ref $current_exception eq 'DBIx::Class::Exception') {
-        $current_exception->{msg} = "Transaction aborted: $current_exception->{msg} "
-          ."Rollback failed: ${rollback_exception}";
-      }
-      elsif ($current_exception) {
-        $current_exception = "Transaction aborted: ${current_exception} "
-          ."Rollback failed: ${rollback_exception}";
-      }
-      else {
-        carp (join ' ',
-          "********************* ROLLBACK FAILED!!! ********************",
-          "\nA rollback operation failed after the guard went out of scope.",
-          'This is potentially a disastrous situation, check your data for',
-          "consistency: $rollback_exception"
-        );
-      }
-    }
+
+  # if our dbh is not ours anymore, the $dbh weakref will go undef
+  $_[0]->{storage}->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
+  return unless defined $_[0]->{dbh};
+
+
+  carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back.'
+    unless defined $current_exception;
+
+
+  if (
+    my $rollback_exception = $_[0]->{storage}->__delicate_rollback(
+      defined $current_exception
+        ? \$current_exception
+        : ()
+    )
+      and
+    ! defined $current_exception
+  ) {
+    carp (join ' ',
+      "********************* ROLLBACK FAILED!!! ********************",
+      "\nA rollback operation failed after the guard went out of scope.",
+      'This is potentially a disastrous situation, check your data for',
+      "consistency: $rollback_exception"
+    );
   }
 
-  $@ = $current_exception;
+  $@ = $current_exception
+    if DBIx::Class::_ENV_::UNSTABLE_DOLLARAT;
 }
 
 1;
index ea9845f..f8e1b35 100644 (file)
@@ -450,7 +450,7 @@ warnings_are {
         die $broken_exception
       });
     })
-  } qr/\QTransaction aborted: $broken_exception. Rollback failed: lost connection to storage at @{[__FILE__]} line $ln\E\n/;  # FIXME wtf - ...\E$/m doesn't work here
+  } qr/\QTransaction aborted: $broken_exception. Rollback failed: DBIx::Class::Storage::DBI::txn_rollback(): lost connection to storage at @{[__FILE__]} line $ln\E\n/;  # FIXME wtf - ...\E$/m doesn't work here
 
   is @w, 1, 'One matching warning only';