Fix annoying warnings on innocent looking MSSQL code
Peter Rabbitson [Fri, 1 Apr 2016 10:19:51 +0000 (12:19 +0200)]
( cherry-pick of d3a2e4249 )

After several rounds of improvements in the retry logic (84efb6d7, 729656c5)
MSSQL code non-fatally aborting due to clashing multiple active resultsets
would emit an annoying warning. Such warnings are especially baffling when
encountered in innocent-looking code like:

   my $first_bar_of_first_foo = $schema->resultset('Foo')
                                        ->search({ foo => 'fa' })
                                         ->next
                                          ->related_resultset("bar")
                                           ->next;

Since no object destruction takes place until the = operator is executed, the
cursor returning "first foo matching fa" is still active when we run a second
search for the "bars". With default MSSQL settings (i.e. without an enabled
MARS[1] implementation) this leads to an exception on the second ->next().
The failed next() is properly retried, since we are not in transaction or some
similar complicating factor, and the entire thing executes correctly, except
the force-disconnect-before-reconnect-after-failed-ping warns about the first
cursor being still alive.

Add extra stack marker for this particular case, and teach the MSSQL driver to
hide the (at this stage spurious) warning

[1] http://p3rl.org/DBIx::Class::Storage::DBI::ODBC::Microsoft_SQL_Server#MULTIPLE-ACTIVE-STATEMENTS

Changes
lib/DBIx/Class/Storage/BlockRunner.pm
lib/DBIx/Class/Storage/DBI/MSSQL.pm
t/746mssql.t

diff --git a/Changes b/Changes
index b6b1a1b..60ae978 100644 (file)
--- a/Changes
+++ b/Changes
@@ -6,6 +6,7 @@ Revision history for DBIx::Class
         - Fix use of ::Schema::Versioned combined with a user-supplied
           $dbh->{HandleError} (GH#101)
         - Fix parsing of DSNs containing driver arguments (GH#99)
+        - Fix spurious warning on MSSQL cursor invalidation retries (RT#102166)
         - Work around unreliable $sth->finish() on INSERT ... RETURNING within
           DBD::Firebird on some compiler/driver combinations (RT#110979)
         - Fix leaktest failures with upcoming version of Sub::Quote
index be29701..2862669 100644 (file)
@@ -188,7 +188,12 @@ sub _run {
           # FIXME - we assume that $storage->{_dbh_autocommit} is there if
           # txn_init_depth is there, but this is a DBI-ism
           $txn_init_depth > ( $storage->{_dbh_autocommit} ? 0 : 1 )
-        ) or ! $self->retry_handler->($self)
+        )
+          or
+        ! do {
+          local $self->storage->{_in_do_block_retry_handler} = 1;
+          $self->retry_handler->($self)
+        }
       );
 
       # we got that far - let's retry
index 5b4c422..90c05f8 100644 (file)
@@ -10,6 +10,7 @@ use base qw/
 use mro 'c3';
 
 use Try::Tiny;
+use DBIx::Class::_Util qw( sigwarn_silencer );
 use List::Util 'first';
 use namespace::clean;
 
@@ -175,13 +176,31 @@ sub _ping {
 
   my $dbh = $self->_dbh or return 0;
 
-  local $dbh->{RaiseError} = 1;
-  local $dbh->{PrintError} = 0;
-
-  return try {
+  try {
+    local $dbh->{RaiseError} = 1;
+    local $dbh->{PrintError} = 0;
     $dbh->do('select 1');
     1;
-  } catch {
+  }
+  catch {
+    # MSSQL is *really* annoying wrt multiple active resultsets,
+    # and this may very well be the reason why the _ping failed
+    #
+    # Proactively disconnect, while hiding annoying warnings if the case
+    #
+    # The callchain is:
+    #   < check basic retryability prerequisites (e.g. no txn) >
+    #    ->retry_handler
+    #     ->storage->connected()
+    #      ->ping
+    # So if we got here with the in_handler bit set - we won't  break
+    # anything by a disconnect
+    if( $self->{_in_do_block_retry_handler} ) {
+      local $SIG{__WARN__} = sigwarn_silencer qr/disconnect invalidates .+? active statement/;
+      $self->disconnect;
+    }
+
+    # RV of _ping itself
     0;
   };
 }
index e4a9de0..e2c3abd 100644 (file)
@@ -3,6 +3,7 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 use Try::Tiny;
 
 use DBIx::Class::Optional::Dependencies ();
@@ -102,11 +103,35 @@ SQL
 
     ok(($new->artistid||0) > 0, "Auto-PK worked for $opts_name");
 
-# Test multiple active statements
-    SKIP: {
-      skip 'not a multiple active statements configuration', 1
-        if $opts_name eq 'plain';
+# Test graceful error handling if not supporting multiple active statements
+    if( $opts_name eq 'plain' ) {
+
+      # keep the first cursor alive (as long as $rs is alive)
+      my $rs = $schema->resultset("Artist");
+
+      my $a1 = $rs->next;
+
+      my $a2;
+
+      warnings_are {
+        # second cursor, invalidates $rs, but it doesn't
+        # matter as long as we do not try to use it
+        $a2 = $schema->resultset("Artist")->next;
+      } [], 'No warning on retry due to previous cursor invalidation';
 
+      is_deeply(
+        { $a1->get_columns },
+        { $a2->get_columns },
+        'Same data',
+      );
+
+      dies_ok {
+        $rs->next;
+      } 'Invalid cursor did not silently return garbage';
+    }
+
+# Test multiple active statements
+    else {
       $schema->storage->ensure_connected;
 
       lives_ok {