List:Commits« Previous MessageNext Message »
From:Vladislav Vaintroub Date:December 15 2009 2:12am
Subject:bzr commit into connector-net-trunk branch (vvaintroub:811) Bug#35330
View as plain text  
#At file:///H:/connector_net/6.2/ based on revid:reggie.burnett@stripped

  811 Vladislav Vaintroub	2009-12-15
      Bug #35330 InvalidOperationException during Transaction Rollback :
      fix race condition between abort thread (the one that issues rollback)
      and the "main" thread (the one that is using TransactionScope).

    modified:
      CHANGES
      MySql.Data/Provider/Source/Connection.cs
      MySql.Data/Provider/Source/Driver.cs
      MySql.Data/Provider/Source/MySqlPromotableTransaction.cs
      MySql.Data/Provider/Source/command.cs
      MySql.Data/Tests/Source/Transactions.cs
=== modified file 'CHANGES'
--- a/CHANGES	2009-11-30 15:33:37 +0000
+++ b/CHANGES	2009-12-15 02:12:15 +0000
@@ -1,3 +1,4 @@
+- Fix race condition during TransactionScope rollback (bug#35330)
 - When sending file to server (LOAD DATA INFILE) open the file for read only, not for read/write
  (bug #48944)
 

=== modified file 'MySql.Data/Provider/Source/Connection.cs'
--- a/MySql.Data/Provider/Source/Connection.cs	2009-11-26 02:37:50 +0000
+++ b/MySql.Data/Provider/Source/Connection.cs	2009-12-15 02:12:15 +0000
@@ -45,7 +45,6 @@ namespace MySql.Data.MySqlClient
     {
         internal ConnectionState connectionState;
         internal Driver driver;
-        private MySqlDataReader dataReader;
         private MySqlConnectionStringBuilder settings;
         private bool hasBeenOpen;
         private SchemaProvider schemaProvider;
@@ -101,8 +100,18 @@ namespace MySql.Data.MySqlClient
 
         internal MySqlDataReader Reader
         {
-            get { return dataReader; }
-            set { dataReader = value; }
+            get 
+            { 
+                if (driver == null)
+                    return null;
+                return driver.reader;
+
+            }
+
+            set 
+            { 
+                driver.reader = value;
+            }
         }
 
         internal void OnInfoMessage(MySqlInfoMessageEventArgs args)
@@ -383,11 +392,22 @@ namespace MySql.Data.MySqlClient
             if (State != ConnectionState.Open)
                 throw new InvalidOperationException(Resources.ConnectionNotOpen);
 
-
-            // We use default command timeout for SetDatabase
-            using (new CommandTimer(this, (int)Settings.DefaultCommandTimeout))
+            // This lock  prevents promotable transaction rollback to run
+            // in parallel
+            lock (driver)
             {
-                driver.SetDatabase(databaseName);
+#if !CF
+                if (Transaction.Current != null &&
+                    Transaction.Current.TransactionInformation.Status == TransactionStatus.Aborted)
+                {
+                    throw new TransactionAbortedException();
+                }
+#endif
+                // We use default command timeout for SetDatabase
+                using (new CommandTimer(this, (int)Settings.DefaultCommandTimeout))
+                {
+                    driver.SetDatabase(databaseName);
+                }
             }
             this.database = databaseName;
         }
@@ -580,8 +600,8 @@ namespace MySql.Data.MySqlClient
         {
             if (State == ConnectionState.Closed) return;
 
-            if (dataReader != null)
-                dataReader.Close();
+            if (Reader != null)
+                Reader.Close();
 
 			// if the reader was opened with CloseConnection then driver
 			// will be null on the second time through

=== modified file 'MySql.Data/Provider/Source/Driver.cs'
--- a/MySql.Data/Provider/Source/Driver.cs	2009-11-26 02:37:50 +0000
+++ b/MySql.Data/Provider/Source/Driver.cs	2009-12-15 02:12:15 +0000
@@ -54,6 +54,7 @@ namespace MySql.Data.MySqlClient
         protected MySqlPool pool;
         private bool firstResult;
         protected IDriver handler;
+        internal MySqlDataReader reader;
 
         /// <summary>
         /// For pooled connections, time when the driver was

=== modified file 'MySql.Data/Provider/Source/MySqlPromotableTransaction.cs'
--- a/MySql.Data/Provider/Source/MySqlPromotableTransaction.cs	2009-04-21 18:02:13 +0000
+++ b/MySql.Data/Provider/Source/MySqlPromotableTransaction.cs	2009-12-15 02:12:15 +0000
@@ -48,20 +48,30 @@ namespace MySql.Data.MySqlClient
                 typeof(System.Transactions.IsolationLevel), baseTransaction.IsolationLevel);
             System.Data.IsolationLevel dataLevel = (System.Data.IsolationLevel)Enum.Parse(
                 typeof(System.Data.IsolationLevel), valueName);
-
             simpleTransaction = connection.BeginTransaction(dataLevel);
         }
 
         void IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
         {
-            simpleTransaction.Rollback();
-            singlePhaseEnlistment.Aborted();
-            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
+            // prevent commands in main thread to run concurrently
+            Driver driver = connection.driver;
+            lock (driver)
+            {
+                while (connection.Reader != null)
+                {
+                    // wait for reader to finish. Maybe we should not wait 
+                    // forever and cancel it after some time?
+                    System.Threading.Thread.Sleep(100);
+                }
+                simpleTransaction.Rollback();
+                singlePhaseEnlistment.Aborted();
+                DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
 
-            connection.driver.CurrentTransaction = null;
+                driver.CurrentTransaction = null;
 
-            if (connection.State == ConnectionState.Closed)
-                connection.CloseFully();
+                if (connection.State == ConnectionState.Closed)
+                    connection.CloseFully();
+            }
         }
 
         void IPromotableSinglePhaseNotification.SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment)

=== modified file 'MySql.Data/Provider/Source/command.cs'
--- a/MySql.Data/Provider/Source/command.cs	2009-11-26 02:37:50 +0000
+++ b/MySql.Data/Provider/Source/command.cs	2009-12-15 02:12:15 +0000
@@ -31,6 +31,9 @@ using System.Diagnostics;
 using System.Globalization;
 using System.Collections.Generic;
 using MySql.Data.MySqlClient.Properties;
+#if !CF
+using System.Transactions;
+#endif
 
 namespace MySql.Data.MySqlClient
 {
@@ -347,7 +350,29 @@ namespace MySql.Data.MySqlClient
         {
 
             CheckState();
+            Driver driver = connection.driver;
+            lock (driver)
+            {
+#if !CF
+            System.Transactions.Transaction curTrans = System.Transactions.Transaction.Current;
 
+            if (curTrans != null)
+            {
+                TransactionStatus status = TransactionStatus.InDoubt;
+                try
+                {
+                    // in some cases (during state transitions) this throws
+                    // an exception. Ignore exceptions, we're only interested 
+                    // whether transaction was aborted or not.
+                    status = curTrans.TransactionInformation.Status;
+                }
+                catch(TransactionException)
+                {
+                }
+                if (status == TransactionStatus.Aborted)
+                    throw new TransactionAbortedException();
+            }
+#endif
             commandTimer = new CommandTimer(connection, CommandTimeout);
 
             lastInsertedId = -1;
@@ -443,6 +468,8 @@ namespace MySql.Data.MySqlClient
                 }
             }
         }
+        }
+
  
  
 

=== modified file 'MySql.Data/Tests/Source/Transactions.cs'
--- a/MySql.Data/Tests/Source/Transactions.cs	2009-09-01 01:02:06 +0000
+++ b/MySql.Data/Tests/Source/Transactions.cs	2009-12-15 02:12:15 +0000
@@ -24,6 +24,7 @@ using System.IO;
 using NUnit.Framework;
 using System.Transactions;
 using System.Data.Common;
+using System.Threading;
 
 namespace MySql.Data.MySqlClient.Tests
 {
@@ -250,10 +251,10 @@ namespace MySql.Data.MySqlClient.Tests
         {
             createTable("CREATE TABLE Test (key2 VARCHAR(1), name VARCHAR(100), name2 VARCHAR(100))", "INNODB");
             string connStr = GetConnectionString(true) + ";auto enlist=false";
-
+            MySqlConnection c = null;
             using (TransactionScope ts = new TransactionScope())
             {
-                MySqlConnection c = new MySqlConnection(connStr);
+                c = new MySqlConnection(connStr);
                 c.Open();
 
                 MySqlCommand cmd = new MySqlCommand("INSERT INTO Test VALUES ('a', 'name', 'name2')", c);
@@ -261,7 +262,7 @@ namespace MySql.Data.MySqlClient.Tests
             }
             MySqlCommand cmd2 = new MySqlCommand("SELECT COUNT(*) FROM Test", conn);
             Assert.AreEqual(1, cmd2.ExecuteScalar());
-
+            c.Dispose();
             KillPooledConnection(connStr);
         }
 
@@ -420,5 +421,86 @@ namespace MySql.Data.MySqlClient.Tests
             ReusingSameConnection(false, false);
       //      Assert.AreEqual(processes + 1, CountProcesses());
         }
+
+        /// <summary>
+        /// bug#35330 - even if transaction scope has expired, rows can be inserted into
+        /// the table, due to race condition with the thread doing rollback
+        /// </summary>
+        [Test]
+        public void ScopeTimeoutWithMySqlHelper()
+        {
+            execSQL("DROP TABLE IF EXISTS Test");
+            createTable("CREATE TABLE Test (id int)", "INNODB");
+            string connStr = GetConnectionString(true);
+            using (new TransactionScope(TransactionScopeOption.RequiresNew,TimeSpan.FromSeconds(1)))
+            {
+                try
+                {
+                    for (int i = 0; ; i++)
+                    {
+                        MySqlHelper.ExecuteNonQuery(connStr, String.Format("INSERT INTO Test VALUES({0})", i));;
+                    }
+                }
+                catch (Exception)
+                {
+                }
+            }
+            long count = (long)MySqlHelper.ExecuteScalar(connStr,"select count(*) from test");
+            Assert.AreEqual(0, count);
+        }
+
+         /// <summary>
+         /// Variation of previous test, with a single connection and maual enlistment.
+         /// Checks that  transaction rollback leaves the connection intact (does not close it) 
+         /// and  checks that no command is possible after scope has expired and 
+         /// rollback by timer thread is finished.
+         /// </summary>
+        [Test]
+        public void AttemptToUseConnectionAfterScopeTimeout()
+        {
+            execSQL("DROP TABLE IF EXISTS Test");
+            createTable("CREATE TABLE Test (id int)", "INNODB");
+            string connStr = GetConnectionString(true);
+            using (MySqlConnection c = new MySqlConnection(connStr))
+            {
+                c.Open();
+                MySqlCommand cmd = new MySqlCommand("select 1", c);
+                using (new TransactionScope(TransactionScopeOption.RequiresNew,
+                    TimeSpan.FromSeconds(1)))
+                {
+                    c.EnlistTransaction(Transaction.Current);
+                    cmd = new MySqlCommand("select 1", c);
+                    try
+                    {
+                        for (int i = 0; ; i++)
+                        {
+                            cmd.CommandText = String.Format("INSERT INTO Test VALUES({0})", i);
+                            cmd.ExecuteNonQuery();
+                        }
+                    }
+                    catch (Exception)
+                    {
+                        // Eat exception
+                    }
+
+                    // Here, scope is timed out and rollback is in progress.
+                    // Wait until timeout thread finishes rollback then try to 
+                    // use an aborted connection.
+                    Thread.Sleep(500);
+                    try
+                    {
+                        cmd.ExecuteNonQuery();
+                        Assert.Fail("Using aborted transaction");
+                    }
+                    catch (TransactionAbortedException)
+                    {
+                    }
+                }
+                Assert.IsTrue(c.State == ConnectionState.Open);
+                cmd.CommandText = "select count(*) from Test";
+                long count = (long)cmd.ExecuteScalar();
+                Assert.AreEqual(0, count);
+            }
+        }
     }
 }


Attachment: [text/bzr-bundle] bzr/vvaintroub@mysql.com-20091215021215-wh6qnhai47hdjvbg.bundle
Thread
bzr commit into connector-net-trunk branch (vvaintroub:811) Bug#35330Vladislav Vaintroub15 Dec