Updates referencesource to .NET 4.7
[mono-project.git] / mcs / class / referencesource / System.Data / System / Data / SqlClient / SqlDelegatedTransaction.cs
blob63bae158d8a96a346f198f8a701542837541dd4e
1 //------------------------------------------------------------------------------
2 // <copyright file="SqlDelegatedTransaction.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
4 // </copyright>
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 {
25 get {
26 return _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;
48 _active = false;
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
57 // place.
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;
64 default:
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
76 // transaction.
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();
83 try {
84 #if DEBUG
85 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
87 RuntimeHelpers.PrepareConstrainedRegions();
88 try {
89 tdsReliabilitySection.Start();
90 #else
92 #endif //DEBUG
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);
109 _active = true;
111 #if DEBUG
112 finally {
113 tdsReliabilitySection.Stop();
115 #endif //DEBUG
117 catch (System.OutOfMemoryException e) {
118 usersConnection.Abort(e);
119 throw;
121 catch (System.StackOverflowException e) {
122 usersConnection.Abort(e);
123 throw;
125 catch (System.Threading.ThreadAbortException e) {
126 usersConnection.Abort(e);
127 throw;
131 internal bool IsActive {
132 get {
133 return _active;
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();
151 try {
152 #if DEBUG
153 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
155 RuntimeHelpers.PrepareConstrainedRegions();
156 try {
157 tdsReliabilitySection.Start();
158 #else
160 #endif //DEBUG
161 lock (connection) {
162 try {
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();
202 #if DEBUG
203 finally {
204 tdsReliabilitySection.Stop();
206 #endif //DEBUG
208 catch (System.OutOfMemoryException e) {
209 usersConnection.Abort(e);
210 throw;
212 catch (System.StackOverflowException e) {
213 usersConnection.Abort(e);
214 throw;
216 catch (System.Threading.ThreadAbortException e) {
217 usersConnection.Abort(e);
218 throw;
221 if (promoteException != null) {
222 throw SQL.PromotionFailed(promoteException);
225 return returnValue;
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();
238 try {
239 #if DEBUG
240 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
242 RuntimeHelpers.PrepareConstrainedRegions();
243 try {
244 tdsReliabilitySection.Start();
245 #else
247 #endif //DEBUG
248 lock (connection) {
249 try {
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
278 // investigation.
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();
291 #if DEBUG
292 finally {
293 tdsReliabilitySection.Stop();
295 #endif //DEBUG
297 catch (System.OutOfMemoryException e) {
298 usersConnection.Abort(e);
299 throw;
301 catch (System.StackOverflowException e) {
302 usersConnection.Abort(e);
303 throw;
305 catch (System.Threading.ThreadAbortException e) {
306 usersConnection.Abort(e);
307 throw;
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();
321 try {
322 #if DEBUG
323 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
325 RuntimeHelpers.PrepareConstrainedRegions();
326 try {
327 tdsReliabilitySection.Start();
328 #else
330 #endif //DEBUG
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) {
335 lock (connection) {
336 _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
337 _connection = null;
340 enlistment.Aborted(SQL.ConnectionDoomed());
342 else {
343 Exception commitException;
344 lock (connection) {
345 try {
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) {
356 commitException = 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) {
366 commitException = 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
380 // SysTx.
381 enlistment.Aborted(commitException);
383 else {
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();
402 #if DEBUG
403 finally {
404 tdsReliabilitySection.Stop();
406 #endif //DEBUG
408 catch (System.OutOfMemoryException e) {
409 usersConnection.Abort(e);
410 throw;
412 catch (System.StackOverflowException e) {
413 usersConnection.Abort(e);
414 throw;
416 catch (System.Threading.ThreadAbortException e) {
417 usersConnection.Abort(e);
418 throw;
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
425 // the transaction).
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);
432 lock (connection) {
433 if (_atomicTransaction.Equals(transaction)) {
434 // No need to validate active on connection, this operation can be called on completed transactions
435 _active = false;
436 _connection = null;
442 // Check for connection validity
443 private SqlInternalConnection GetValidConnection() {
444 SqlInternalConnection connection = _connection;
445 if (null == connection) {
446 throw ADP.ObjectDisposed(this);
449 return connection;
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);
458 if (!valid) {
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);