Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / System.Data / System / Data / SqlClient / sqlinternaltransaction.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="SqlInternalTransaction.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 //------------------------------------------------------------------------------
8
9 namespace System.Data.SqlClient {
10
11     using System.Data;
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;
18
19     internal enum TransactionState {
20         Pending = 0,
21         Active = 1,
22         Aborted = 2,
23         Committed = 3,
24         Unknown = 4,
25     }
26     
27     internal enum TransactionType {
28         LocalFromTSQL   = 1,
29         LocalFromAPI    = 2,
30         Delegated       = 3,
31         Distributed     = 4,
32         Context         = 5,     // only valid in proc.
33     };
34
35     sealed internal class SqlInternalTransaction {
36
37         internal const long NullTransactionId = 0;
38
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.
46
47         private  static   int _objectTypeCount; // Bid counter
48         internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount);
49
50         internal bool RestoreBrokenConnection { get; set; }
51         internal bool ConnectionHasBeenRestored { get; set; }
52
53         internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) {
54         }
55         
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",
58                         ObjectID,
59                         innerConnection.ObjectID,
60                         (null != outerTransaction) ? outerTransaction.ObjectID : -1,
61                         (int)type);
62
63             _innerConnection = innerConnection;
64             _transactionType = type;
65
66             if (null != outerTransaction) {
67                 _parent = new WeakReference(outerTransaction);
68             }
69
70             _transactionId = transactionId;
71             RestoreBrokenConnection = false;
72             ConnectionHasBeenRestored = false;
73         }
74
75         internal bool HasParentTransaction {
76             get {
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) );
82                 return result;
83             }
84         }
85
86         internal bool IsAborted {
87             get {
88                 return (TransactionState.Aborted == _transactionState);
89             }
90         }
91
92         internal bool IsActive {
93             get {
94                 return (TransactionState.Active == _transactionState);
95             }
96         }
97
98         internal bool IsCommitted {
99             get {
100                 return (TransactionState.Committed == _transactionState);
101             }
102         }
103
104         internal bool IsCompleted {
105             get {
106                 return (TransactionState.Aborted   == _transactionState
107                      || TransactionState.Committed == _transactionState 
108                      || TransactionState.Unknown   == _transactionState);
109             }
110         }
111
112         internal bool IsContext {
113             get {
114                 bool result = (TransactionType.Context == _transactionType);
115                 return result;
116             }
117         }
118
119         internal bool IsDelegated {
120             get {
121                 bool result = (TransactionType.Delegated == _transactionType);
122                 return result;
123             }
124         }
125
126         internal bool IsDistributed {
127             get {
128                 bool result = (TransactionType.Distributed == _transactionType);
129                 return result;
130             }
131         }
132
133         internal bool IsLocal {
134             get {
135                 bool result = (TransactionType.LocalFromTSQL == _transactionType
136                             || TransactionType.LocalFromAPI  == _transactionType
137                             || TransactionType.Context       == _transactionType);
138                 return result;
139             }
140         }
141
142         internal bool IsOrphaned {
143             get {
144                 // An internal transaction is orphaned when its parent has been
145                 // reclaimed by GC.
146                 bool result;
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");
152                     result = false;
153                 }
154                 else if (null == _parent.Target) {
155                     // We have an parent, but parent was GC'ed.
156                     result = true;
157                 }
158                 else {
159                     // We have an parent, and parent is alive.
160                     result = false;
161                 }
162
163                 return result;
164             }
165         }
166
167         internal bool IsZombied {
168             get {
169                 return (null == _innerConnection);
170             }
171         }
172
173         internal int ObjectID {
174             get {
175                 return _objectID;
176             }
177         }
178
179         internal int OpenResultsCount {
180             get {
181                 return _openResultCount;
182             }
183         }
184
185         internal SqlTransaction Parent {
186             get {
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;
192                 }
193                 return result;
194             }
195         }
196
197         internal long TransactionId {
198             get {
199                 return _transactionId;
200             }
201             set {
202                 Debug.Assert(NullTransactionId == _transactionId, "setting transaction cookie while one is active?");
203                 _transactionId = value;
204             }
205         }
206
207         internal void Activate () {
208             _transactionState = TransactionState.Active;
209         }
210
211         private void CheckTransactionLevelAndZombie() {
212             try {
213                 if (!IsZombied && GetServerTransactionLevel() == 0) {
214                     // If not zombied, not closed, and not in transaction, zombie.
215                     Zombie();
216                 }
217             }
218             catch (Exception e) {
219                 // 
220                 if (!ADP.IsCatchableExceptionType(e)) {
221                     throw;
222                 }
223
224                 ADP.TraceExceptionWithoutRethrow(e);
225                 Zombie(); // If exception caught when trying to check level, zombie.
226             }
227         }
228
229         internal void CloseFromConnection() {
230             SqlInternalConnection innerConnection = _innerConnection;
231
232             Debug.Assert (innerConnection != null,"How can we be here if the connection is null?");
233
234             Bid.PoolerTrace("<sc.SqlInteralTransaction.CloseFromConnection|RES|CPOOL> %d#, Closing\n", ObjectID);
235             bool processFinallyBlock = true;
236             try {
237                 innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.IfRollback, null, IsolationLevel.Unspecified, null, false);
238             }
239             catch (Exception e) {
240                 processFinallyBlock = ADP.IsCatchableExceptionType(e);
241                 throw;
242             }
243             finally {
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
249                     // them ourselves.
250                     Zombie();
251                 }
252             }
253         }
254
255         internal void Commit() {
256             IntPtr hscp;
257             Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Commit|API> %d#", ObjectID);
258
259             if (_innerConnection.IsLockedForBulkCopy) {
260                 throw SQL.ConnectionLockedForBcpEvent();
261             }
262
263             _innerConnection.ValidateConnectionForExecute(null);
264
265             try {
266                 // If this transaction has been completed, throw exception since it is unusable.
267                 try {
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);
271
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
274                     // or later.
275                     if (!IsZombied && !_innerConnection.IsYukonOrNewer) {
276                         // Since nested transactions are no longer allowed, set flag to false.
277                         // This transaction has been completed.
278                         Zombie();
279                     }
280                     else {
281                         ZombieParent();
282                     }
283                 }
284                 catch (Exception e) {
285                     // 
286                     if (ADP.IsCatchableExceptionType(e)) {
287                         CheckTransactionLevelAndZombie();
288                     }
289
290                     throw;
291                 }
292             }
293             finally {
294                 Bid.ScopeLeave(ref hscp);
295             }
296         }
297
298         internal void Completed(TransactionState transactionState) {
299             Debug.Assert (TransactionState.Active < transactionState, "invalid transaction completion state?");
300             _transactionState = transactionState;
301             Zombie();
302         }
303
304         internal Int32 DecrementAndObtainOpenResultCount() {
305             Int32 openResultCount = Interlocked.Decrement(ref _openResultCount);
306             if (openResultCount < 0) {
307                 throw SQL.OpenResultCountExceeded();
308             }
309             return openResultCount;
310         }
311
312         internal void Dispose() {
313             this.Dispose(true);
314             System.GC.SuppressFinalize(this);
315         }
316
317         private /*protected override*/ void Dispose(bool disposing) {
318             Bid.PoolerTrace("<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing\n", ObjectID);
319             if (disposing) {
320                 if (null != _innerConnection) {
321                     // implicitly rollback if transaction still valid
322                     _disposing = true;
323                     this.Rollback();
324                 }
325             }
326         }
327
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.
332
333             // 
334
335             using (SqlCommand transactionLevelCommand = new SqlCommand("set @out = @@trancount", (SqlConnection)(_innerConnection.Owner))) {
336                 transactionLevelCommand.Transaction = Parent;
337
338                 SqlParameter parameter = new SqlParameter("@out", SqlDbType.Int);
339                 parameter.Direction    = ParameterDirection.Output;
340                 transactionLevelCommand.Parameters.Add(parameter);
341
342                 // 
343
344                 transactionLevelCommand.RunExecuteReader(0, RunBehavior.UntilDone, false /* returnDataStream */, ADP.GetServerTransactionLevel);
345
346                 return (int)parameter.Value;
347             }
348         }
349
350         internal Int32 IncrementAndObtainOpenResultCount() {
351             Int32 openResultCount = Interlocked.Increment(ref _openResultCount);
352
353             if (openResultCount < 0) {
354                 throw SQL.OpenResultCountExceeded();
355             }
356             return openResultCount;
357         }
358
359         internal void InitParent(SqlTransaction transaction) {
360             Debug.Assert(_parent == null, "Why do we have a parent on InitParent?");
361             _parent = new WeakReference(transaction);
362         }
363
364         internal void Rollback() {
365             IntPtr hscp;
366             Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Rollback|API> %d#", ObjectID);
367
368             if (_innerConnection.IsLockedForBulkCopy) {
369                 throw SQL.ConnectionLockedForBcpEvent();
370             }
371
372             _innerConnection.ValidateConnectionForExecute(null);
373
374             try {
375                 try {
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);
379
380                     // Since Rollback will rollback to outermost begin, no need to check
381                     // server transaction level.  This transaction has been completed.
382                     Zombie();
383                 }
384                 catch (Exception e) {
385                     // 
386                     if (ADP.IsCatchableExceptionType(e)) {
387                         CheckTransactionLevelAndZombie();
388
389                         if (!_disposing) {
390                             throw;
391                         }
392                     }
393                     else {
394                         throw;
395                     }
396                 }
397             }
398             finally {
399                 Bid.ScopeLeave(ref hscp);
400             }
401         }
402
403         internal void Rollback(string transactionName) {
404             IntPtr hscp;
405             Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Rollback|API> %d#, transactionName='%ls'", ObjectID, transactionName);
406
407             if (_innerConnection.IsLockedForBulkCopy) {
408                 throw SQL.ConnectionLockedForBcpEvent();
409             }
410
411             _innerConnection.ValidateConnectionForExecute(null);
412
413             try {
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
418                 // save point name.
419                 if (ADP.IsEmpty(transactionName))
420                     throw SQL.NullEmptyTransactionName();
421
422                 try {
423                     _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, transactionName, IsolationLevel.Unspecified, null, false);
424
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();
430                     }
431                 }
432                 catch (Exception e) {
433                     // 
434                     if (ADP.IsCatchableExceptionType(e)) {
435                         CheckTransactionLevelAndZombie();
436                     }
437                     throw;
438                 }
439             }
440             finally {
441                 Bid.ScopeLeave(ref hscp);
442             }
443         }
444
445         internal void Save(string savePointName) {
446             IntPtr hscp;
447             Bid.ScopeEnter(out hscp, "<sc.SqlInternalTransaction.Save|API> %d#, savePointName='%ls'", ObjectID, savePointName);
448
449             _innerConnection.ValidateConnectionForExecute(null);
450
451             try {
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();
460
461                 try {
462                     _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Save, savePointName, IsolationLevel.Unspecified, null, false);
463                 }
464                 catch (Exception e) {
465                     // 
466                     if (ADP.IsCatchableExceptionType(e)) {
467                         CheckTransactionLevelAndZombie();
468                     }
469
470                     throw;
471                 }
472             }
473             finally {
474                 Bid.ScopeLeave(ref hscp);
475             }
476         }
477
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.
482
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.
489
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.
498
499             ZombieParent();
500
501             SqlInternalConnection innerConnection = _innerConnection;
502             _innerConnection = null;
503
504             if (null != innerConnection) {
505                 innerConnection.DisconnectTransaction(this);
506             }
507         }
508
509         private void ZombieParent() {
510             if (null != _parent) {
511                 SqlTransaction parent = (SqlTransaction) _parent.Target;
512                 if (null != parent) {
513                     parent.Zombie();
514                 }
515                 _parent = null;
516             }
517         }
518
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);
522         }
523     }
524 }