#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#45098 | Vladislav Vaintroub | 18 Nov |