1 //------------------------------------------------------------------------------
2 // <copyright file="SqlInternalTransaction.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 {
12 using System.Data.Common;
13 using System.Data.ProviderBase;
14 using System.Data.Sql;
15 using System.Data.SqlTypes;
16 using System.Diagnostics;
17 using System.Threading;
19 internal enum TransactionState {
27 internal enum TransactionType {
32 Context = 5, // only valid in proc.
35 sealed internal class SqlInternalTransaction {
37 internal const long NullTransactionId = 0;
39 private TransactionState _transactionState;
40 private TransactionType _transactionType;
41 private long _transactionId; // passed in the MARS headers
42 private int _openResultCount; // passed in the MARS headers
43 private SqlInternalConnection _innerConnection;
44 private bool _disposing; // used to prevent us from throwing exceptions while we're disposing
45 private WeakReference _parent; // weak ref to the outer transaction object; needs to be weak to allow GC to occur.
47 private static int _objectTypeCount; // Bid counter
48 internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount);
50 internal bool RestoreBrokenConnection { get; set; }
51 internal bool ConnectionHasBeenRestored { get; set; }
53 internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) {
56 internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) {
57 Bid.PoolerTrace("<sc.SqlInternalTransaction.ctor|RES|CPOOL> %d#, Created for connection %d#, outer transaction %d#, Type %d\n",
59 innerConnection.ObjectID,
60 (null != outerTransaction) ? outerTransaction.ObjectID : -1,
63 _innerConnection = innerConnection;
64 _transactionType = type;
66 if (null != outerTransaction) {
67 _parent = new WeakReference(outerTransaction);
70 _transactionId = transactionId;
71 RestoreBrokenConnection = false;
72 ConnectionHasBeenRestored = false;
75 internal bool HasParentTransaction {
77 // Return true if we are an API started local transaction, or if we were a TSQL
78 // started local transaction and were then wrapped with a parent transaction as
79 // a result of a later API begin transaction.
80 bool result = ( (TransactionType.LocalFromAPI == _transactionType) ||
81 (TransactionType.LocalFromTSQL == _transactionType && _parent != null) );
86 internal bool IsAborted {
88 return (TransactionState.Aborted == _transactionState);
92 internal bool IsActive {
94 return (TransactionState.Active == _transactionState);
98 internal bool IsCommitted {
100 return (TransactionState.Committed == _transactionState);
104 internal bool IsCompleted {
106 return (TransactionState.Aborted == _transactionState
107 || TransactionState.Committed == _transactionState
108 || TransactionState.Unknown == _transactionState);
112 internal bool IsContext {
114 bool result = (TransactionType.Context == _transactionType);
119 internal bool IsDelegated {
121 bool result = (TransactionType.Delegated == _transactionType);
126 internal bool IsDistributed {
128 bool result = (TransactionType.Distributed == _transactionType);
133 internal bool IsLocal {
135 bool result = (TransactionType.LocalFromTSQL == _transactionType
136 || TransactionType.LocalFromAPI == _transactionType
137 || TransactionType.Context == _transactionType);
142 internal bool IsOrphaned {
144 // An internal transaction is orphaned when its parent has been
147 if (null == _parent) {
148 // No parent, so we better be LocalFromTSQL. Should we even return in this case -
149 // since it could be argued this is invalid?
150 Debug.Assert(false, "Why are we calling IsOrphaned with no parent?");
151 Debug.Assert(_transactionType == TransactionType.LocalFromTSQL, "invalid state");
154 else if (null == _parent.Target) {
155 // We have an parent, but parent was GC'ed.
159 // We have an parent, and parent is alive.
167 internal bool IsZombied {
169 return (null == _innerConnection);
173 internal int ObjectID {
179 internal int OpenResultsCount {
181 return _openResultCount;
185 internal SqlTransaction Parent {
187 SqlTransaction result = null;
188 // Should we protect against this, since this probably is an invalid state?
189 Debug.Assert(null != _parent, "Why are we calling Parent with no parent?");
190 if (null != _parent) {
191 result = (SqlTransaction)_parent.Target;
197 internal long TransactionId {
199 return _transactionId;
202 Debug.Assert(NullTransactionId == _transactionId, "setting transaction cookie while one is active?");
203 _transactionId = value;
207 internal void Activate () {
208 _transactionState = TransactionState.Active;
211 private void CheckTransactionLevelAndZombie() {
213 if (!IsZombied && GetServerTransactionLevel() == 0) {
214 // If not zombied, not closed, and not in transaction, zombie.
218 catch (Exception e) {
220 if (!ADP.IsCatchableExceptionType(e)) {
224 ADP.TraceExceptionWithoutRethrow(e);
225 Zombie(); // If exception caught when trying to check level, zombie.
229 internal void CloseFromConnection() {
230 SqlInternalConnection innerConnection = _innerConnection;
232 Debug.Assert (innerConnection != null,"How can we be here if the connection is null?");
234 Bid.PoolerTrace("<sc.SqlInteralTransaction.CloseFromConnection|RES|CPOOL> %d#, Closing\n", ObjectID);
235 bool processFinallyBlock = true;
237 innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.IfRollback, null, IsolationLevel.Unspecified, null, false);
239 catch (Exception e) {
240 processFinallyBlock = ADP.IsCatchableExceptionType(e);
244 TdsParser.ReliabilitySection.Assert("unreliable call to CloseFromConnection"); // you need to setup for a thread abort somewhere before you call this method
245 if (processFinallyBlock) {
246 // Always ensure we're zombied; Yukon will send an EnvChange that
247 // will cause the zombie, but only if we actually go to the wire;
248 // Sphinx and Shiloh won't send the env change, so we have to handle
255 internal void Commit() {
257 Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Commit|API> %d#", ObjectID);
259 if (_innerConnection.IsLockedForBulkCopy) {
260 throw SQL.ConnectionLockedForBcpEvent();
263 _innerConnection.ValidateConnectionForExecute(null);
266 // If this transaction has been completed, throw exception since it is unusable.
268 // COMMIT ignores transaction names, and so there is no reason to pass it anything. COMMIT
269 // simply commits the transaction from the most recent BEGIN, nested or otherwise.
270 _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, IsolationLevel.Unspecified, null, false);
272 // SQL BU DT 291159 - perform full Zombie on pre-Yukon, but do not actually
273 // complete internal transaction until informed by server in the case of Yukon
275 if (!IsZombied && !_innerConnection.IsYukonOrNewer) {
276 // Since nested transactions are no longer allowed, set flag to false.
277 // This transaction has been completed.
284 catch (Exception e) {
286 if (ADP.IsCatchableExceptionType(e)) {
287 CheckTransactionLevelAndZombie();
294 Bid.ScopeLeave(ref hscp);
298 internal void Completed(TransactionState transactionState) {
299 Debug.Assert (TransactionState.Active < transactionState, "invalid transaction completion state?");
300 _transactionState = transactionState;
304 internal Int32 DecrementAndObtainOpenResultCount() {
305 Int32 openResultCount = Interlocked.Decrement(ref _openResultCount);
306 if (openResultCount < 0) {
307 throw SQL.OpenResultCountExceeded();
309 return openResultCount;
312 internal void Dispose() {
314 System.GC.SuppressFinalize(this);
317 private /*protected override*/ void Dispose(bool disposing) {
318 Bid.PoolerTrace("<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing\n", ObjectID);
320 if (null != _innerConnection) {
321 // implicitly rollback if transaction still valid
328 private int GetServerTransactionLevel() {
329 // This function is needed for those times when it is impossible to determine the server's
330 // transaction level, unless the user's arguments were parsed - which is something we don't want
331 // to do. An example when it is impossible to determine the level is after a rollback.
335 using (SqlCommand transactionLevelCommand = new SqlCommand("set @out = @@trancount", (SqlConnection)(_innerConnection.Owner))) {
336 transactionLevelCommand.Transaction = Parent;
338 SqlParameter parameter = new SqlParameter("@out", SqlDbType.Int);
339 parameter.Direction = ParameterDirection.Output;
340 transactionLevelCommand.Parameters.Add(parameter);
344 transactionLevelCommand.RunExecuteReader(0, RunBehavior.UntilDone, false /* returnDataStream */, ADP.GetServerTransactionLevel);
346 return (int)parameter.Value;
350 internal Int32 IncrementAndObtainOpenResultCount() {
351 Int32 openResultCount = Interlocked.Increment(ref _openResultCount);
353 if (openResultCount < 0) {
354 throw SQL.OpenResultCountExceeded();
356 return openResultCount;
359 internal void InitParent(SqlTransaction transaction) {
360 Debug.Assert(_parent == null, "Why do we have a parent on InitParent?");
361 _parent = new WeakReference(transaction);
364 internal void Rollback() {
366 Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Rollback|API> %d#", ObjectID);
368 if (_innerConnection.IsLockedForBulkCopy) {
369 throw SQL.ConnectionLockedForBcpEvent();
372 _innerConnection.ValidateConnectionForExecute(null);
376 // If no arg is given to ROLLBACK it will rollback to the outermost begin - rolling back
377 // all nested transactions as well as the outermost transaction.
378 _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.IfRollback, null, IsolationLevel.Unspecified, null, false);
380 // Since Rollback will rollback to outermost begin, no need to check
381 // server transaction level. This transaction has been completed.
384 catch (Exception e) {
386 if (ADP.IsCatchableExceptionType(e)) {
387 CheckTransactionLevelAndZombie();
399 Bid.ScopeLeave(ref hscp);
403 internal void Rollback(string transactionName) {
405 Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Rollback|API> %d#, transactionName='%ls'", ObjectID, transactionName);
407 if (_innerConnection.IsLockedForBulkCopy) {
408 throw SQL.ConnectionLockedForBcpEvent();
411 _innerConnection.ValidateConnectionForExecute(null);
414 // ROLLBACK takes either a save point name or a transaction name. It will rollback the
415 // transaction to either the save point with the save point name or begin with the
416 // transacttion name. NOTE: for simplicity it is possible to give all save point names
417 // the same name, and ROLLBACK will simply rollback to the most recent save point with the
419 if (ADP.IsEmpty(transactionName))
420 throw SQL.NullEmptyTransactionName();
423 _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, transactionName, IsolationLevel.Unspecified, null, false);
425 if (!IsZombied && !_innerConnection.IsYukonOrNewer) {
426 // Check if Zombied before making round-trip to server.
427 // Against Yukon we receive an envchange on the ExecuteTransaction above on the
428 // parser that calls back into SqlTransaction for the Zombie() call.
429 CheckTransactionLevelAndZombie();
432 catch (Exception e) {
434 if (ADP.IsCatchableExceptionType(e)) {
435 CheckTransactionLevelAndZombie();
441 Bid.ScopeLeave(ref hscp);
445 internal void Save(string savePointName) {
447 Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Save|API> %d#, savePointName='%ls'", ObjectID, savePointName);
449 _innerConnection.ValidateConnectionForExecute(null);
452 // ROLLBACK takes either a save point name or a transaction name. It will rollback the
453 // transaction to either the save point with the save point name or begin with the
454 // transacttion name. So, to rollback a nested transaction you must have a save point.
455 // SAVE TRANSACTION MUST HAVE AN ARGUMENT!!! Save Transaction without an arg throws an
456 // exception from the server. So, an overload for SaveTransaction without an arg doesn't make
457 // sense to have. Save Transaction does not affect the transaction level.
458 if (ADP.IsEmpty(savePointName))
459 throw SQL.NullEmptyTransactionName();
462 _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Save, savePointName, IsolationLevel.Unspecified, null, false);
464 catch (Exception e) {
466 if (ADP.IsCatchableExceptionType(e)) {
467 CheckTransactionLevelAndZombie();
474 Bid.ScopeLeave(ref hscp);
478 internal void Zombie() {
479 // Called by several places in the code to ensure that the outer
480 // transaction object has been zombied and the parser has broken
481 // it's reference to us.
483 // NOTE: we'll be called from the TdsParser when it gets appropriate
484 // ENVCHANGE events that indicate the transaction has completed, however
485 // we cannot rely upon those events occuring in the case of pre-Yukon
486 // servers (and when we don't go to the wire because the connection
487 // is broken) so we can also be called from the Commit/Rollback/Save
488 // methods to handle that case as well.
490 // There are two parts to a full zombie:
491 // 1) Zombie parent and disconnect outer transaction from internal transaction
492 // 2) Disconnect internal transaction from connection and parser
493 // Number 1 needs to be done whenever a SqlTransaction object is completed. Number
494 // 2 is only done when a transaction is actually completed. Since users can begin
495 // transactions both in and outside of the API, and since nested begins are not actual
496 // transactions we need to distinguish between #1 and #2. See SQL BU DT 291159
497 // for further details.
501 SqlInternalConnection innerConnection = _innerConnection;
502 _innerConnection = null;
504 if (null != innerConnection) {
505 innerConnection.DisconnectTransaction(this);
509 private void ZombieParent() {
510 if (null != _parent) {
511 SqlTransaction parent = (SqlTransaction) _parent.Target;
512 if (null != parent) {
519 internal string TraceString() {
520 return String.Format(/*IFormatProvider*/ null, "(ObjId={0}, tranId={1}, state={2}, type={3}, open={4}, disp={5}",
521 ObjectID, _transactionId, _transactionState, _transactionType, _openResultCount, _disposing);