diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 46ce26373c..c2df6e7d6a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -434,6 +434,9 @@ Microsoft\Data\SqlClient\SqlDataAdapter.cs + + Microsoft\Data\SqlClient\SqlDelegatedTransaction.cs + Microsoft\Data\SqlClient\SqlDependency.cs @@ -664,8 +667,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.NetCoreApp.cs deleted file mode 100644 index 561f9bd6fb..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.NetCoreApp.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Transactions; - -namespace Microsoft.Data.SqlClient -{ - internal sealed partial class SqlDelegatedTransaction : IPromotableSinglePhaseNotification - { - // Get the server-side Global Transaction Id from the PromotedDTCToken - // Skip first 4 bytes since they contain the version - private Guid GetGlobalTxnIdentifierFromToken() => new Guid(new ReadOnlySpan(_connection.PromotedDTCToken, _globalTransactionsTokenVersionSizeInBytes, 16)); - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs deleted file mode 100644 index 4f5a70bf7f..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ /dev/null @@ -1,520 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Transactions; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - sealed internal partial class SqlDelegatedTransaction : IPromotableSinglePhaseNotification - { - private static int _objectTypeCount; - internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); - - private const int _globalTransactionsTokenVersionSizeInBytes = 4; // the size of the version in the PromotedDTCToken for Global Transactions - - // WARNING!!! Multithreaded object! - // Locking strategy: Any potentailly-multithreaded operation must first lock the associated connection, then - // validate this object's active state. Locked activities should ONLY include Sql-transaction state altering activities - // or notifications of same. Updates to the connection's association with the transaction or to the connection pool - // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy. - - private SqlInternalConnection _connection; // the internal connection that is the root of the transaction - private System.Data.IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server - private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to - - private Transaction _atomicTransaction; - - private bool _active; // Is the transaction active? - - internal SqlDelegatedTransaction(SqlInternalConnection connection, Transaction tx) - { - Debug.Assert(connection != null, "null connection?"); - _connection = connection; - _atomicTransaction = tx; - _active = false; - System.Transactions.IsolationLevel systxIsolationLevel = (System.Transactions.IsolationLevel)tx.IsolationLevel; - - // We need to map the System.Transactions IsolationLevel to the one - // that System.Data uses and communicates to SqlServer. We could - // arguably do that in Initialize when the transaction is delegated, - // however it is better to do this before we actually begin the process - // of delegation, in case System.Transactions adds another isolation - // level we don't know about -- we can throw the exception at a better - // place. - switch (systxIsolationLevel) - { - case System.Transactions.IsolationLevel.ReadCommitted: - _isolationLevel = System.Data.IsolationLevel.ReadCommitted; - break; - case System.Transactions.IsolationLevel.ReadUncommitted: - _isolationLevel = System.Data.IsolationLevel.ReadUncommitted; - break; - case System.Transactions.IsolationLevel.RepeatableRead: - _isolationLevel = System.Data.IsolationLevel.RepeatableRead; - break; - case System.Transactions.IsolationLevel.Serializable: - _isolationLevel = System.Data.IsolationLevel.Serializable; - break; - case System.Transactions.IsolationLevel.Snapshot: - _isolationLevel = System.Data.IsolationLevel.Snapshot; - break; - default: - throw SQL.UnknownSysTxIsolationLevel(systxIsolationLevel); - } - } - - internal Transaction Transaction - { - get { return _atomicTransaction; } - } - - public void Initialize() - { - // if we get here, then we know for certain that we're the delegated - // transaction. - SqlInternalConnection connection = _connection; - SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | Object Id {0}, Client Connection Id {1}, delegating transaction.", ObjectID, usersConnection?.ClientConnectionId); -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - if (connection.IsEnlistedInTransaction) - { - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | {0}, Client Connection Id {1}, was enlisted, now defecting.", ObjectID, usersConnection?.ClientConnectionId); - - // defect first - connection.EnlistNull(); - } - - _internalTransaction = new SqlInternalTransaction(connection, TransactionType.Delegated, null); - - connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Begin, null, _isolationLevel, _internalTransaction, true); - - // Handle case where ExecuteTran didn't produce a new transaction, but also didn't throw. - if (connection.CurrentTransaction == null) - { - connection.DoomThisConnection(); - throw ADP.InternalError(ADP.InternalErrorCode.UnknownTransactionFailure); - } - - _active = true; - } - catch (System.OutOfMemoryException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - usersConnection.Abort(e); - throw; - } - } - - internal bool IsActive - { - get - { - return _active; - } - } - - public byte[] Promote() - { - // Operations that might be affected by multi-threaded use MUST be done inside the lock. - // Don't read values off of the connection outside the lock unless it doesn't really matter - // from an operational standpoint (i.e. logging connection's ObjectID should be fine, - // but the PromotedDTCToken can change over calls. so that must be protected). - SqlInternalConnection connection = GetValidConnection(); - Exception promoteException; - byte[] returnValue = null; - - if (connection != null) - { - SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, promoting transaction.", ObjectID, usersConnection?.ClientConnectionId); -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - lock (connection) - { - try - { - // Now that we've acquired the lock, make sure we still have valid state for this operation. - ValidateActiveOnConnection(connection); - - connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Promote, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true); - returnValue = connection.PromotedDTCToken; - - // For Global Transactions, we need to set the Transaction Id since we use a Non-MSDTC Promoter type. - if (connection.IsGlobalTransaction) - { - if (SysTxForGlobalTransactions.SetDistributedTransactionIdentifier == null) - { - throw SQL.UnsupportedSysTxForGlobalTransactions(); - } - - if (!connection.IsGlobalTransactionsEnabledForServer) - { - throw SQL.GlobalTransactionsNotEnabled(); - } - - SysTxForGlobalTransactions.SetDistributedTransactionIdentifier.Invoke(_atomicTransaction, new object[] { this, GetGlobalTxnIdentifierFromToken() }); - } - - promoteException = null; - } - catch (SqlException e) - { - promoteException = e; - - ADP.TraceExceptionWithoutRethrow(e); - - // Doom the connection, to make sure that the transaction is - // eventually rolled back. - // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event - connection.DoomThisConnection(); - } - catch (InvalidOperationException e) - { - promoteException = e; - ADP.TraceExceptionWithoutRethrow(e); - connection.DoomThisConnection(); - } - } - } - catch (System.OutOfMemoryException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - usersConnection.Abort(e); - throw; - } - - //Throw exception only if Transaction is still active and not yet aborted. - if (promoteException != null) - { - try - { - // Safely access Transaction status - as it's possible Transaction is not in right state. - if (Transaction?.TransactionInformation?.Status != TransactionStatus.Aborted) - { - throw SQL.PromotionFailed(promoteException); - } - } - catch (TransactionException te) - { - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, Transaction exception occurred: {2}.", ObjectID, usersConnection?.ClientConnectionId, te.Message); - // Throw promote exception if transaction state is unknown. - throw SQL.PromotionFailed(promoteException); - } - } - else - { - // The transaction was aborted externally, since it's already doomed above, we only log the same. - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, Aborted during promotion.", ObjectID, usersConnection?.ClientConnectionId); - } - } - else - { - // The transaction was aborted externally, doom the connection to make sure it's eventually rolled back and log the same. - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | {0}, Connection null, aborted before promoting.", ObjectID); - } - return returnValue; - } - - // Called by transaction to initiate abort sequence - public void Rollback(SinglePhaseEnlistment enlistment) - { - Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); - - if (connection != null) - { - SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Rollback | RES | CPOOL | Object Id {0}, Client Connection Id {1}, rolling back transaction.", ObjectID, usersConnection?.ClientConnectionId); -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - lock (connection) - { - try - { - // Now that we've acquired the lock, make sure we still have valid state for this operation. - ValidateActiveOnConnection(connection); - _active = false; // set to inactive first, doesn't matter how the execute completes, this transaction is done. - _connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event - - // If we haven't already rolled back (or aborted) then tell the SQL Server to roll back - if (!_internalTransaction.IsAborted) - { - connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true); - } - } - catch (SqlException e) - { - ADP.TraceExceptionWithoutRethrow(e); - - // Doom the connection, to make sure that the transaction is - // eventually rolled back. - // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event - connection.DoomThisConnection(); - - // Unlike SinglePhaseCommit, a rollback is a rollback, regardless - // of how it happens, so SysTx won't throw an exception, and we - // don't want to throw an exception either, because SysTx isn't - // handling it and it may create a fail fast scenario. In the end, - // there is no way for us to communicate to the consumer that this - // failed for more serious reasons than usual. - // - // This is a bit like "should you throw if Close fails", however, - // it only matters when you really need to know. In that case, - // we have the tracing that we're doing to fallback on for the - // investigation. - } - catch (InvalidOperationException e) - { - ADP.TraceExceptionWithoutRethrow(e); - connection.DoomThisConnection(); - } - } - - // it doesn't matter whether the rollback succeeded or not, we presume - // that the transaction is aborted, because it will be eventually. - connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction); - enlistment.Aborted(); - } - catch (System.OutOfMemoryException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - usersConnection.Abort(e); - throw; - } - } - else - { - // The transaction was aborted, report that to SysTx and log the same. - enlistment.Aborted(); - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Rollback | RES | CPOOL | Object Id {0}, Connection null, aborted before rollback.", ObjectID); - } - } - - // Called by the transaction to initiate commit sequence - public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) - { - Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); - - if (connection != null) - { - SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.SinglePhaseCommit | RES | CPOOL | Object Id {0}, Client Connection Id {1}, committing transaction.", ObjectID, usersConnection?.ClientConnectionId); -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - // If the connection is doomed, we can be certain that the - // transaction will eventually be rolled back or has already been aborted externally, and we shouldn't - // attempt to commit it. - if (connection.IsConnectionDoomed) - { - lock (connection) - { - _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done. - _connection = null; - } - - enlistment.Aborted(SQL.ConnectionDoomed()); - } - else - { - Exception commitException; - lock (connection) - { - try - { - // Now that we've acquired the lock, make sure we still have valid state for this operation. - ValidateActiveOnConnection(connection); - - _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done. - _connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event - - connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true); - commitException = null; - } - catch (SqlException e) - { - commitException = e; - - ADP.TraceExceptionWithoutRethrow(e); - - // Doom the connection, to make sure that the transaction is - // eventually rolled back. - // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event - connection.DoomThisConnection(); - } - catch (InvalidOperationException e) - { - commitException = e; - ADP.TraceExceptionWithoutRethrow(e); - connection.DoomThisConnection(); - } - } - if (commitException != null) - { - // connection.ExecuteTransaction failed with exception - if (_internalTransaction.IsCommitted) - { - // Even though we got an exception, the transaction - // was committed by the server. - enlistment.Committed(); - } - else if (_internalTransaction.IsAborted) - { - // The transaction was aborted, report that to - // SysTx. - enlistment.Aborted(commitException); - } - else - { - // The transaction is still active, we cannot - // know the state of the transaction. - enlistment.InDoubt(commitException); - } - - // We eat the exception. This is called on the SysTx - // thread, not the applications thread. If we don't - // eat the exception an UnhandledException will occur, - // causing the process to FailFast. - } - - connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction); - if (commitException == null) - { - // connection.ExecuteTransaction succeeded - enlistment.Committed(); - } - } - } - catch (System.OutOfMemoryException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - usersConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - usersConnection.Abort(e); - throw; - } - } - else - { - // The transaction was aborted before we could commit, report that to SysTx and log the same. - enlistment.Aborted(); - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.SinglePhaseCommit | RES | CPOOL | Object Id {0}, Connection null, aborted before commit.", ObjectID); - } - } - - // Event notification that transaction ended. This comes from the subscription to the Transaction's - // ended event via the internal connection. If it occurs without a prior Rollback or SinglePhaseCommit call, - // it indicates the transaction was ended externally (generally that one of the DTC participants aborted - // the transaction). - internal void TransactionEnded(Transaction transaction) - { - SqlInternalConnection connection = _connection; - - if (connection != null) - { - SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.TransactionEnded | RES | CPOOL | Object Id {0}, Connection Id {1}, transaction completed externally.", ObjectID, connection.ObjectID); - lock (connection) - { - if (_atomicTransaction.Equals(transaction)) - { - // No need to validate active on connection, this operation can be called on completed transactions - _active = false; - _connection = null; - } - // Safest approach is to doom this connection, whose transaction has been aborted externally. - // If we want to avoid dooming the connection for performance, state needs to be properly restored. (future TODO) - connection.DoomThisConnection(); - } - } - } - - // Check for connection validity - private SqlInternalConnection GetValidConnection() - { - SqlInternalConnection connection = _connection; - if (connection == null && Transaction.TransactionInformation.Status != TransactionStatus.Aborted) - { - throw ADP.ObjectDisposed(this); - } - - return connection; - } - - // Dooms connection and throws and error if not a valid, active, delegated transaction for the given - // connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return - // may not be trusted. - private void ValidateActiveOnConnection(SqlInternalConnection connection) - { - bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this); - - if (!valid) - { - // Invalid indicates something BAAAD happened (Commit after TransactionEnded, for instance) - // Doom anything remotely involved. - if (connection != null) - { - connection.DoomThisConnection(); - } - if (connection != _connection && _connection != null) - { - _connection.DoomThisConnection(); - } - - throw ADP.InternalError(ADP.InternalErrorCode.UnpooledObjectHasWrongOwner); //TODO: Create a new code - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index ef8113aa21..d7b2469fa5 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -577,6 +577,9 @@ Microsoft\Data\SqlClient\SqlDataAdapter.cs + + Microsoft\Data\SqlClient\SqlDelegatedTransaction.cs + Microsoft\Data\SqlClient\SqlDependency.cs @@ -780,7 +783,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs similarity index 87% rename from src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs index 1782538ae3..174a1eb1bb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs @@ -3,31 +3,23 @@ // See the LICENSE file in the project root for more information. using System; -using System.Data; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.Data.Common; using System.Transactions; +using Microsoft.Data.Common; namespace Microsoft.Data.SqlClient { - - sealed internal class SqlDelegatedTransaction : IPromotableSinglePhaseNotification + internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotification { private static int _objectTypeCount; - private readonly int _objectID = Interlocked.Increment(ref _objectTypeCount); + internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); + private const int _globalTransactionsTokenVersionSizeInBytes = 4; // the size of the version in the PromotedDTCToken for Global Transactions - internal int ObjectID - { - get - { - return _objectID; - } - } // WARNING!!! Multithreaded object! - // Locking strategy: Any potentailly-multithreaded operation must first lock the associated connection, then + // Locking strategy: Any potentially-multithreaded operation must first lock the associated connection, then // validate this object's active state. Locked activities should ONLY include Sql-transaction state altering activities // or notifications of same. Updates to the connection's association with the transaction or to the connection pool // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy. @@ -35,6 +27,7 @@ internal int ObjectID private SqlInternalConnection _connection; // the internal connection that is the root of the transaction private System.Data.IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to + private Transaction _atomicTransaction; private bool _active; // Is the transaction active? @@ -87,12 +80,13 @@ public void Initialize() // transaction. SqlInternalConnection connection = _connection; SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, delegating transaction.", ObjectID, connection.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | Object Id {0}, Client Connection Id {1}, delegating transaction.", ObjectID, usersConnection?.ClientConnectionId); +#if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); - +#endif try { -#if DEBUG +#if NETFRAMEWORK && DEBUG TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection(); RuntimeHelpers.PrepareConstrainedRegions(); @@ -101,10 +95,10 @@ public void Initialize() tdsReliabilitySection.Start(); #else { -#endif //DEBUG +#endif if (connection.IsEnlistedInTransaction) { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, was enlisted, now defecting.", ObjectID, connection.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | {0}, Client Connection Id {1}, was enlisted, now defecting.", ObjectID, usersConnection?.ClientConnectionId); // defect first connection.EnlistNull(); @@ -123,12 +117,12 @@ public void Initialize() _active = true; } -#if DEBUG +#if NETFRAMEWORK && DEBUG finally { tdsReliabilitySection.Stop(); } -#endif //DEBUG +#endif } catch (System.OutOfMemoryException e) { @@ -155,25 +149,26 @@ internal bool IsActive } } - public Byte[] Promote() + public byte[] Promote() { // Operations that might be affected by multi-threaded use MUST be done inside the lock. // Don't read values off of the connection outside the lock unless it doesn't really matter // from an operational standpoint (i.e. logging connection's ObjectID should be fine, // but the PromotedDTCToken can change over calls. so that must be protected). SqlInternalConnection connection = GetValidConnection(); - Exception promoteException; byte[] returnValue = null; + if (connection != null) { SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, promoting transaction.", ObjectID, connection.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, promoting transaction.", ObjectID, usersConnection?.ClientConnectionId); +#if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); - +#endif try { -#if DEBUG +#if NETFRAMEWORK && DEBUG TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection(); RuntimeHelpers.PrepareConstrainedRegions(); @@ -182,7 +177,7 @@ public Byte[] Promote() tdsReliabilitySection.Start(); #else { -#endif //DEBUG +#endif lock (connection) { try @@ -230,12 +225,12 @@ public Byte[] Promote() } } } -#if DEBUG +#if NETFRAMEWORK && DEBUG finally { tdsReliabilitySection.Stop(); } -#endif //DEBUG +#endif } catch (System.OutOfMemoryException e) { @@ -258,13 +253,13 @@ public Byte[] Promote() { try { - // Safely access Transction status - as it's possible Transaction is not in right state. - if(Transaction?.TransactionInformation?.Status == System.Transactions.TransactionStatus.Aborted) + // Safely access Transaction status - as it's possible Transaction is not in right state. + if (Transaction?.TransactionInformation?.Status != TransactionStatus.Aborted) { throw SQL.PromotionFailed(promoteException); } } - catch(TransactionException te) + catch (TransactionException te) { SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, Transaction exception occurred: {2}.", ObjectID, usersConnection?.ClientConnectionId, te.Message); // Throw promote exception if transaction state is unknown. @@ -274,13 +269,13 @@ public Byte[] Promote() else { // The transaction was aborted externally, since it's already doomed above, we only log the same. - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, aborted during promote.", ObjectID, connection.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | Object Id {0}, Client Connection Id {1}, Aborted during promotion.", ObjectID, usersConnection?.ClientConnectionId); } } else { // The transaction was aborted externally, doom the connection to make sure it's eventually rolled back and log the same. - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection null, aborted before promoting.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Promote | RES | CPOOL | {0}, Connection null, aborted before promoting.", ObjectID); } return returnValue; } @@ -293,13 +288,15 @@ public void Rollback(SinglePhaseEnlistment enlistment) if (connection != null) { -#if DEBUG +#if NETFRAMEWORK && DEBUG TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection(); tdsReliabilitySection.Start(); #endif //DEBUG SqlConnection usersConnection = connection.Connection; + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Rollback | RES | CPOOL | Object Id {0}, Client Connection Id {1}, rolling back transaction.", ObjectID, usersConnection?.ClientConnectionId); +#if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, rolling back transaction.", ObjectID, connection.ObjectID); +#endif try { lock (connection) @@ -326,15 +323,15 @@ public void Rollback(SinglePhaseEnlistment enlistment) // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event connection.DoomThisConnection(); - // Unlike SinglePhaseCommit, a rollback is a rollback, regardless + // Unlike SinglePhaseCommit, a rollback is a rollback, regardless // of how it happens, so SysTx won't throw an exception, and we - // don't want to throw an exception either, because SysTx isn't + // don't want to throw an exception either, because SysTx isn't // handling it and it may create a fail fast scenario. In the end, // there is no way for us to communicate to the consumer that this // failed for more serious reasons than usual. - // + // // This is a bit like "should you throw if Close fails", however, - // it only matters when you really need to know. In that case, + // it only matters when you really need to know. In that case, // we have the tracing that we're doing to fallback on for the // investigation. } @@ -365,18 +362,18 @@ public void Rollback(SinglePhaseEnlistment enlistment) usersConnection.Abort(e); throw; } -#if DEBUG +#if NETFRAMEWORK && DEBUG finally { tdsReliabilitySection.Stop(); } -#endif //DEBUG +#endif } else { // The transaction was aborted, report that to SysTx and log the same. enlistment.Aborted(); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection null, aborted before rollback.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Rollback | RES | CPOOL | Object Id {0}, Connection null, aborted before rollback.", ObjectID); } } @@ -389,21 +386,23 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) if (connection != null) { SqlConnection usersConnection = connection.Connection; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, committing transaction.", ObjectID, connection.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.SinglePhaseCommit | RES | CPOOL | Object Id {0}, Client Connection Id {1}, committing transaction.", ObjectID, usersConnection?.ClientConnectionId); +#if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); +#endif try { -#if DEBUG +#if NETFRAMEWORK && DEBUG TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection(); try { tdsReliabilitySection.Start(); #else { -#endif //DEBUG +#endif // If the connection is doomed, we can be certain that the - // transaction will eventually be rolled back, and we shouldn't + // transaction will eventually be rolled back or has already been aborted externally, and we shouldn't // attempt to commit it. if (connection.IsConnectionDoomed) { @@ -412,6 +411,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done. _connection = null; } + enlistment.Aborted(SQL.ConnectionDoomed()); } else @@ -425,7 +425,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) ValidateActiveOnConnection(connection); _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done. - _connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event + _connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true); commitException = null; @@ -471,7 +471,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) } // We eat the exception. This is called on the SysTx - // thread, not the applications thread. If we don't + // thread, not the applications thread. If we don't // eat the exception an UnhandledException will occur, // causing the process to FailFast. } @@ -484,12 +484,12 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) } } } -#if DEBUG +#if NETFRAMEWORK && DEBUG finally { tdsReliabilitySection.Stop(); } -#endif //DEBUG +#endif } catch (System.OutOfMemoryException e) { @@ -511,13 +511,13 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) { // The transaction was aborted before we could commit, report that to SysTx and log the same. enlistment.Aborted(); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection null, aborted before commit.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.SinglePhaseCommit | RES | CPOOL | Object Id {0}, Connection null, aborted before commit.", ObjectID); } } // Event notification that transaction ended. This comes from the subscription to the Transaction's // ended event via the internal connection. If it occurs without a prior Rollback or SinglePhaseCommit call, - // it indicates the transaction was ended externally (generally that one the the DTC participants aborted + // it indicates the transaction was ended externally (generally that one of the DTC participants aborted // the transaction). internal void TransactionEnded(Transaction transaction) { @@ -525,8 +525,7 @@ internal void TransactionEnded(Transaction transaction) if (connection != null) { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection {1}, transaction completed externally.", ObjectID, connection.ObjectID); - + SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.TransactionEnded | RES | CPOOL | Object Id {0}, Connection Id {1}, transaction completed externally.", ObjectID, connection.ObjectID); lock (connection) { if (_atomicTransaction.Equals(transaction)) @@ -546,7 +545,7 @@ internal void TransactionEnded(Transaction transaction) private SqlInternalConnection GetValidConnection() { SqlInternalConnection connection = _connection; - if (connection == null && _atomicTransaction.TransactionInformation.Status != TransactionStatus.Aborted) + if (connection == null && Transaction.TransactionInformation.Status != TransactionStatus.Aborted) { throw ADP.ObjectDisposed(this); } @@ -582,9 +581,13 @@ private void ValidateActiveOnConnection(SqlInternalConnection connection) // Skip first 4 bytes since they contain the version private Guid GetGlobalTxnIdentifierFromToken() { +#if NET + return new Guid(new ReadOnlySpan(_connection.PromotedDTCToken, _globalTransactionsTokenVersionSizeInBytes, 16)); +#else byte[] txnGuid = new byte[16]; Buffer.BlockCopy(_connection.PromotedDTCToken, _globalTransactionsTokenVersionSizeInBytes /* Skip the version */, txnGuid, 0, txnGuid.Length); return new Guid(txnGuid); +#endif } } }