|
//------------------------------------------------------------------------------
// <copyright file="SqlDelegatedTransaction.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="true" primary="false">Microsoft</owner>
//------------------------------------------------------------------------------
namespace System.Data.SqlClient {
using System.Data.Common;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ConstrainedExecution;
using System.Threading;
using SysTx = System.Transactions;
sealed internal class SqlDelegatedTransaction : SysTx.IPromotableSinglePhaseNotification {
private static int _objectTypeCount;
private readonly int _objectID = 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
// 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 IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server
private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to
private SysTx.Transaction _atomicTransaction;
private bool _active; // Is the transaction active?
internal SqlDelegatedTransaction(SqlInternalConnection connection, SysTx.Transaction tx) {
Debug.Assert(null != connection, "null connection?");
_connection = connection;
_atomicTransaction = tx;
_active = false;
SysTx.IsolationLevel systxIsolationLevel = 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 SysTx.IsolationLevel.ReadCommitted: _isolationLevel = IsolationLevel.ReadCommitted; break;
case SysTx.IsolationLevel.ReadUncommitted: _isolationLevel = IsolationLevel.ReadUncommitted; break;
case SysTx.IsolationLevel.RepeatableRead: _isolationLevel = IsolationLevel.RepeatableRead; break;
case SysTx.IsolationLevel.Serializable: _isolationLevel = IsolationLevel.Serializable; break;
case SysTx.IsolationLevel.Snapshot: _isolationLevel = IsolationLevel.Snapshot; break;
default:
throw SQL.UnknownSysTxIsolationLevel(systxIsolationLevel);
}
}
internal SysTx.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;
Bid.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, delegating transaction.\n", ObjectID, connection.ObjectID);
RuntimeHelpers.PrepareConstrainedRegions();
try {
#if DEBUG
TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
RuntimeHelpers.PrepareConstrainedRegions();
try {
tdsReliabilitySection.Start();
#else
{
#endif //DEBUG
if (connection.IsEnlistedInTransaction) { // defect first
Bid.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, was enlisted, now defecting.\n", ObjectID, connection.ObjectID);
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 (null == connection.CurrentTransaction)
{
connection.DoomThisConnection();
throw ADP.InternalError(ADP.InternalErrorCode.UnknownTransactionFailure);
}
_active = true;
}
#if DEBUG
finally {
tdsReliabilitySection.Stop();
}
#endif //DEBUG
}
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;
SqlConnection usersConnection = connection.Connection;
Bid.Trace("<sc.SqlDelegatedTransaction.Promote|RES|CPOOL> %d#, Connection %d#, promoting transaction.\n", ObjectID, connection.ObjectID);
RuntimeHelpers.PrepareConstrainedRegions();
try {
#if DEBUG
TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
RuntimeHelpers.PrepareConstrainedRegions();
try {
tdsReliabilitySection.Start();
#else
{
#endif //DEBUG
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, 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();
}
}
}
#if DEBUG
finally {
tdsReliabilitySection.Stop();
}
#endif //DEBUG
}
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;
}
if (promoteException != null) {
throw SQL.PromotionFailed(promoteException);
}
return returnValue;
}
// Called by transaction to initiate abort sequence
public void Rollback(SysTx.SinglePhaseEnlistment enlistment) {
Debug.Assert(null != enlistment, "null enlistment?");
SqlInternalConnection connection = GetValidConnection();
SqlConnection usersConnection = connection.Connection;
Bid.Trace("<sc.SqlDelegatedTransaction.Rollback|RES|CPOOL> %d#, Connection %d#, aborting transaction.\n", ObjectID, connection.ObjectID);
RuntimeHelpers.PrepareConstrainedRegions();
try {
#if DEBUG
TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
RuntimeHelpers.PrepareConstrainedRegions();
try {
tdsReliabilitySection.Start();
#else
{
#endif //DEBUG
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, 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();
}
#if DEBUG
finally {
tdsReliabilitySection.Stop();
}
#endif //DEBUG
}
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;
}
}
// Called by the transaction to initiate commit sequence
public void SinglePhaseCommit(SysTx.SinglePhaseEnlistment enlistment) {
Debug.Assert(null != enlistment, "null enlistment?");
SqlInternalConnection connection = GetValidConnection();
SqlConnection usersConnection = connection.Connection;
Bid.Trace("<sc.SqlDelegatedTransaction.SinglePhaseCommit|RES|CPOOL> %d#, Connection %d#, committing transaction.\n", ObjectID, connection.ObjectID);
RuntimeHelpers.PrepareConstrainedRegions();
try {
#if DEBUG
TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
RuntimeHelpers.PrepareConstrainedRegions();
try {
tdsReliabilitySection.Start();
#else
{
#endif //DEBUG
// If the connection is dooomed, we can be certain that the
// transaction will eventually be rolled back, 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, 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();
}
}
}
#if DEBUG
finally {
tdsReliabilitySection.Stop();
}
#endif //DEBUG
}
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;
}
}
// 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
// the transaction).
internal void TransactionEnded(SysTx.Transaction transaction) {
SqlInternalConnection connection = _connection;
if (connection != null) {
Bid.Trace("<sc.SqlDelegatedTransaction.TransactionEnded|RES|CPOOL> %d#, Connection %d#, transaction completed externally.\n", 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;
}
}
}
}
// Check for connection validity
private SqlInternalConnection GetValidConnection() {
SqlInternalConnection connection = _connection;
if (null == connection) {
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 (null != connection) {
connection.DoomThisConnection();
}
if (connection != _connection && null != _connection) {
_connection.DoomThisConnection();
}
throw ADP.InternalError(ADP.InternalErrorCode.UnpooledObjectHasWrongOwner); //
}
}
// Get the server-side Global Transaction Id from the PromotedDTCToken
// Skip first 4 bytes since they contain the version
private Guid GetGlobalTxnIdentifierFromToken() {
byte[] txnGuid = new byte[16];
Array.Copy(_connection.PromotedDTCToken, _globalTransactionsTokenVersionSizeInBytes /* Skip the version */, txnGuid, 0, txnGuid.Length);
return new Guid(txnGuid);
}
}
}
|