1 //------------------------------------------------------------------------------
2 // <copyright file="SqlDelegatedTransaction.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 // <owner current="true" primary="true">Microsoft</owner>
6 // <owner current="true" primary="false">Microsoft</owner>
7 //------------------------------------------------------------------------------
9 namespace System
.Data
.SqlClient
{
11 using System
.Data
.Common
;
12 using System
.Data
.SqlClient
;
13 using System
.Diagnostics
;
14 using System
.Reflection
;
15 using System
.Runtime
.CompilerServices
;
16 using System
.Runtime
.ConstrainedExecution
;
17 using System
.Threading
;
18 using SysTx
= System
.Transactions
;
20 sealed internal class SqlDelegatedTransaction
: SysTx
.IPromotableSinglePhaseNotification
{
21 private static int _objectTypeCount
;
22 private readonly int _objectID
= Interlocked
.Increment(ref _objectTypeCount
);
23 private const int _globalTransactionsTokenVersionSizeInBytes
= 4; // the size of the version in the PromotedDTCToken for Global Transactions
24 internal int ObjectID
{
30 // WARNING!!! Multithreaded object!
31 // Locking strategy: Any potentailly-multithreaded operation must first lock the associated connection, then
32 // validate this object's active state. Locked activities should ONLY include Sql-transaction state altering activities
33 // or notifications of same. Updates to the connection's association with the transaction or to the connection pool
34 // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy.
36 private SqlInternalConnection _connection
; // the internal connection that is the root of the transaction
37 private IsolationLevel _isolationLevel
; // the IsolationLevel of the transaction we delegated to the server
38 private SqlInternalTransaction _internalTransaction
; // the SQL Server transaction we're delegating to
40 private SysTx
.Transaction _atomicTransaction
;
42 private bool _active
; // Is the transaction active?
44 internal SqlDelegatedTransaction(SqlInternalConnection connection
, SysTx
.Transaction tx
) {
45 Debug
.Assert(null != connection
, "null connection?");
46 _connection
= connection
;
47 _atomicTransaction
= tx
;
49 SysTx
.IsolationLevel systxIsolationLevel
= tx
.IsolationLevel
;
51 // We need to map the System.Transactions IsolationLevel to the one
52 // that System.Data uses and communicates to SqlServer. We could
53 // arguably do that in Initialize when the transaction is delegated,
54 // however it is better to do this before we actually begin the process
55 // of delegation, in case System.Transactions adds another isolation
56 // level we don't know about -- we can throw the exception at a better
58 switch (systxIsolationLevel
) {
59 case SysTx
.IsolationLevel
.ReadCommitted
: _isolationLevel
= IsolationLevel
.ReadCommitted
; break;
60 case SysTx
.IsolationLevel
.ReadUncommitted
: _isolationLevel
= IsolationLevel
.ReadUncommitted
; break;
61 case SysTx
.IsolationLevel
.RepeatableRead
: _isolationLevel
= IsolationLevel
.RepeatableRead
; break;
62 case SysTx
.IsolationLevel
.Serializable
: _isolationLevel
= IsolationLevel
.Serializable
; break;
63 case SysTx
.IsolationLevel
.Snapshot
: _isolationLevel
= IsolationLevel
.Snapshot
; break;
65 throw SQL
.UnknownSysTxIsolationLevel(systxIsolationLevel
);
69 internal SysTx
.Transaction Transaction
71 get { return _atomicTransaction; }
74 public void Initialize() {
75 // if we get here, then we know for certain that we're the delegated
77 SqlInternalConnection connection
= _connection
;
78 SqlConnection usersConnection
= connection
.Connection
;
80 Bid
.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, delegating transaction.\n", ObjectID
, connection
.ObjectID
);
82 RuntimeHelpers
.PrepareConstrainedRegions();
85 TdsParser
.ReliabilitySection tdsReliabilitySection
= new TdsParser
.ReliabilitySection();
87 RuntimeHelpers
.PrepareConstrainedRegions();
89 tdsReliabilitySection
.Start();
93 if (connection
.IsEnlistedInTransaction
) { // defect first
94 Bid
.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, was enlisted, now defecting.\n", ObjectID
, connection
.ObjectID
);
95 connection
.EnlistNull();
98 _internalTransaction
= new SqlInternalTransaction(connection
, TransactionType
.Delegated
, null);
100 connection
.ExecuteTransaction(SqlInternalConnection
.TransactionRequest
.Begin
, null, _isolationLevel
, _internalTransaction
, true);
102 // Handle case where ExecuteTran didn't produce a new transaction, but also didn't throw.
103 if (null == connection
.CurrentTransaction
)
105 connection
.DoomThisConnection();
106 throw ADP
.InternalError(ADP
.InternalErrorCode
.UnknownTransactionFailure
);
113 tdsReliabilitySection
.Stop();
117 catch (System
.OutOfMemoryException e
) {
118 usersConnection
.Abort(e
);
121 catch (System
.StackOverflowException e
) {
122 usersConnection
.Abort(e
);
125 catch (System
.Threading
.ThreadAbortException e
) {
126 usersConnection
.Abort(e
);
131 internal bool IsActive
{
137 public Byte
[] Promote() {
138 // Operations that might be affected by multi-threaded use MUST be done inside the lock.
139 // Don't read values off of the connection outside the lock unless it doesn't really matter
140 // from an operational standpoint (i.e. logging connection's ObjectID should be fine,
141 // but the PromotedDTCToken can change over calls. so that must be protected).
142 SqlInternalConnection connection
= GetValidConnection();
144 Exception promoteException
;
145 byte[] returnValue
= null;
146 SqlConnection usersConnection
= connection
.Connection
;
148 Bid
.Trace("<sc.SqlDelegatedTransaction.Promote|RES|CPOOL> %d#, Connection %d#, promoting transaction.\n", ObjectID
, connection
.ObjectID
);
150 RuntimeHelpers
.PrepareConstrainedRegions();
153 TdsParser
.ReliabilitySection tdsReliabilitySection
= new TdsParser
.ReliabilitySection();
155 RuntimeHelpers
.PrepareConstrainedRegions();
157 tdsReliabilitySection
.Start();
163 // Now that we've acquired the lock, make sure we still have valid state for this operation.
164 ValidateActiveOnConnection(connection
);
166 connection
.ExecuteTransaction(SqlInternalConnection
.TransactionRequest
.Promote
, null, IsolationLevel
.Unspecified
, _internalTransaction
, true);
167 returnValue
= _connection
.PromotedDTCToken
;
169 // For Global Transactions, we need to set the Transaction Id since we use a Non-MSDTC Promoter type.
170 if(_connection
.IsGlobalTransaction
) {
171 if (SysTxForGlobalTransactions
.SetDistributedTransactionIdentifier
== null) {
172 throw SQL
.UnsupportedSysTxForGlobalTransactions();
175 if(!_connection
.IsGlobalTransactionsEnabledForServer
) {
176 throw SQL
.GlobalTransactionsNotEnabled();
179 SysTxForGlobalTransactions
.SetDistributedTransactionIdentifier
.Invoke(_atomicTransaction
, new object[] { this, GetGlobalTxnIdentifierFromToken() }
);
182 promoteException
= null;
184 catch (SqlException e
) {
185 promoteException
= e
;
187 ADP
.TraceExceptionWithoutRethrow(e
);
189 // Doom the connection, to make sure that the transaction is
190 // eventually rolled back.
191 // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
192 connection
.DoomThisConnection();
194 catch (InvalidOperationException e
)
196 promoteException
= e
;
197 ADP
.TraceExceptionWithoutRethrow(e
);
198 connection
.DoomThisConnection();
204 tdsReliabilitySection
.Stop();
208 catch (System
.OutOfMemoryException e
) {
209 usersConnection
.Abort(e
);
212 catch (System
.StackOverflowException e
) {
213 usersConnection
.Abort(e
);
216 catch (System
.Threading
.ThreadAbortException e
) {
217 usersConnection
.Abort(e
);
221 if (promoteException
!= null) {
222 throw SQL
.PromotionFailed(promoteException
);
228 // Called by transaction to initiate abort sequence
229 public void Rollback(SysTx
.SinglePhaseEnlistment enlistment
) {
230 Debug
.Assert(null != enlistment
, "null enlistment?");
232 SqlInternalConnection connection
= GetValidConnection();
233 SqlConnection usersConnection
= connection
.Connection
;
235 Bid
.Trace("<sc.SqlDelegatedTransaction.Rollback|RES|CPOOL> %d#, Connection %d#, aborting transaction.\n", ObjectID
, connection
.ObjectID
);
237 RuntimeHelpers
.PrepareConstrainedRegions();
240 TdsParser
.ReliabilitySection tdsReliabilitySection
= new TdsParser
.ReliabilitySection();
242 RuntimeHelpers
.PrepareConstrainedRegions();
244 tdsReliabilitySection
.Start();
250 // Now that we've acquired the lock, make sure we still have valid state for this operation.
251 ValidateActiveOnConnection(connection
);
252 _active
= false; // set to inactive first, doesn't matter how the execute completes, this transaction is done.
253 _connection
= null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event
255 // If we haven't already rolled back (or aborted) then tell the SQL Server to roll back
256 if (!_internalTransaction
.IsAborted
) {
257 connection
.ExecuteTransaction(SqlInternalConnection
.TransactionRequest
.Rollback
, null, IsolationLevel
.Unspecified
, _internalTransaction
, true);
260 catch (SqlException e
) {
261 ADP
.TraceExceptionWithoutRethrow(e
);
263 // Doom the connection, to make sure that the transaction is
264 // eventually rolled back.
265 // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
266 connection
.DoomThisConnection();
268 // Unlike SinglePhaseCommit, a rollback is a rollback, regardless
269 // of how it happens, so SysTx won't throw an exception, and we
270 // don't want to throw an exception either, because SysTx isn't
271 // handling it and it may create a fail fast scenario. In the end,
272 // there is no way for us to communicate to the consumer that this
273 // failed for more serious reasons than usual.
275 // This is a bit like "should you throw if Close fails", however,
276 // it only matters when you really need to know. In that case,
277 // we have the tracing that we're doing to fallback on for the
280 catch (InvalidOperationException e
) {
281 ADP
.TraceExceptionWithoutRethrow(e
);
282 connection
.DoomThisConnection();
286 // it doesn't matter whether the rollback succeeded or not, we presume
287 // that the transaction is aborted, because it will be eventually.
288 connection
.CleanupConnectionOnTransactionCompletion(_atomicTransaction
);
289 enlistment
.Aborted();
293 tdsReliabilitySection
.Stop();
297 catch (System
.OutOfMemoryException e
) {
298 usersConnection
.Abort(e
);
301 catch (System
.StackOverflowException e
) {
302 usersConnection
.Abort(e
);
305 catch (System
.Threading
.ThreadAbortException e
) {
306 usersConnection
.Abort(e
);
311 // Called by the transaction to initiate commit sequence
312 public void SinglePhaseCommit(SysTx
.SinglePhaseEnlistment enlistment
) {
313 Debug
.Assert(null != enlistment
, "null enlistment?");
315 SqlInternalConnection connection
= GetValidConnection();
316 SqlConnection usersConnection
= connection
.Connection
;
318 Bid
.Trace("<sc.SqlDelegatedTransaction.SinglePhaseCommit|RES|CPOOL> %d#, Connection %d#, committing transaction.\n", ObjectID
, connection
.ObjectID
);
320 RuntimeHelpers
.PrepareConstrainedRegions();
323 TdsParser
.ReliabilitySection tdsReliabilitySection
= new TdsParser
.ReliabilitySection();
325 RuntimeHelpers
.PrepareConstrainedRegions();
327 tdsReliabilitySection
.Start();
331 // If the connection is dooomed, we can be certain that the
332 // transaction will eventually be rolled back, and we shouldn't
333 // attempt to commit it.
334 if (connection
.IsConnectionDoomed
) {
336 _active
= false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
340 enlistment
.Aborted(SQL
.ConnectionDoomed());
343 Exception commitException
;
346 // Now that we've acquired the lock, make sure we still have valid state for this operation.
347 ValidateActiveOnConnection(connection
);
349 _active
= false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
350 _connection
= null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event
352 connection
.ExecuteTransaction(SqlInternalConnection
.TransactionRequest
.Commit
, null, IsolationLevel
.Unspecified
, _internalTransaction
, true);
353 commitException
= null;
355 catch (SqlException e
) {
358 ADP
.TraceExceptionWithoutRethrow(e
);
360 // Doom the connection, to make sure that the transaction is
361 // eventually rolled back.
362 // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
363 connection
.DoomThisConnection();
365 catch (InvalidOperationException e
) {
367 ADP
.TraceExceptionWithoutRethrow(e
);
368 connection
.DoomThisConnection();
371 if (commitException
!= null) {
372 // connection.ExecuteTransaction failed with exception
373 if (_internalTransaction
.IsCommitted
) {
374 // Even though we got an exception, the transaction
375 // was committed by the server.
376 enlistment
.Committed();
378 else if (_internalTransaction
.IsAborted
) {
379 // The transaction was aborted, report that to
381 enlistment
.Aborted(commitException
);
384 // The transaction is still active, we cannot
385 // know the state of the transaction.
386 enlistment
.InDoubt(commitException
);
389 // We eat the exception. This is called on the SysTx
390 // thread, not the applications thread. If we don't
391 // eat the exception an UnhandledException will occur,
392 // causing the process to FailFast.
395 connection
.CleanupConnectionOnTransactionCompletion(_atomicTransaction
);
396 if (commitException
== null) {
397 // connection.ExecuteTransaction succeeded
398 enlistment
.Committed();
404 tdsReliabilitySection
.Stop();
408 catch (System
.OutOfMemoryException e
) {
409 usersConnection
.Abort(e
);
412 catch (System
.StackOverflowException e
) {
413 usersConnection
.Abort(e
);
416 catch (System
.Threading
.ThreadAbortException e
) {
417 usersConnection
.Abort(e
);
422 // Event notification that transaction ended. This comes from the subscription to the Transaction's
423 // ended event via the internal connection. If it occurs without a prior Rollback or SinglePhaseCommit call,
424 // it indicates the transaction was ended externally (generally that one the the DTC participants aborted
426 internal void TransactionEnded(SysTx
.Transaction transaction
) {
427 SqlInternalConnection connection
= _connection
;
429 if (connection
!= null) {
430 Bid
.Trace("<sc.SqlDelegatedTransaction.TransactionEnded|RES|CPOOL> %d#, Connection %d#, transaction completed externally.\n", ObjectID
, connection
.ObjectID
);
433 if (_atomicTransaction
.Equals(transaction
)) {
434 // No need to validate active on connection, this operation can be called on completed transactions
442 // Check for connection validity
443 private SqlInternalConnection
GetValidConnection() {
444 SqlInternalConnection connection
= _connection
;
445 if (null == connection
) {
446 throw ADP
.ObjectDisposed(this);
452 // Dooms connection and throws and error if not a valid, active, delegated transaction for the given
453 // connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return
454 // may not be trusted.
455 private void ValidateActiveOnConnection(SqlInternalConnection connection
) {
456 bool valid
= _active
&& (connection
== _connection
) && (connection
.DelegatedTransaction
== this);
459 // Invalid indicates something BAAAD happened (Commit after TransactionEnded, for instance)
460 // Doom anything remotely involved.
461 if (null != connection
) {
462 connection
.DoomThisConnection();
464 if (connection
!= _connection
&& null != _connection
) {
465 _connection
.DoomThisConnection();
468 throw ADP
.InternalError(ADP
.InternalErrorCode
.UnpooledObjectHasWrongOwner
); //
472 // Get the server-side Global Transaction Id from the PromotedDTCToken
473 // Skip first 4 bytes since they contain the version
474 private Guid
GetGlobalTxnIdentifierFromToken() {
475 byte[] txnGuid
= new byte[16];
476 Array
.Copy(_connection
.PromotedDTCToken
, _globalTransactionsTokenVersionSizeInBytes
/* Skip the version */, txnGuid
, 0, txnGuid
.Length
);
477 return new Guid(txnGuid
);