List:Commits« Previous MessageNext Message »
From:Vladislav Vaintroub Date:November 18 2009 2:22pm
Subject:bzr commit into connector-net-trunk branch (vvaintroub:798) Bug#45098
View as plain text  
#At file:///H:/connector_net/trunk/ based on revid:vvaintroub@stripped

  798 Vladislav Vaintroub	2009-11-18
      Bug #45098 : Implemented nested transaction scopes.
      We maintain a per-thread stack of scopes now, requied to handle nested
      scopes with RequiresNew or Suppress options.

    modified:
      CHANGES
      MySql.Data/Provider/Source/MySqlPromotableTransaction.cs
      MySql.Data/Tests/Source/Transactions.cs
=== modified file 'CHANGES'
--- a/CHANGES	2009-11-11 20:17:35 +0000
+++ b/CHANGES	2009-11-18 14:22:38 +0000
@@ -1,3 +1,5 @@
+Version 6.3.0
+- Implemented nested transaction scopes (bug #45098)
 Version 6.2.1
 - fixed SessionProvider to be compatible with 4.x MySQL, replaced TIMESTAMPDIFF with TIME_TO_SEC
   (bug#47219)

=== 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-11-18 14:22:38 +0000
@@ -21,15 +21,59 @@
 using System;
 using System.Transactions;
 using System.Collections;
+using System.Collections.Generic;
 using System.Data;
 
 namespace MySql.Data.MySqlClient
 {
+    /// <summary>
+    /// Represents a single(not nested) TransactionScope
+    /// </summary>
+    internal class MySqlTransactionScope
+    {
+        public MySqlConnection connection;
+        public Transaction baseTransaction;
+        public MySqlTransaction simpleTransaction;
+        public MySqlTransactionScope(MySqlConnection con, Transaction trans, 
+            MySqlTransaction simpleTransaction)
+        {
+            connection = con;
+            baseTransaction = trans;
+            this.simpleTransaction = simpleTransaction;
+        }
+
+        public void Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
+        {
+            simpleTransaction.Rollback();
+            singlePhaseEnlistment.Aborted();
+            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
+            connection.driver.CurrentTransaction = null;
+            if (connection.State == ConnectionState.Closed)
+                connection.CloseFully();
+        }
+
+        public void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment)
+        {
+            simpleTransaction.Commit();
+            singlePhaseEnlistment.Committed();
+            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
+            connection.driver.CurrentTransaction = null;
+
+            if (connection.State == ConnectionState.Closed)
+                connection.CloseFully();
+        }
+    }
+
     internal sealed class MySqlPromotableTransaction : IPromotableSinglePhaseNotification, ITransactionPromoter
     {
-        private MySqlConnection connection;
-        private Transaction baseTransaction;
-        private MySqlTransaction simpleTransaction;
+        // Per-thread stack to manage nested transaction scopes
+        [ThreadStatic]
+        static Stack<MySqlTransactionScope> globalScopeStack = new Stack<MySqlTransactionScope>();
+
+        MySqlConnection connection;
+        Transaction baseTransaction;
+        Stack<MySqlTransactionScope> scopeStack;
+
 
         public MySqlPromotableTransaction(MySqlConnection connection, Transaction baseTransaction)
         {
@@ -39,41 +83,39 @@ namespace MySql.Data.MySqlClient
 
         public Transaction BaseTransaction
         {
-            get { return baseTransaction; }
+            get 
+            {
+                if (scopeStack.Count > 0)
+                    return scopeStack.Peek().baseTransaction;
+                else
+                    return null;
+            }
         }
 
         void IPromotableSinglePhaseNotification.Initialize()
         {
-            string valueName = Enum.GetName(
-                typeof(System.Transactions.IsolationLevel), baseTransaction.IsolationLevel);
-            System.Data.IsolationLevel dataLevel = (System.Data.IsolationLevel)Enum.Parse(
+           string valueName = Enum.GetName(
+           typeof(System.Transactions.IsolationLevel), baseTransaction.IsolationLevel);
+           System.Data.IsolationLevel dataLevel = (System.Data.IsolationLevel)Enum.Parse(
                 typeof(System.Data.IsolationLevel), valueName);
+           MySqlTransaction simpleTransaction = connection.BeginTransaction(dataLevel);
 
-            simpleTransaction = connection.BeginTransaction(dataLevel);
+           // We need to save the per-thread scope stack locally.
+           // We cannot always use thread static variable in rollback: when scope
+           // times out, rollback is issued by another thread.
+           scopeStack = globalScopeStack;
+           scopeStack.Push(new MySqlTransactionScope(connection, baseTransaction, 
+              simpleTransaction));
         }
 
         void IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment)
         {
-            simpleTransaction.Rollback();
-            singlePhaseEnlistment.Aborted();
-            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
-
-            connection.driver.CurrentTransaction = null;
-
-            if (connection.State == ConnectionState.Closed)
-                connection.CloseFully();
+            scopeStack.Pop().Rollback(singlePhaseEnlistment);
         }
 
         void IPromotableSinglePhaseNotification.SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment)
         {
-            simpleTransaction.Commit();
-            singlePhaseEnlistment.Committed();
-            DriverTransactionManager.RemoveDriverInTransaction(baseTransaction);
-
-            connection.driver.CurrentTransaction = null;
-
-            if (connection.State == ConnectionState.Closed)
-                connection.CloseFully();
+            scopeStack.Pop().SinglePhaseCommit(singlePhaseEnlistment);;
         }
 
         byte[] ITransactionPromoter.Promote()

=== 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-11-18 14:22:38 +0000
@@ -249,20 +249,20 @@ namespace MySql.Data.MySqlClient.Tests
         public void ManualEnlistment()
         {
             createTable("CREATE TABLE Test (key2 VARCHAR(1), name VARCHAR(100), name2 VARCHAR(100))", "INNODB");
-            string connStr = GetConnectionString(true) + ";auto enlist=false";
 
             using (TransactionScope ts = new TransactionScope())
             {
-                MySqlConnection c = new MySqlConnection(connStr);
-                c.Open();
-
-                MySqlCommand cmd = new MySqlCommand("INSERT INTO Test VALUES ('a', 'name', 'name2')", c);
-                cmd.ExecuteNonQuery();
+                string connStr = GetConnectionString(true) + ";auto enlist=false";
+                using (MySqlConnection c = new MySqlConnection(connStr))
+                {
+                    c.Open();
+                    MySqlCommand cmd = new MySqlCommand("INSERT INTO Test VALUES ('a', 'name', 'name2')", c);
+                    cmd.ExecuteNonQuery();
+                }
             }
             MySqlCommand cmd2 = new MySqlCommand("SELECT COUNT(*) FROM Test", conn);
             Assert.AreEqual(1, cmd2.ExecuteScalar());
 
-            KillPooledConnection(connStr);
         }
 
         private void ManuallyEnlistingInitialConnection(bool complete)
@@ -358,6 +358,141 @@ namespace MySql.Data.MySqlClient.Tests
             }
         }
 
+
+        private void NestedScopeInternalTest(
+            TransactionScopeOption nestedOption,
+            bool innerComplete,
+            bool outerComplete,
+            bool expectInnerChangesVisible,
+            bool expectOuterChangesVisible)
+        {
+            createTable("CREATE TABLE T(str varchar(10))", "INNODB");
+            try
+            {
+                using (TransactionScope outer = new TransactionScope())
+                {
+                    string connStr = GetConnectionString(true);
+                    using (MySqlConnection c1 = new MySqlConnection(connStr))
+                    {
+                        c1.Open();
+                        MySqlCommand cmd1 = new MySqlCommand("INSERT INTO T VALUES ('outer')", c1);
+                        cmd1.ExecuteNonQuery();
+                        using (TransactionScope inner = new TransactionScope(nestedOption))
+                        {
+                          
+                            MySqlConnection c2;
+                            if (nestedOption == TransactionScopeOption.Required)
+                            {
+                                // inner scope joins already running ambient
+                                // transaction, we cannot use new connection here
+                                c2 = c1;
+                            }
+                            else
+                            {
+                                // when TransactionScopeOption.RequiresNew or 
+                                // new TransactionScopeOption.Suppress is used,
+                                // we have to use a new transaction. We create a
+                                // new connection for it.
+                                c2 = new MySqlConnection(connStr);
+                                c2.Open();
+                            }
+
+                            MySqlCommand cmd2 =
+                                    new MySqlCommand("INSERT INTO T VALUES ('inner')", c2);
+                            cmd2.ExecuteNonQuery();
+
+                            if (innerComplete)
+                                inner.Complete();
+
+                            // Dispose connection if it was created.
+                            if (c2 != c1)
+                                c2.Dispose();
+                        }
+                    }
+                    if (outerComplete)
+                        outer.Complete();
+
+                }
+                bool innerChangesVisible =
+                   ((long)MySqlHelper.ExecuteScalar(conn, "select count(*) from T where str = 'inner'") == 1);
+                bool outerChangesVisible =
+                    ((long)MySqlHelper.ExecuteScalar(conn, "select count(*) from T where str = 'outer'") == 1);
+                Assert.AreEqual(innerChangesVisible, expectInnerChangesVisible);
+                Assert.AreEqual(outerChangesVisible, expectOuterChangesVisible);
+            }
+            finally
+            {
+                MySqlHelper.ExecuteNonQuery(conn, "DROP TABLE T");
+            }
+        }
+
+        /// <summary>
+        /// Test inner/outer scope behavior, with different scope options, 
+        /// completing either inner or outer scope, or both.
+        /// </summary>
+        [Test]
+        public void NestedScope()
+        {
+
+            // inner scope joins the ambient scope, neither inner not outer  scope completes
+            // Expect empty table.
+            NestedScopeInternalTest(TransactionScopeOption.Required, false, false, false, false);
+
+            // inner scope joins the ambient scope, inner does not complete, outer completes
+            // Expect exception while disposing outer transaction
+            try
+            {
+                NestedScopeInternalTest(TransactionScopeOption.Required, false, true, false, false);
+                Assert.Fail("expected TransactionAborted exception");
+            }
+            catch (TransactionAbortedException)
+            {
+            }
+
+            // inner scope joins the ambient scope, inner completes, outer does not
+            // Expect empty table.
+            NestedScopeInternalTest(TransactionScopeOption.Required, true, false, false, false);
+
+            // inner scope joins the ambient scope, both complete.
+            // Expect table with entries for inner and outer scope
+            NestedScopeInternalTest(TransactionScopeOption.Required, true, true, true, true);
+
+
+
+            // inner scope creates new transaction, neither inner not outer  scope completes
+            // Expect empty table.
+            NestedScopeInternalTest(TransactionScopeOption.RequiresNew, false, false, false, false);
+
+            // inner scope creates new transaction, inner does not complete, outer completes
+            // Expect changes by outer transaction visible ??
+            NestedScopeInternalTest(TransactionScopeOption.RequiresNew, false, true, false, true);
+
+            // inner scope creates new transactiion, inner completes, outer does not
+            // Expect changes by inner transaction visible
+            NestedScopeInternalTest(TransactionScopeOption.RequiresNew, true, false, true, false);
+
+            // inner scope creates new transaction, both complete
+            NestedScopeInternalTest(TransactionScopeOption.RequiresNew, true, true, true, true);
+
+
+            // inner scope suppresses transaction, neither inner not outer  scope completes
+            // Expect changes made by inner scope to be visible
+            NestedScopeInternalTest(TransactionScopeOption.Suppress, false, false, true, false);
+
+            // inner scope supresses transaction, inner does not complete, outer completes
+            // Expect changes by inner scope to be visible ??
+            NestedScopeInternalTest(TransactionScopeOption.Suppress,  true, false, true, false);
+
+            // inner scope supresses transaction, inner completes, outer does not
+            // Expect changes by inner transaction visible
+            NestedScopeInternalTest(TransactionScopeOption.Suppress, true, false, true, false);
+
+            // inner scope supresses transaction, both complete
+            NestedScopeInternalTest(TransactionScopeOption.Suppress, true, true, true, true);
+        }
+
+
+
         private void ReusingSameConnection(bool pooling, bool complete)
         {
             int c1Thread;


Attachment: [text/bzr-bundle] bzr/vvaintroub@mysql.com-20091118142238-n2c8e4ne5r27oduy.bundle
Thread
bzr commit into connector-net-trunk branch (vvaintroub:798) Bug#45098Vladislav Vaintroub18 Nov