List:Commits« Previous MessageNext Message »
From:Vladislav Vaintroub Date:August 12 2009 5:28pm
Subject:bzr commit into connector-net-6.0 branch (vvaintroub:743) Bug#35330
View as plain text  
#At file:///H:/connector_net/6.0/ based on revid:vvaintroub@strippeda9ykn

  743 Vladislav Vaintroub	2009-08-12
      Bug #35330 InvalidOperationException during Transaction Rollback
      
      The problem with the bug is missing synchronization between the
      thread doing transaction rollback when TransactionScope has expired 
      and the main thread. In worst case, even if transaction scope is expired
      and transaction should be aborted, table would still be modified.
      
      The solution introduces driver locking that is used in ExecuteReader
      (central operation for MySqlCommand.ExecuteXXX methods) , 
      in Driver.Configure() and in MySqlPromotableTransaction.Rollback().
      
      Except for MySqlPromotableTransaction.Rollback, the lock uses 
      non-blocking logic  -  if the same driver is used by another thread, 
      the operation throws an exception, rather than waits.
      PromotableTransaction.Rollback() however would wait until current 
      command in the main thread completes.

    modified:
      CHANGES
      MySql.Data/Provider/Properties/Resources.Designer.cs
      MySql.Data/Provider/Properties/Resources.resx
      MySql.Data/Provider/Source/Driver.cs
      MySql.Data/Provider/Source/MySqlPromotableTransaction.cs
      MySql.Data/Provider/Source/command.cs
      MySql.Data/Tests/Source/SimpleTransactions.cs
=== modified file 'CHANGES'
--- a/CHANGES	2009-08-11 01:17:28 +0000
+++ b/CHANGES	2009-08-12 17:28:36 +0000
@@ -1,4 +1,7 @@
 Version 6.0.5
+- fixed race condition between mysql command executing in main thread  and 
+  MySqlPromotableTransaction.Rollback() from another thread, when TransactionScope 
+  is expired.(bug#35330)
 - fixed memory leak in server when calling stored procedure with output parameter many times 
   in the same session (bug#36027)
 - workaround for .NET compact framework problems when writing behind the end of MemoryStream 

=== modified file 'MySql.Data/Provider/Properties/Resources.Designer.cs'
--- a/MySql.Data/Provider/Properties/Resources.Designer.cs	2009-07-28 13:10:23 +0000
+++ b/MySql.Data/Provider/Properties/Resources.Designer.cs	2009-08-12 17:28:36 +0000
@@ -1,7 +1,7 @@
 //------------------------------------------------------------------------------
 // <auto-generated>
 //     This code was generated by a tool.
-//     Runtime Version:2.0.50727.3053
+//     Runtime Version:2.0.50727.4918
 //
 //     Changes to this file may cause incorrect behavior and will be lost if
 //     the code is regenerated.
@@ -21,9 +21,7 @@ namespace MySql.Data.MySqlClient.Propert
     // with the /str option, or rebuild your VS project.
     [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
-#if !CF
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
-#endif
     public class Resources {
         
         private static global::System.Resources.ResourceManager resourceMan;
@@ -261,6 +259,15 @@ namespace MySql.Data.MySqlClient.Propert
         }
         
         /// <summary>
+        ///   Looks up a localized string similar to Driver is in use by another thread.
+        /// </summary>
+        public static string DriverInUse {
+            get {
+                return ResourceManager.GetString("DriverInUse", resourceCulture);
+            }
+        }
+        
+        /// <summary>
         ///   Looks up a localized string similar to Error creating socket connection.
         /// </summary>
         public static string ErrorCreatingSocket {
@@ -612,6 +619,15 @@ namespace MySql.Data.MySqlClient.Propert
         }
         
         /// <summary>
+        ///   Looks up a localized string similar to Promotable transaction is being rolled back. A possible reason for it is  that TransactionScope is timed out..
+        /// </summary>
+        public static string PromotableTransactionRollingBack {
+            get {
+                return ResourceManager.GetString("PromotableTransactionRollingBack", resourceCulture);
+            }
+        }
+        
+        /// <summary>
         ///   Looks up a localized string similar to Packets larger than max_allowed_packet are not allowed..
         /// </summary>
         public static string QueryTooLarge {

=== modified file 'MySql.Data/Provider/Properties/Resources.resx'
--- a/MySql.Data/Provider/Properties/Resources.resx	2009-07-13 02:34:29 +0000
+++ b/MySql.Data/Provider/Properties/Resources.resx	2009-08-12 17:28:36 +0000
@@ -355,6 +355,12 @@
   <data name="DataNotInSupportedFormat" xml:space="preserve">    
     <value>The given value was not in a supported format.</value>  
   </data>
+  <data name="DriverInUse" xml:space="preserve">
+    <value>Driver is in use by another thread</value>
+  </data>
+  <data name="PromotableTransactionRollingBack" xml:space="preserve">
+    <value>Promotable transaction is being rolled back. A possible reason for it is  that TransactionScope is timed out.</value>
+  </data>
   <resheader name="resmimetype">
     <value>text/microsoft-resx</value>
   </resheader>

=== modified file 'MySql.Data/Provider/Source/Driver.cs'
--- a/MySql.Data/Provider/Source/Driver.cs	2009-07-12 16:21:45 +0000
+++ b/MySql.Data/Provider/Source/Driver.cs	2009-08-12 17:28:36 +0000
@@ -25,6 +25,7 @@ using System.Text;
 using MySql.Data.Common;
 using MySql.Data.Types;
 using MySql.Data.MySqlClient.Properties;
+using System.Threading;
 
 namespace MySql.Data.MySqlClient
 {
@@ -188,9 +189,15 @@ namespace MySql.Data.MySqlClient
             Dispose(true);
             GC.SuppressFinalize(this);
 		}
-
         public virtual void Configure(MySqlConnection connection)
         {
+            using( new DriverLock(this, false))
+            {
+                ConfigureInternal(connection);
+            }
+        }
+        private void ConfigureInternal(MySqlConnection connection)
+        {
             this.connection = connection;
 
             bool firstConfigure = false;
@@ -372,4 +379,74 @@ namespace MySql.Data.MySqlClient
 
         #endregion
     }
+
+    /// <summary>
+    /// A class for locking the driver to disable
+    /// operations that are done in parallel by another thread,
+    /// since driver is not thread-safe.
+    /// 
+    /// Intended usage:
+    /// using (new DriverLock(driver, false))
+    /// {
+    ///   // Do some work
+    /// }
+    /// It is similar to "lock" keyword. However, if second parameter
+    /// to the constructor is false (non-blocking), and the driver is
+    /// being used by another thread, constructor throws an exception
+    /// instead of waiting.
+    /// </summary>
+    class DriverLock : IDisposable
+    {
+        bool blocking;
+        Driver driver;
+        /// <summary>
+        /// Constructor for driver lock.
+        /// Acquires a lock on the driver.
+        /// </summary>
+        /// <param name="driver">The driver </param>
+        /// <param name="block">
+        /// if false, and the driver is being locked by another thread
+        /// the constructor will fail with invalid operation exception.
+        /// </param>
+        internal DriverLock(Driver driver, bool blocking)
+        {
+            this.blocking = blocking;
+            this.driver = driver;
+            if (blocking)
+                Monitor.Enter(driver);
+            else
+            {
+                if (!Monitor.TryEnter(driver))
+                {
+                    MySqlPromotableTransaction trans =  driver.CurrentTransaction;
+                    // give a special exception if promotable exception does rollback in
+                    // parallel (e.g when timeout of TransactionScope is expired)
+                    if (trans != null && trans.InRollback)
+                    {
+                      throw new MySqlException(Resources.PromotableTransactionRollingBack);
+                    }
+                    throw new InvalidOperationException(Resources.DriverInUse);
+                }
+            }
+        }
+
+        ~DriverLock()
+        {
+            Dispose(false);
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                Monitor.Exit(driver);
+            }
+        }
+    }
 }

=== 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-08-12 17:28:36 +0000
@@ -30,11 +30,13 @@ namespace MySql.Data.MySqlClient
         private MySqlConnection connection;
         private Transaction baseTransaction;
         private MySqlTransaction simpleTransaction;
+        bool inRollback;
 
         public MySqlPromotableTransaction(MySqlConnection connection, Transaction baseTransaction)
         {
             this.connection = connection;
             this.baseTransaction = baseTransaction;
+            inRollback = false;
         }
 
         public Transaction BaseTransaction
@@ -42,6 +44,11 @@ namespace MySql.Data.MySqlClient
             get { return baseTransaction; }
         }
 
+        public bool InRollback
+        {
+            get { return inRollback; }
+        }
+
         void IPromotableSinglePhaseNotification.Initialize()
         {
             string valueName = Enum.GetName(
@@ -54,14 +61,26 @@ namespace MySql.Data.MySqlClient
 
         void IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
         {
-            simpleTransaction.Rollback();
-            singlePhaseEnlistment.Aborted();
-            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
-
-            connection.driver.CurrentTransaction = null;
-
-            if (connection.State == ConnectionState.Closed)
-                connection.CloseFully();
+            // The lock prevents executing commands on this driver in paralell.
+            using (new DriverLock(connection.driver, true))
+            {
+                try
+                {
+                    inRollback = true;
+                    simpleTransaction.Rollback();
+                    singlePhaseEnlistment.Aborted();
+                    DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
+
+                    connection.driver.CurrentTransaction = null;
+
+                    if (connection.State == ConnectionState.Closed)
+                        connection.CloseFully();
+                }
+                finally
+                {
+                    inRollback = false;
+                }
+            }
         }
 
         void IPromotableSinglePhaseNotification.SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment)

=== modified file 'MySql.Data/Provider/Source/command.cs'
--- a/MySql.Data/Provider/Source/command.cs	2009-08-11 01:17:28 +0000
+++ b/MySql.Data/Provider/Source/command.cs	2009-08-12 17:28:36 +0000
@@ -366,8 +366,16 @@ namespace MySql.Data.MySqlClient
             }
 		}
 
-		/// <include file='docs/mysqlcommand.xml' path='docs/ExecuteReader1/*'/>
-		public new MySqlDataReader ExecuteReader(CommandBehavior behavior)
+        /// <include file='docs/mysqlcommand.xml' path='docs/ExecuteReader1/*'/>
+        public new MySqlDataReader ExecuteReader(CommandBehavior behavior)
+        {
+            using(new DriverLock(connection.driver, false))
+            {
+                return ExecuteReaderInternal(behavior);
+            }
+        }
+
+        private MySqlDataReader ExecuteReaderInternal(CommandBehavior behavior)
 		{
 			lastInsertedId = -1;
 			CheckState();

=== modified file 'MySql.Data/Tests/Source/SimpleTransactions.cs'
--- a/MySql.Data/Tests/Source/SimpleTransactions.cs	2009-07-12 16:21:45 +0000
+++ b/MySql.Data/Tests/Source/SimpleTransactions.cs	2009-08-12 17:28:36 +0000
@@ -24,6 +24,7 @@ using System.IO;
 using NUnit.Framework;
 using System.Data.Common;
 using System.Reflection;
+using System.Transactions;
 
 namespace MySql.Data.MySqlClient.Tests
 {
@@ -158,5 +159,33 @@ namespace MySql.Data.MySqlClient.Tests
             bool isOpen = (bool)fi.GetValue(txn);
             Assert.IsFalse(isOpen);
         }
+
+        /// <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 TransactionScopeExpired()
+        {
+            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 (MySqlException e)
+                {
+                    Assert.IsTrue(e.Message.Contains("timed out"));
+                }
+            }
+            long count = (long)MySqlHelper.ExecuteScalar(connStr,"select count(*) from Test");
+            Assert.AreEqual(count, 0);
+        }
     }
 }

Attachment: [text/bzr-bundle] bzr/vvaintroub@mysql.com-20090812172836-5e5ux1mtemf5i2s6.bundle
Thread
bzr commit into connector-net-6.0 branch (vvaintroub:743) Bug#35330Vladislav Vaintroub12 Aug