MySQL.Data/src/Framework/netstandard2_0/MySqlPromotableTransaction.cs (164 lines of code) (raw):

// Copyright © 2004, 2025, Oracle and/or its affiliates. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License, version 2.0, as // published by the Free Software Foundation. // // This program is designed to work with certain software (including // but not limited to OpenSSL) that is licensed under separate terms, as // designated in a particular file or component or in included license // documentation. The authors of MySQL hereby grant you an additional // permission to link the program and your derivative works with the // separately licensed software that they have either included with // the program or referenced in the documentation. // // Without limiting anything contained in the foregoing, this file, // which is part of MySQL Connector/NET, is also subject to the // Universal FOSS Exception, version 1.0, a copy of which can be found at // http://oss.oracle.com/licenses/universal-foss-exception. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License, version 2.0, for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA using System; using System.Transactions; using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; 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 int rollbackThreadId; public MySqlTransactionScope(MySqlConnection con, Transaction trans, MySqlTransaction simpleTransaction) { connection = con; baseTransaction = trans; this.simpleTransaction = simpleTransaction; } public async Task RollbackAsync(SinglePhaseEnlistment singlePhaseEnlistment, bool execAsync) { // prevent commands in main thread to run concurrently Driver driver = connection.driver; SemaphoreSlim semaphoreSlim = new(1); semaphoreSlim.Wait(); rollbackThreadId = Thread.CurrentThread.ManagedThreadId; 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); driver.currentTransaction = null; if (connection.State == ConnectionState.Closed) await connection.CloseFullyAsync(execAsync).ConfigureAwait(false); rollbackThreadId = 0; semaphoreSlim.Release(); } public async Task SinglePhaseCommitAsync(SinglePhaseEnlistment singlePhaseEnlistment, bool execAsync) { simpleTransaction.Commit(); singlePhaseEnlistment.Committed(); DriverTransactionManager.RemoveDriverInTransaction(baseTransaction); connection.driver.currentTransaction = null; if (connection.State == ConnectionState.Closed) await connection.CloseFullyAsync(execAsync).ConfigureAwait(false); } public void ChangeConnection(MySqlConnection connection) { this.connection = connection; this.simpleTransaction.Connection = connection; } } internal sealed class MySqlPromotableTransaction : IPromotableSinglePhaseNotification, ITransactionPromoter { // Per-thread stack to manage nested transaction scopes [ThreadStatic] static Stack<MySqlTransactionScope> globalScopeStack; MySqlConnection connection; Transaction baseTransaction; Stack<MySqlTransactionScope> scopeStack; public MySqlPromotableTransaction(MySqlConnection connection, Transaction baseTransaction) { this.connection = connection; this.baseTransaction = baseTransaction; } public Transaction BaseTransaction { get { if (scopeStack.Count > 0) return scopeStack.Peek().baseTransaction; else return null; } } public MySqlConnection Connection { get { return connection; } set { connection = value; if (scopeStack.Count > 0) scopeStack.Peek().ChangeConnection(value); } } public bool InRollback { get { if (scopeStack.Count > 0) { MySqlTransactionScope currentScope = scopeStack.Peek(); if (currentScope.rollbackThreadId == Thread.CurrentThread.ManagedThreadId) { return true; } } return false; } } void IPromotableSinglePhaseNotification.Initialize() { 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, "SESSION"); // 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. if (globalScopeStack == null) { globalScopeStack = new Stack<MySqlTransactionScope>(); } scopeStack = globalScopeStack; scopeStack.Push(new MySqlTransactionScope(connection, baseTransaction, simpleTransaction)); } void IPromotableSinglePhaseNotification.Rollback(SinglePhaseEnlistment singlePhaseEnlistment) { MySqlTransactionScope current = scopeStack.Peek(); current.RollbackAsync(singlePhaseEnlistment, false).GetAwaiter().GetResult(); scopeStack.Pop(); } void IPromotableSinglePhaseNotification.SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment) { scopeStack.Pop().SinglePhaseCommitAsync(singlePhaseEnlistment, false).GetAwaiter().GetResult(); } byte[] ITransactionPromoter.Promote() { 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()); } } } }