386a6cea58ea04613b6901a0a4440c4ffa00b994
[mono.git] / mcs / class / referencesource / System.Data / System / Data / SqlClient / SqlInternalConnection.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="SqlInternalConnection.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;
12     using System.Collections.Generic;
13     using System.Data;
14     using System.Data.Common;
15     using System.Data.ProviderBase;
16     using System.Diagnostics;
17     using System.Globalization;
18     using System.Reflection;
19     using System.Runtime.CompilerServices;
20     using System.Runtime.ConstrainedExecution;
21     using System.Runtime.InteropServices;
22     using System.Security;
23     using System.Security.Permissions;
24     using System.Text;
25     using System.Threading;
26     using SysTx = System.Transactions;
27
28     abstract internal class SqlInternalConnection : DbConnectionInternal {
29         private readonly SqlConnectionString _connectionOptions;
30         private bool                         _isEnlistedInTransaction; // is the server-side connection enlisted? true while we're enlisted, reset only after we send a null...
31         private byte[]                       _promotedDTCToken;        // token returned by the server when we promote transaction
32         private byte[]                       _whereAbouts;             // cache the whereabouts (DTC Address) for exporting
33
34         // if connection is not open: null
35         // if connection is open: currently active database
36         internal string CurrentDatabase { get; set; }
37
38         // if connection is not open yet, CurrentDataSource is null
39         // if connection is open:
40         // * for regular connections, it is set to Data Source value from connection string
41         // * for connections with FailoverPartner, it is set to the FailoverPartner value from connection string if the connection was opened to it.
42         internal string CurrentDataSource { get; set; }
43
44         // the delegated (or promoted) transaction we're responsible for.
45         internal SqlDelegatedTransaction DelegatedTransaction { get; set; }
46
47         internal enum TransactionRequest {
48             Begin,
49             Promote,
50             Commit,
51             Rollback,
52             IfRollback,
53             Save
54         };
55
56         internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() {
57             Debug.Assert(null != connectionOptions, "null connectionOptions?");
58             _connectionOptions = connectionOptions;
59         }
60
61         internal SqlConnection Connection {
62             get {
63                 return (SqlConnection)Owner;
64             }
65         }
66
67         internal SqlConnectionString ConnectionOptions {
68             get {
69                 return _connectionOptions;
70             }
71         }
72
73         abstract internal SqlInternalTransaction CurrentTransaction {
74             get;
75         }
76
77         // SQLBU 415870
78         //  Get the internal transaction that should be hooked to a new outer transaction
79         //  during a BeginTransaction API call.  In some cases (i.e. connection is going to 
80         //  be reset), CurrentTransaction should not be hooked up this way.
81         virtual internal SqlInternalTransaction AvailableInternalTransaction {
82             get {
83                 return CurrentTransaction;
84             }
85         }
86
87         abstract internal SqlInternalTransaction PendingTransaction {
88             get;
89         }
90
91         override protected internal bool IsNonPoolableTransactionRoot {
92             get {
93                 return IsTransactionRoot;  // default behavior is that root transactions are NOT poolable.  Subclasses may override.
94             }
95         }
96
97         override internal bool IsTransactionRoot {
98             get {
99                 var delegatedTransaction = DelegatedTransaction;
100                 return ((null != delegatedTransaction) && (delegatedTransaction.IsActive));
101             }
102         }
103
104         internal bool HasLocalTransaction {
105             get {
106                 SqlInternalTransaction currentTransaction = CurrentTransaction;
107                 bool result = (null != currentTransaction && currentTransaction.IsLocal);
108                 return result;
109             }
110         }
111
112         internal bool HasLocalTransactionFromAPI {
113             get {
114                 SqlInternalTransaction currentTransaction = CurrentTransaction;
115                 bool result = (null != currentTransaction && currentTransaction.HasParentTransaction);
116                 return result;
117             }
118         }
119
120         internal bool IsEnlistedInTransaction {
121             get {
122                 return _isEnlistedInTransaction;
123             }
124         }
125
126         abstract internal bool IsLockedForBulkCopy {
127             get;            
128         }
129
130         abstract internal bool IsShiloh {
131             get;
132         }
133
134         abstract internal bool IsYukonOrNewer {
135             get;
136         }
137
138         abstract internal bool IsKatmaiOrNewer {
139             get;
140         }
141
142         internal byte[] PromotedDTCToken {
143             get {
144                 return _promotedDTCToken;
145             }
146             set {
147                 _promotedDTCToken = value;
148             }
149         }
150                 
151         override public DbTransaction BeginTransaction(IsolationLevel iso) {
152             return BeginSqlTransaction(iso, null, false);
153         }
154
155         virtual internal SqlTransaction BeginSqlTransaction(IsolationLevel iso, string transactionName, bool shouldReconnect) {
156             SqlStatistics statistics = null;
157             TdsParser bestEffortCleanupTarget = null;
158             RuntimeHelpers.PrepareConstrainedRegions();
159             try {
160 #if DEBUG
161                 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
162
163                 RuntimeHelpers.PrepareConstrainedRegions();
164                 try {
165                     tdsReliabilitySection.Start();
166 #else
167                 {
168 #endif //DEBUG
169                     bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
170                     statistics = SqlStatistics.StartTimer(Connection.Statistics);
171
172                     SqlConnection.ExecutePermission.Demand(); // MDAC 81476
173
174                     ValidateConnectionForExecute(null);
175
176                     if (HasLocalTransactionFromAPI)
177                         throw ADP.ParallelTransactionsNotSupported(Connection);
178
179                     if (iso == IsolationLevel.Unspecified) {
180                         iso = IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified.
181                     }
182
183                     SqlTransaction transaction = new SqlTransaction(this, Connection, iso, AvailableInternalTransaction);
184                     transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect;
185                     ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false);
186                     transaction.InternalTransaction.RestoreBrokenConnection = false;
187                     return transaction;
188                 }
189 #if DEBUG
190                 finally {
191                     tdsReliabilitySection.Stop();
192                 }
193 #endif //DEBUG
194             }
195             catch (System.OutOfMemoryException e) {
196                 Connection.Abort(e);
197                 throw;
198             }
199             catch (System.StackOverflowException e) {
200                 Connection.Abort(e);
201                 throw;
202             }
203             catch (System.Threading.ThreadAbortException e) {
204                 Connection.Abort(e);
205                 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
206                 throw;
207             }
208             finally {
209                 SqlStatistics.StopTimer(statistics);
210             }
211         }
212
213         override public void ChangeDatabase(string database) {
214             SqlConnection.ExecutePermission.Demand(); // MDAC 80961
215
216             if (ADP.IsEmpty(database)) {
217                 throw ADP.EmptyDatabaseName();
218             }
219
220             ValidateConnectionForExecute(null); // 
221
222             ChangeDatabaseInternal(database);  // do the real work...
223         }
224
225         abstract protected void ChangeDatabaseInternal(string database);
226         
227         override protected void CleanupTransactionOnCompletion(SysTx.Transaction transaction) {
228             // Note: unlocked, potentially multi-threaded code, so pull delegate to local to 
229             //  ensure it doesn't change between test and call.
230             SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction;
231             if (null != delegatedTransaction) {
232                 delegatedTransaction.TransactionEnded(transaction);
233             }
234         }
235
236         override protected DbReferenceCollection CreateReferenceCollection() {
237             return new SqlReferenceCollection();
238         }
239
240         override protected void Deactivate() {
241             if (Bid.AdvancedOn) {
242                 Bid.Trace("<sc.SqlInternalConnection.Deactivate|ADV> %d# deactivating\n", base.ObjectID);
243             }
244             TdsParser bestEffortCleanupTarget = null;
245             RuntimeHelpers.PrepareConstrainedRegions();
246             try {
247
248 #if DEBUG
249                 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
250
251                 RuntimeHelpers.PrepareConstrainedRegions();
252                 try {
253                     tdsReliabilitySection.Start();
254 #else
255                 {
256 #endif //DEBUG
257                     bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
258                     SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
259                     if (null != referenceCollection) {
260                         referenceCollection.Deactivate();
261                     }
262
263                     // Invoke subclass-specific deactivation logic
264                     InternalDeactivate();
265                 }
266 #if DEBUG
267                 finally {
268                     tdsReliabilitySection.Stop();
269                 }
270 #endif //DEBUG
271             }
272             catch (System.OutOfMemoryException) {
273                 DoomThisConnection();
274                 throw;
275             }
276             catch (System.StackOverflowException) {
277                 DoomThisConnection();
278                 throw;
279             }
280             catch (System.Threading.ThreadAbortException) {
281                 DoomThisConnection();
282                 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
283                 throw;
284             }
285             catch (Exception e) {
286                 // 
287                 if (!ADP.IsCatchableExceptionType(e)) {
288                     throw;
289                 }
290
291                 // if an exception occurred, the inner connection will be
292                 // marked as unusable and destroyed upon returning to the
293                 // pool
294                 DoomThisConnection();
295
296                 ADP.TraceExceptionWithoutRethrow(e);
297             }
298         }
299        
300         abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction);
301
302         override public void Dispose() {
303             _whereAbouts = null;
304             base.Dispose();
305         }
306
307         protected void Enlist(SysTx.Transaction tx) {
308             // This method should not be called while the connection has a 
309             // reference to an active delegated transaction.
310             // Manual enlistment via SqlConnection.EnlistTransaction
311             // should catch this case and throw an exception.
312             //
313             // Automatic enlistment isn't possible because 
314             // Sys.Tx keeps the connection alive until the transaction is completed.
315             Debug.Assert (!IsNonPoolableTransactionRoot, "cannot defect an active delegated transaction!");  // potential race condition, but it's an assert
316
317             if (null == tx) {
318                 if (IsEnlistedInTransaction)
319                 {
320                     EnlistNull();
321                 }
322                 else 
323                 {
324                     // When IsEnlistedInTransaction is false, it means we are in one of two states:
325                     // 1. EnlistTransaction is null, so the connection is truly not enlisted in a transaction, or
326                     // 2. Connection is enlisted in a SqlDelegatedTransaction.
327                     //
328                     // For #2, we have to consider whether or not the delegated transaction is active.
329                     // If it is not active, we allow the enlistment in the NULL transaction.
330                     //
331                     // If it is active, technically this is an error.
332                     // However, no exception is thrown as this was the precedent (and this case is silently ignored, no error, but no enlistment either).
333                     // There are two mitigations for this:
334                     // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has completed before allowing a different enlistment.
335                     // 2. For debug builds, the assert at the beginning of this method checks for an enlistment in an active delegated transaction.
336                     SysTx.Transaction enlistedTransaction = EnlistedTransaction;
337                     if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != SysTx.TransactionStatus.Active)
338                     {
339                         EnlistNull();
340                     }
341                 }
342             }
343             // Only enlist if it's different...
344             else if (!tx.Equals(EnlistedTransaction)) { // WebData 20000024 - Must use Equals, not !=
345                 EnlistNonNull(tx);
346             }
347         }
348
349         private void EnlistNonNull(SysTx.Transaction tx) {
350             Debug.Assert(null != tx, "null transaction?");
351
352             if (Bid.AdvancedOn) {
353                 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, transaction %d#.\n", base.ObjectID, tx.GetHashCode());
354             }
355             
356             bool hasDelegatedTransaction = false;
357
358             if (IsYukonOrNewer) {
359                 if (Bid.AdvancedOn) {
360                     Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, attempting to delegate\n", base.ObjectID);
361                 }
362
363                 // Promotable transactions are only supported on Yukon
364                 // servers or newer.
365                 SqlDelegatedTransaction delegatedTransaction = new SqlDelegatedTransaction(this, tx);
366                 
367                 try {
368                     // NOTE: System.Transactions claims to resolve all
369                     // potential race conditions between multiple delegate
370                     // requests of the same transaction to different
371                     // connections in their code, such that only one
372                     // attempt to delegate will succeed.
373
374                     // NOTE: PromotableSinglePhaseEnlist will eventually
375                     // make a round trip to the server; doing this inside
376                     // a lock is not the best choice.  We presume that you
377                     // aren't trying to enlist concurrently on two threads
378                     // and leave it at that -- We don't claim any thread
379                     // safety with regard to multiple concurrent requests
380                     // to enlist the same connection in different
381                     // transactions, which is good, because we don't have
382                     // it anyway.
383
384                     // PromotableSinglePhaseEnlist may not actually promote
385                     // the transaction when it is already delegated (this is
386                     // the way they resolve the race condition when two
387                     // threads attempt to delegate the same Lightweight
388                     // Transaction)  In that case, we can safely ignore
389                     // our delegated transaction, and proceed to enlist
390                     // in the promoted one.
391
392                     if (tx.EnlistPromotableSinglePhase(delegatedTransaction)) {
393                         hasDelegatedTransaction = true;
394
395                         this.DelegatedTransaction = delegatedTransaction;
396
397                         if (Bid.AdvancedOn) {
398                             long transactionId = SqlInternalTransaction.NullTransactionId;
399                             int transactionObjectID = 0; 
400                             if (null != CurrentTransaction) {
401                                 transactionId = CurrentTransaction.TransactionId;
402                                 transactionObjectID = CurrentTransaction.ObjectID;
403                             }
404                             Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegated to transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
405                         }
406                     }
407                 }
408                 catch (SqlException e) {
409                     // we do not want to eat the error if it is a fatal one
410                     if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) {
411                         throw;
412                     }
413
414                     // if the parser is null or its state is not openloggedin, the connection is no longer good.
415                     SqlInternalConnectionTds tdsConnection = this as SqlInternalConnectionTds;
416                     if (tdsConnection != null)
417                     {
418                         TdsParser parser = tdsConnection.Parser;
419                         if (parser == null || parser.State != TdsParserState.OpenLoggedIn)
420                         {
421                             throw;
422                         }
423                     }
424
425                     ADP.TraceExceptionWithoutRethrow(e);
426
427                     // In this case, SqlDelegatedTransaction.Initialize
428                     // failed and we don't necessarily want to reject
429                     // things -- there may have been a legitimate reason
430                     // for the failure.
431                 }
432             }
433             
434             if (!hasDelegatedTransaction) {
435                 if (Bid.AdvancedOn) {
436                     Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegation not possible, enlisting.\n", base.ObjectID);
437                 }
438
439                 byte[] cookie = null;
440
441                 if (null == _whereAbouts) {
442                      byte[] dtcAddress = GetDTCAddress();
443
444                      if (null == dtcAddress) {
445                         throw SQL.CannotGetDTCAddress();
446                      }
447                      _whereAbouts = dtcAddress;
448                 }
449                     
450                 cookie = GetTransactionCookie(tx, _whereAbouts);
451
452                 // send cookie to server to finish enlistment
453                 PropagateTransactionCookie(cookie);
454                 
455                 _isEnlistedInTransaction = true;
456
457                 if (Bid.AdvancedOn) {
458                     long transactionId = SqlInternalTransaction.NullTransactionId;
459                     int transactionObjectID = 0; 
460                     if (null != CurrentTransaction) {
461                         transactionId = CurrentTransaction.TransactionId;
462                         transactionObjectID = CurrentTransaction.ObjectID;
463                     }
464                     Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, enlisted with transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
465                 }
466             }
467
468             EnlistedTransaction = tx; // Tell the base class about our enlistment
469
470
471             // If we're on a Yukon or newer server, and we we delegate the 
472             // transaction successfully, we will have done a begin transaction, 
473             // which produces a transaction id that we should execute all requests
474             // on.  The TdsParser or SmiEventSink will store this information as
475             // the current transaction.
476             // 
477             // Likewise, propagating a transaction to a Yukon or newer server will
478             // produce a transaction id that The TdsParser or SmiEventSink will 
479             // store as the current transaction.
480             //
481             // In either case, when we're working with a Yukon or newer server 
482             // we better have a current transaction by now.
483
484             Debug.Assert(!IsYukonOrNewer || null != CurrentTransaction, "delegated/enlisted transaction with null current transaction?");
485         }
486
487         internal void EnlistNull() {
488             if (Bid.AdvancedOn) {
489                 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisting.\n", base.ObjectID);
490             }
491
492             // We were in a transaction, but now we are not - so send
493             // message to server with empty transaction - confirmed proper
494             // behavior from Sameet Agarwal
495             //
496             // The connection pooler maintains separate pools for enlisted
497             // transactions, and only when that transaction is committed or
498             // rolled back will those connections be taken from that
499             // separate pool and returned to the general pool of connections
500             // that are not affiliated with any transactions.  When this
501             // occurs, we will have a new transaction of null and we are
502             // required to send an empty transaction payload to the server.
503
504             PropagateTransactionCookie(null);
505
506             _isEnlistedInTransaction = false;
507             EnlistedTransaction = null; // Tell the base class about our enlistment
508
509             if (Bid.AdvancedOn) {
510                 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisted.\n", base.ObjectID);
511             }
512
513             // The EnlistTransaction above will return an TransactionEnded event, 
514             // which causes the TdsParser or SmiEventSink should to clear the
515             // current transaction.
516             //
517             // In either case, when we're working with a Yukon or newer server 
518             // we better not have a current transaction at this point.
519
520             Debug.Assert(!IsYukonOrNewer || null == CurrentTransaction, "unenlisted transaction with non-null current transaction?");   // verify it!
521         }
522         
523         override public void EnlistTransaction(SysTx.Transaction transaction) {
524             SqlConnection.VerifyExecutePermission();
525
526             ValidateConnectionForExecute(null);
527
528             // If a connection has a local transaction outstanding and you try
529             // to enlist in a DTC transaction, SQL Server will rollback the
530             // local transaction and then do the enlist (7.0 and 2000).  So, if
531             // the user tries to do this, throw.
532             if (HasLocalTransaction) {
533                 throw ADP.LocalTransactionPresent();
534             }
535
536             if (null != transaction && transaction.Equals(EnlistedTransaction)) {
537                 // No-op if this is the current transaction
538                 return;
539             }
540
541             // If a connection is already enlisted in a DTC transaction and you
542             // try to enlist in another one, in 7.0 the existing DTC transaction
543             // would roll back and then the connection would enlist in the new
544             // one. In SQL 2000 & Yukon, when you enlist in a DTC transaction
545             // while the connection is already enlisted in a DTC transaction,
546             // the connection simply switches enlistments.  Regardless, simply
547             // enlist in the user specified distributed transaction.  This
548             // behavior matches OLEDB and ODBC.
549
550             TdsParser bestEffortCleanupTarget = null;
551             RuntimeHelpers.PrepareConstrainedRegions();
552             try {
553 #if DEBUG
554                 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
555
556                 RuntimeHelpers.PrepareConstrainedRegions();
557                 try {
558                     tdsReliabilitySection.Start();
559 #else
560                 {
561 #endif //DEBUG
562                     bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
563                     Enlist(transaction);
564                 }
565 #if DEBUG
566                 finally {
567                     tdsReliabilitySection.Stop();
568                 }
569 #endif //DEBUG
570             }
571             catch (System.OutOfMemoryException e) {
572                 Connection.Abort(e);
573                 throw;
574             }
575             catch (System.StackOverflowException e) {
576                 Connection.Abort(e);
577                 throw;
578             }
579             catch (System.Threading.ThreadAbortException e) {
580                 Connection.Abort(e);
581                 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
582                 throw;
583             }
584         }
585
586         abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest);
587
588         internal SqlDataReader FindLiveReader(SqlCommand command) {
589             SqlDataReader reader = null;
590             SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
591             if (null != referenceCollection) {
592                 reader =  referenceCollection.FindLiveReader(command);
593             }
594             return reader;
595         }
596
597         internal SqlCommand FindLiveCommand(TdsParserStateObject stateObj) {
598             SqlCommand command = null;
599             SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
600             if (null != referenceCollection) {
601                 command =  referenceCollection.FindLiveCommand(stateObj);
602             }
603             return command;
604         }
605
606         static internal TdsParser GetBestEffortCleanupTarget(SqlConnection connection) {
607             if (null != connection) {
608                 SqlInternalConnectionTds innerConnection = (connection.InnerConnection as SqlInternalConnectionTds);
609                 if (null != innerConnection) {
610                     return innerConnection.Parser;
611                 }
612             }
613
614             return null;
615         }
616
617         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
618         static internal void BestEffortCleanup(TdsParser target) {
619             if (null != target) {
620                 target.BestEffortCleanup();
621             }
622         }
623
624         abstract protected byte[] GetDTCAddress();
625
626         static private byte[] GetTransactionCookie(SysTx.Transaction transaction, byte[] whereAbouts) {
627             byte[] transactionCookie = null;
628             if (null != transaction) {
629                 transactionCookie = SysTx.TransactionInterop.GetExportCookie(transaction, whereAbouts);
630             }
631             return transactionCookie;
632         }
633
634         virtual protected void InternalDeactivate() {
635         }
636
637         // If wrapCloseInAction is defined, then the action it defines will be run with the connection close action passed in as a parameter
638         // The close action also supports being run asynchronously
639         internal void OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction = null) {
640             if (breakConnection) {
641                 DoomThisConnection();
642             }
643
644             var connection = Connection;
645             if (null != connection) {
646                 connection.OnError(exception, breakConnection, wrapCloseInAction);
647             }
648             else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) {
649                 // It is an error, and should be thrown.  Class of TdsEnums.MIN_ERROR_CLASS
650                 // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message.
651                 throw exception;
652             }
653         }
654
655         abstract protected void PropagateTransactionCookie(byte[] transactionCookie);
656         
657         abstract internal void ValidateConnectionForExecute(SqlCommand command);
658     }
659 }