1 //------------------------------------------------------------------------------
2 // <copyright file="SqlInternalConnection.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.Collections.Generic;
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;
25 using System.Threading;
26 using SysTx = System.Transactions;
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
34 // if connection is not open: null
35 // if connection is open: currently active database
36 internal string CurrentDatabase { get; set; }
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; }
44 // the delegated (or promoted) transaction we're responsible for.
45 internal SqlDelegatedTransaction DelegatedTransaction { get; set; }
47 internal enum TransactionRequest {
56 internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() {
57 Debug.Assert(null != connectionOptions, "null connectionOptions?");
58 _connectionOptions = connectionOptions;
61 internal SqlConnection Connection {
63 return (SqlConnection)Owner;
67 internal SqlConnectionString ConnectionOptions {
69 return _connectionOptions;
73 abstract internal SqlInternalTransaction CurrentTransaction {
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 {
83 return CurrentTransaction;
87 abstract internal SqlInternalTransaction PendingTransaction {
91 override protected internal bool IsNonPoolableTransactionRoot {
93 return IsTransactionRoot; // default behavior is that root transactions are NOT poolable. Subclasses may override.
97 override internal bool IsTransactionRoot {
99 var delegatedTransaction = DelegatedTransaction;
100 return ((null != delegatedTransaction) && (delegatedTransaction.IsActive));
104 internal bool HasLocalTransaction {
106 SqlInternalTransaction currentTransaction = CurrentTransaction;
107 bool result = (null != currentTransaction && currentTransaction.IsLocal);
112 internal bool HasLocalTransactionFromAPI {
114 SqlInternalTransaction currentTransaction = CurrentTransaction;
115 bool result = (null != currentTransaction && currentTransaction.HasParentTransaction);
120 internal bool IsEnlistedInTransaction {
122 return _isEnlistedInTransaction;
126 abstract internal bool IsLockedForBulkCopy {
130 abstract internal bool IsShiloh {
134 abstract internal bool IsYukonOrNewer {
138 abstract internal bool IsKatmaiOrNewer {
142 internal byte[] PromotedDTCToken {
144 return _promotedDTCToken;
147 _promotedDTCToken = value;
151 override public DbTransaction BeginTransaction(IsolationLevel iso) {
152 return BeginSqlTransaction(iso, null, false);
155 virtual internal SqlTransaction BeginSqlTransaction(IsolationLevel iso, string transactionName, bool shouldReconnect) {
156 SqlStatistics statistics = null;
157 TdsParser bestEffortCleanupTarget = null;
158 RuntimeHelpers.PrepareConstrainedRegions();
161 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
163 RuntimeHelpers.PrepareConstrainedRegions();
165 tdsReliabilitySection.Start();
169 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
170 statistics = SqlStatistics.StartTimer(Connection.Statistics);
172 SqlConnection.ExecutePermission.Demand(); // MDAC 81476
174 ValidateConnectionForExecute(null);
176 if (HasLocalTransactionFromAPI)
177 throw ADP.ParallelTransactionsNotSupported(Connection);
179 if (iso == IsolationLevel.Unspecified) {
180 iso = IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified.
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;
191 tdsReliabilitySection.Stop();
195 catch (System.OutOfMemoryException e) {
199 catch (System.StackOverflowException e) {
203 catch (System.Threading.ThreadAbortException e) {
205 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
209 SqlStatistics.StopTimer(statistics);
213 override public void ChangeDatabase(string database) {
214 SqlConnection.ExecutePermission.Demand(); // MDAC 80961
216 if (ADP.IsEmpty(database)) {
217 throw ADP.EmptyDatabaseName();
220 ValidateConnectionForExecute(null); //
222 ChangeDatabaseInternal(database); // do the real work...
225 abstract protected void ChangeDatabaseInternal(string database);
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);
236 override protected DbReferenceCollection CreateReferenceCollection() {
237 return new SqlReferenceCollection();
240 override protected void Deactivate() {
241 if (Bid.AdvancedOn) {
242 Bid.Trace("<sc.SqlInternalConnection.Deactivate|ADV> %d# deactivating\n", base.ObjectID);
244 TdsParser bestEffortCleanupTarget = null;
245 RuntimeHelpers.PrepareConstrainedRegions();
249 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
251 RuntimeHelpers.PrepareConstrainedRegions();
253 tdsReliabilitySection.Start();
257 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
258 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
259 if (null != referenceCollection) {
260 referenceCollection.Deactivate();
263 // Invoke subclass-specific deactivation logic
264 InternalDeactivate();
268 tdsReliabilitySection.Stop();
272 catch (System.OutOfMemoryException) {
273 DoomThisConnection();
276 catch (System.StackOverflowException) {
277 DoomThisConnection();
280 catch (System.Threading.ThreadAbortException) {
281 DoomThisConnection();
282 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
285 catch (Exception e) {
287 if (!ADP.IsCatchableExceptionType(e)) {
291 // if an exception occurred, the inner connection will be
292 // marked as unusable and destroyed upon returning to the
294 DoomThisConnection();
296 ADP.TraceExceptionWithoutRethrow(e);
300 abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction);
302 override public void Dispose() {
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.
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
318 if (IsEnlistedInTransaction)
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.
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.
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)
343 // Only enlist if it's different...
344 else if (!tx.Equals(EnlistedTransaction)) { // WebData 20000024 - Must use Equals, not !=
349 private void EnlistNonNull(SysTx.Transaction tx) {
350 Debug.Assert(null != tx, "null transaction?");
352 if (Bid.AdvancedOn) {
353 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, transaction %d#.\n", base.ObjectID, tx.GetHashCode());
356 bool hasDelegatedTransaction = false;
358 if (IsYukonOrNewer) {
359 if (Bid.AdvancedOn) {
360 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, attempting to delegate\n", base.ObjectID);
363 // Promotable transactions are only supported on Yukon
365 SqlDelegatedTransaction delegatedTransaction = new SqlDelegatedTransaction(this, tx);
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.
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
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.
392 if (tx.EnlistPromotableSinglePhase(delegatedTransaction)) {
393 hasDelegatedTransaction = true;
395 this.DelegatedTransaction = delegatedTransaction;
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;
404 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegated to transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
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) {
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)
418 TdsParser parser = tdsConnection.Parser;
419 if (parser == null || parser.State != TdsParserState.OpenLoggedIn)
425 ADP.TraceExceptionWithoutRethrow(e);
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
434 if (!hasDelegatedTransaction) {
435 if (Bid.AdvancedOn) {
436 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegation not possible, enlisting.\n", base.ObjectID);
439 byte[] cookie = null;
441 if (null == _whereAbouts) {
442 byte[] dtcAddress = GetDTCAddress();
444 if (null == dtcAddress) {
445 throw SQL.CannotGetDTCAddress();
447 _whereAbouts = dtcAddress;
450 cookie = GetTransactionCookie(tx, _whereAbouts);
452 // send cookie to server to finish enlistment
453 PropagateTransactionCookie(cookie);
455 _isEnlistedInTransaction = true;
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;
464 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, enlisted with transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
468 EnlistedTransaction = tx; // Tell the base class about our enlistment
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.
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.
481 // In either case, when we're working with a Yukon or newer server
482 // we better have a current transaction by now.
484 Debug.Assert(!IsYukonOrNewer || null != CurrentTransaction, "delegated/enlisted transaction with null current transaction?");
487 internal void EnlistNull() {
488 if (Bid.AdvancedOn) {
489 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisting.\n", base.ObjectID);
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
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.
504 PropagateTransactionCookie(null);
506 _isEnlistedInTransaction = false;
507 EnlistedTransaction = null; // Tell the base class about our enlistment
509 if (Bid.AdvancedOn) {
510 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisted.\n", base.ObjectID);
513 // The EnlistTransaction above will return an TransactionEnded event,
514 // which causes the TdsParser or SmiEventSink should to clear the
515 // current transaction.
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.
520 Debug.Assert(!IsYukonOrNewer || null == CurrentTransaction, "unenlisted transaction with non-null current transaction?"); // verify it!
523 override public void EnlistTransaction(SysTx.Transaction transaction) {
524 SqlConnection.VerifyExecutePermission();
526 ValidateConnectionForExecute(null);
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();
536 if (null != transaction && transaction.Equals(EnlistedTransaction)) {
537 // No-op if this is the current transaction
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.
550 TdsParser bestEffortCleanupTarget = null;
551 RuntimeHelpers.PrepareConstrainedRegions();
554 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
556 RuntimeHelpers.PrepareConstrainedRegions();
558 tdsReliabilitySection.Start();
562 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
567 tdsReliabilitySection.Stop();
571 catch (System.OutOfMemoryException e) {
575 catch (System.StackOverflowException e) {
579 catch (System.Threading.ThreadAbortException e) {
581 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
586 abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest);
588 internal SqlDataReader FindLiveReader(SqlCommand command) {
589 SqlDataReader reader = null;
590 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
591 if (null != referenceCollection) {
592 reader = referenceCollection.FindLiveReader(command);
597 internal SqlCommand FindLiveCommand(TdsParserStateObject stateObj) {
598 SqlCommand command = null;
599 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
600 if (null != referenceCollection) {
601 command = referenceCollection.FindLiveCommand(stateObj);
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;
617 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
618 static internal void BestEffortCleanup(TdsParser target) {
619 if (null != target) {
620 target.BestEffortCleanup();
624 abstract protected byte[] GetDTCAddress();
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);
631 return transactionCookie;
634 virtual protected void InternalDeactivate() {
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();
644 var connection = Connection;
645 if (null != connection) {
646 connection.OnError(exception, breakConnection, wrapCloseInAction);
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.
655 abstract protected void PropagateTransactionCookie(byte[] transactionCookie);
657 abstract internal void ValidateConnectionForExecute(SqlCommand command);