From: Date: August 7 2007 8:29pm Subject: Connector/NET commit: r838 - in branches/5.1: . Driver/Properties Driver/Source TestSuite/Source List-Archive: http://lists.mysql.com/commits/32216 X-Bug: 28709 Message-Id: <200708071829.l77ITLYR007941@bk-internal.mysql.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modified: branches/5.1/CHANGES branches/5.1/Driver/Properties/Resources.Designer.cs branches/5.1/Driver/Properties/Resources.resx branches/5.1/Driver/Source/Connection.cs branches/5.1/Driver/Source/Driver.cs branches/5.1/Driver/Source/MySqlPromotableTransaction.cs branches/5.1/Driver/Source/command.cs branches/5.1/Driver/Source/transaction.cs branches/5.1/TestSuite/Source/BaseTest.cs branches/5.1/TestSuite/Source/Transactions.cs Log: Added code to implement better TransactionScope support. This code is brand new and will be heavily refactored in 5.2. (bug #28709) The approach in this change is to take advantage of the fact that all work under a single txn must happen on the same physical connection so we just keep a hash of active driver connections and reuse them when necessary. Modified: branches/5.1/CHANGES =================================================================== --- branches/5.1/CHANGES 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/CHANGES 2007-08-07 18:29:20 UTC (rev 838) @@ -24,6 +24,8 @@ foreign keys and foreign key columns - Fixed index and foreign key enumerators inside the DDEX provider to work with the new binary respect behavior of 5.1 + - Added code to implement better TransactionScope support. This code is brand new and will be + heavily refactored in 5.2. (bug #28709) Version 5.1.2 - 6/12/2007 - Fixed integration with the Website Administration Tool. Before this fix, the test link Modified: branches/5.1/Driver/Properties/Resources.Designer.cs =================================================================== --- branches/5.1/Driver/Properties/Resources.Designer.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Properties/Resources.Designer.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -343,7 +343,14 @@ return ResourceManager.GetString("MixingUpdatedRowSource", resourceCulture); } } - + + internal static string MultipleConnectionsInTransactionNotSupported + { + get { + return ResourceManager.GetString("MultipleConnectionsInTransactionNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to NamedPipeStream does not support seeking. /// Modified: branches/5.1/Driver/Properties/Resources.resx =================================================================== --- branches/5.1/Driver/Properties/Resources.resx 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Properties/Resources.resx 2007-08-07 18:29:20 UTC (rev 838) @@ -324,4 +324,7 @@ The object is not open or has been disposed. - + + Multiple simultaneous connections or connections with different connection strings inside the same transaction are not currently supported. + + \ No newline at end of file Modified: branches/5.1/Driver/Source/Connection.cs =================================================================== --- branches/5.1/Driver/Source/Connection.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Source/Connection.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -30,6 +30,7 @@ using System.Text; using IsolationLevel=System.Data.IsolationLevel; using MySql.Data.Common; +using System.Diagnostics; namespace MySql.Data.MySqlClient { @@ -51,7 +52,6 @@ private ProcedureCache procedureCache; #if !CF private PerformanceMonitor perfMonitor; - private MySqlPromotableTransaction currentTransaction; #endif private bool isExecutingBuggyQuery; private string database; @@ -84,12 +84,6 @@ #region Interal Methods & Properties #if !CF - internal MySqlPromotableTransaction CurrentTransaction - { - get { return currentTransaction; } - set { currentTransaction = value; } - } - internal PerformanceMonitor PerfMonitor { get { return perfMonitor; } @@ -135,6 +129,17 @@ get { return isExecutingBuggyQuery; } set { isExecutingBuggyQuery = value; } } + internal bool SoftClosed + { + get + { +#if !CF + return (State == ConnectionState.Closed) && driver.CurrentTransaction != null; +#else + return false; +#endif + } + } #endregion @@ -286,20 +291,50 @@ /// public override void EnlistTransaction(Transaction transaction) { + // enlisting in the null transaction is a noop if (transaction == null) return; - if (currentTransaction != null) + // guard against trying to enlist in more than one transaction + if (driver.CurrentTransaction != null) { - if (currentTransaction.BaseTransaction == transaction) + if (driver.CurrentTransaction.BaseTransaction == transaction) return; throw new MySqlException("Already enlisted"); } - MySqlPromotableTransaction t = new MySqlPromotableTransaction(this, transaction); - transaction.EnlistPromotableSinglePhase(t); - currentTransaction = t; + // now see if we need to swap out drivers. We would need to do this since + // we have to make sure all ops for a given transaction are done on the + // same physical connection. + Driver existingDriver = DriverTransactionManager.GetDriverInTransaction(transaction); + if (existingDriver != null) + { + // we can't allow more than one driver to contribute to the same connection + if (existingDriver.IsInActiveUse) + throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); + + // there is an existing driver and it's not being currently used. + // now we need to see if it is using the same connection string + string text1 = existingDriver.Settings.GetConnectionString(true); + string text2 = Settings.GetConnectionString(true); + if (String.Compare(text1, text2, true) != 0) + throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); + + // close existing driver + // set this new driver as our existing driver + CloseDriver(); + driver = existingDriver; + } + + if (driver.CurrentTransaction == null) + { + MySqlPromotableTransaction t = new MySqlPromotableTransaction(this, transaction); + transaction.EnlistPromotableSinglePhase(t); + driver.CurrentTransaction = t; + DriverTransactionManager.SetDriverInTransaction(driver); + driver.IsInActiveUse = true; + } } #endif @@ -395,17 +430,30 @@ SetState(ConnectionState.Connecting); +#if !CF + // if we are auto enlisting in a current transaction, then we will be + // treating the connection as pooled + if (settings.AutoEnlist && Transaction.Current != null) + { + driver = DriverTransactionManager.GetDriverInTransaction(Transaction.Current); + if (driver != null && driver.IsInActiveUse) + throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); + } +#endif + try { if (settings.Pooling) { MySqlPool pool = MySqlPoolManager.GetPool(settings); - driver = pool.GetConnection(); + if (driver == null) + driver = pool.GetConnection(); procedureCache = pool.ProcedureCache; } else { - driver = Driver.Create(settings); + if (driver == null) + driver = Driver.Create(settings); procedureCache = new ProcedureCache((int) settings.ProcedureCacheSize); } } @@ -505,15 +553,12 @@ SetState(ConnectionState.Closed); } - /// - public override void Close() + internal void CloseDriver() { - //TODO: rollback any pending transaction - if (State == ConnectionState.Closed) return; +#if !CF + driver.CurrentTransaction = null; +#endif - if (dataReader != null) - dataReader.Close(); - if (settings.Pooling) { // if we are in a transaction, roll it back @@ -527,7 +572,24 @@ } else driver.Close(); + } + /// + public override void Close() + { + if (State == ConnectionState.Closed) return; + + if (dataReader != null) + dataReader.Close(); +#if !CF + if (driver.CurrentTransaction == null) +#endif + CloseDriver(); +#if !CF + else + driver.IsInActiveUse = false; +#endif + SetState(ConnectionState.Closed); } Modified: branches/5.1/Driver/Source/Driver.cs =================================================================== --- branches/5.1/Driver/Source/Driver.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Source/Driver.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -47,6 +47,10 @@ protected Hashtable charSets; protected bool hasWarnings; protected long maxPacketSize; +#if !CF + protected MySqlPromotableTransaction currentTransaction; + protected bool inActiveUse; +#endif public Driver(MySqlConnectionStringBuilder settings) { @@ -97,6 +101,20 @@ get { return hasWarnings; } } +#if !CF + public MySqlPromotableTransaction CurrentTransaction + { + get { return currentTransaction; } + set { currentTransaction = value; } + } + + public bool IsInActiveUse + { + get { return inActiveUse; } + set { inActiveUse = value; } + } +#endif + #endregion public string Property(string key) Modified: branches/5.1/Driver/Source/MySqlPromotableTransaction.cs =================================================================== --- branches/5.1/Driver/Source/MySqlPromotableTransaction.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Source/MySqlPromotableTransaction.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -20,6 +20,7 @@ using System; using System.Transactions; +using System.Collections; namespace MySql.Data.MySqlClient { @@ -49,14 +50,16 @@ { simpleTransaction.Rollback(); singlePhaseEnlistment.Aborted(); - connection.CurrentTransaction = null; + DriverTransactionManager.RemoveDriverInTransaction(baseTransaction); + connection.CloseDriver(); } void IPromotableSinglePhaseNotification.SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment) { simpleTransaction.Commit(); singlePhaseEnlistment.Committed(); - connection.CurrentTransaction = null; + DriverTransactionManager.RemoveDriverInTransaction(baseTransaction); + connection.CloseDriver(); } byte[] ITransactionPromoter.Promote() @@ -64,5 +67,35 @@ throw new NotSupportedException(); } } + + internal class DriverTransactionManager + { + private static Hashtable driversInUse = new Hashtable(); + + public static Driver GetDriverInTransaction(Transaction transaction) + { + lock (driversInUse.SyncRoot) + { + Driver d = (Driver)driversInUse[transaction.GetHashCode()]; + return d; + } + } + + public static void SetDriverInTransaction(Driver driver) + { + lock (driversInUse.SyncRoot) + { + driversInUse[driver.CurrentTransaction.BaseTransaction.GetHashCode()] = driver; + } + } + + public static void RemoveDriverInTransaction(Transaction transaction) + { + lock (driversInUse.SyncRoot) + { + driversInUse.Remove(transaction.GetHashCode()); + } + } + } } Modified: branches/5.1/Driver/Source/command.cs =================================================================== --- branches/5.1/Driver/Source/command.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Source/command.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -273,7 +273,8 @@ private void CheckState() { // There must be a valid and open connection. - if (connection == null || connection.State != ConnectionState.Open) + if ((connection == null || connection.State != ConnectionState.Open) && + !connection.SoftClosed) throw new InvalidOperationException("Connection must be valid and open"); // Data readers have to be closed first Modified: branches/5.1/Driver/Source/transaction.cs =================================================================== --- branches/5.1/Driver/Source/transaction.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/Driver/Source/transaction.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -80,7 +80,7 @@ /// public override void Commit() { - if (conn == null || conn.State != ConnectionState.Open) + if (conn == null || (conn.State != ConnectionState.Open && !conn.SoftClosed)) throw new InvalidOperationException("Connection must be valid and open to commit transaction"); if (!open) throw new InvalidOperationException("Transaction has already been committed or is not pending"); @@ -92,8 +92,8 @@ /// public override void Rollback() { - if (conn == null || conn.State != ConnectionState.Open) - throw new InvalidOperationException("Connection must be valid and open to commit transaction"); + if (conn == null || (conn.State != ConnectionState.Open && !conn.SoftClosed)) + throw new InvalidOperationException("Connection must be valid and open to rollback transaction"); if (!open) throw new InvalidOperationException("Transaction has already been rolled back or is not pending"); MySqlCommand cmd = new MySqlCommand("ROLLBACK", conn); Modified: branches/5.1/TestSuite/Source/BaseTest.cs =================================================================== --- branches/5.1/TestSuite/Source/BaseTest.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/TestSuite/Source/BaseTest.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -44,12 +44,12 @@ protected string rootUser; protected string rootPassword; protected Version version; + protected bool pooling; public BaseTest() { databases = new string[2]; - csAdditions = ";pooling=false;"; user = "test"; password = "test"; host = "localhost"; @@ -113,16 +113,25 @@ return String.Format("protocol=sockets;port={0}", port); } - protected string GetConnectionString(bool includedb) + protected string GetConnectionStringBasic(bool includedb) { string connStr = String.Format("server={0};user id={1};password={2};" + - "persist security info=true;{3}", host, user, password, csAdditions); + "persist security info=true;", host, user, password); if (includedb) connStr += String.Format("database={0};", databases[0]); + if (!pooling) + connStr += ";pooling=false;"; connStr += GetConnectionInfo(); return connStr; } + protected string GetConnectionString(bool includedb) + { + string connStr = String.Format("{0};{1}", + GetConnectionStringBasic(includedb), csAdditions); + return connStr; + } + protected string GetConnectionStringEx(string user, string pw, bool includedb) { string connStr = String.Format("server={0};user id={1};" + @@ -186,6 +195,7 @@ { try { + pooling = true; IDataReader reader = execReader("SHOW TABLES LIKE 'Test'"); bool exists = reader.Read(); reader.Close(); @@ -265,5 +275,12 @@ return cmd.ExecuteReader(); } + protected int CountProcesses() + { + MySqlDataAdapter da = new MySqlDataAdapter("SHOW PROCESSLIST", conn); + DataTable dt = new DataTable(); + da.Fill(dt); + return dt.Rows.Count; + } } } Modified: branches/5.1/TestSuite/Source/Transactions.cs =================================================================== --- branches/5.1/TestSuite/Source/Transactions.cs 2007-08-03 17:44:45 UTC (rev 837) +++ branches/5.1/TestSuite/Source/Transactions.cs 2007-08-07 18:29:20 UTC (rev 838) @@ -33,7 +33,6 @@ [TestFixtureSetUp] public void FixtureSetup() { - csAdditions += ";pooling=true;"; Open(); } @@ -284,54 +283,158 @@ Assert.AreEqual(1, cmd2.ExecuteScalar()); } + private void ManuallyEnlistingInitialConnection(bool complete) + { + using (TransactionScope ts = new TransactionScope()) + { + string connStr = GetConnectionStringBasic(true) + ";auto enlist=false"; -/* [Test] - public void XATransaction1Rollback() - { - XATransaction1(false); + using (MySqlConnection c1 = new MySqlConnection(connStr)) + { + c1.Open(); + c1.EnlistTransaction(Transaction.Current); + MySqlCommand cmd1 = new MySqlCommand("INSERT INTO test (key2) VALUES ('a')", c1); + cmd1.ExecuteNonQuery(); + } + + using (MySqlConnection c2 = new MySqlConnection(connStr)) + { + c2.Open(); + c2.EnlistTransaction(Transaction.Current); + MySqlCommand cmd2 = new MySqlCommand("INSERT INTO test (key2) VALUES ('b')", c2); + cmd2.ExecuteNonQuery(); + } + if (complete) + ts.Complete(); + } } [Test] - public void XATransaction1Commit() + public void ManuallyEnlistingInitialConnection() { - XATransaction1(true); + ManuallyEnlistingInitialConnection(true); + ManuallyEnlistingInitialConnection(false); } - private void XATransaction1(bool commit) + [Test] + public void ManualEnlistmentWithActiveConnection() { - try + using (TransactionScope ts = new TransactionScope()) { - using (TransactionScope ts = new TransactionScope()) + string connStr = GetConnectionStringBasic(true); + + using (MySqlConnection c1 = new MySqlConnection(connStr)) { - using (MySqlConnection c = new MySqlConnection(GetConnectionString(true))) + c1.Open(); + + connStr += "; auto enlist=false"; + using (MySqlConnection c2 = new MySqlConnection(connStr)) { - c.Open(); - - MySqlCommand cmd = new MySqlCommand("INSERT INTO test VALUES ('a', 'name', 'name2')", c); - cmd.ExecuteNonQuery(); + c2.Open(); + try + { + c2.EnlistTransaction(Transaction.Current); + } + catch (NotSupportedException) + { + } } + } + } + } - using (MySqlConnection c2 = new MySqlConnection(GetConnectionString(true))) + [Test] + public void AttemptToEnlistTwoConnections() + { + using (TransactionScope ts = new TransactionScope()) + { + string connStr = GetConnectionStringBasic(true); + + using (MySqlConnection c1 = new MySqlConnection(connStr)) + { + c1.Open(); + + using (MySqlConnection c2 = new MySqlConnection(connStr)) { - c2.Open(); - MySqlCommand cmd2 = new MySqlCommand("INSERT INTO test VALUES ('b', 'name', 'name2')", c2); - cmd2.ExecuteNonQuery(); + try + { + c2.Open(); + } + catch (NotSupportedException) + { + } } + } + } + } - if (commit) + private void ReusingSameConnection(bool pooling, bool complete) + { + execSQL("TRUNCATE TABLE test"); + using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, TimeSpan.MaxValue)) + { + string connStr = GetConnectionStringBasic(true); + if (!pooling) + connStr += ";pooling=false"; + + using (MySqlConnection c1 = new MySqlConnection(connStr)) + { + c1.Open(); + MySqlCommand cmd1 = new MySqlCommand("INSERT INTO test (key2) VALUES ('a')", c1); + cmd1.ExecuteNonQuery(); + } + + using (MySqlConnection c2 = new MySqlConnection(connStr)) + { + c2.Open(); + MySqlCommand cmd2 = new MySqlCommand("INSERT INTO test (key2) VALUES ('b')", c2); + cmd2.ExecuteNonQuery(); + } + + try + { + if (complete) ts.Complete(); } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } - MySqlCommand cmd3 = new MySqlCommand("SELECT COUNT(*) FROM test", conn); - object count = cmd3.ExecuteScalar(); - Assert.AreEqual(commit ? 2 : 0, count); + MySqlDataAdapter da = new MySqlDataAdapter("SELECT * FROM test", conn); + DataTable dt = new DataTable(); + da.Fill(dt); + if (complete) + { + Assert.AreEqual(2, dt.Rows.Count); + Assert.AreEqual("a", dt.Rows[0][0]); + Assert.AreEqual("b", dt.Rows[1][0]); } - catch (Exception ex) + else { - Assert.Fail(ex.Message); + Assert.AreEqual(0, dt.Rows.Count); } } - */ + + [Test] + public void ReusingSameConnection() + { + int processes = CountProcesses(); + + ReusingSameConnection(true, true); + Assert.AreEqual(processes + 1, CountProcesses()); + + ReusingSameConnection(true, false); + Assert.AreEqual(processes + 1, CountProcesses()); + + ReusingSameConnection(false, true); + Assert.AreEqual(processes + 1, CountProcesses()); + + ReusingSameConnection(false, false); + Assert.AreEqual(processes + 1, CountProcesses()); + } + #endif }