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 private bool _isGlobalTransaction = false; // Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction)
35 private bool _isGlobalTransactionEnabledForServer = false; // Whether Global Transactions are enabled for this Azure SQL DB Server
36 private static readonly Guid _globalTransactionTMID = new Guid("1c742caf-6680-40ea-9c26-6b6846079764"); // ID of the Non-MSDTC, Azure SQL DB Transaction Manager
38 // if connection is not open: null
39 // if connection is open: currently active database
40 internal string CurrentDatabase { get; set; }
42 // if connection is not open yet, CurrentDataSource is null
43 // if connection is open:
44 // * for regular connections, it is set to Data Source value from connection string
45 // * for connections with FailoverPartner, it is set to the FailoverPartner value from connection string if the connection was opened to it.
46 internal string CurrentDataSource { get; set; }
48 // the delegated (or promoted) transaction we're responsible for.
49 internal SqlDelegatedTransaction DelegatedTransaction { get; set; }
51 internal enum TransactionRequest {
60 internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() {
61 Debug.Assert(null != connectionOptions, "null connectionOptions?");
62 _connectionOptions = connectionOptions;
65 internal SqlConnection Connection {
67 return (SqlConnection)Owner;
71 internal SqlConnectionString ConnectionOptions {
73 return _connectionOptions;
77 abstract internal SqlInternalTransaction CurrentTransaction {
82 // Get the internal transaction that should be hooked to a new outer transaction
83 // during a BeginTransaction API call. In some cases (i.e. connection is going to
84 // be reset), CurrentTransaction should not be hooked up this way.
85 virtual internal SqlInternalTransaction AvailableInternalTransaction {
87 return CurrentTransaction;
91 abstract internal SqlInternalTransaction PendingTransaction {
95 override protected internal bool IsNonPoolableTransactionRoot {
97 return IsTransactionRoot; // default behavior is that root transactions are NOT poolable. Subclasses may override.
101 override internal bool IsTransactionRoot {
103 var delegatedTransaction = DelegatedTransaction;
104 return ((null != delegatedTransaction) && (delegatedTransaction.IsActive));
108 internal bool HasLocalTransaction {
110 SqlInternalTransaction currentTransaction = CurrentTransaction;
111 bool result = (null != currentTransaction && currentTransaction.IsLocal);
116 internal bool HasLocalTransactionFromAPI {
118 SqlInternalTransaction currentTransaction = CurrentTransaction;
119 bool result = (null != currentTransaction && currentTransaction.HasParentTransaction);
124 internal bool IsEnlistedInTransaction {
126 return _isEnlistedInTransaction;
130 abstract internal bool IsLockedForBulkCopy {
134 abstract internal bool IsShiloh {
138 abstract internal bool IsYukonOrNewer {
142 abstract internal bool IsKatmaiOrNewer {
146 internal byte[] PromotedDTCToken {
148 return _promotedDTCToken;
151 _promotedDTCToken = value;
155 internal bool IsGlobalTransaction {
157 return _isGlobalTransaction;
160 _isGlobalTransaction = value;
164 internal bool IsGlobalTransactionsEnabledForServer {
166 return _isGlobalTransactionEnabledForServer;
169 _isGlobalTransactionEnabledForServer = value;
173 override public DbTransaction BeginTransaction(IsolationLevel iso) {
174 return BeginSqlTransaction(iso, null, false);
177 virtual internal SqlTransaction BeginSqlTransaction(IsolationLevel iso, string transactionName, bool shouldReconnect) {
178 SqlStatistics statistics = null;
179 TdsParser bestEffortCleanupTarget = null;
180 RuntimeHelpers.PrepareConstrainedRegions();
183 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
185 RuntimeHelpers.PrepareConstrainedRegions();
187 tdsReliabilitySection.Start();
191 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
192 statistics = SqlStatistics.StartTimer(Connection.Statistics);
194 SqlConnection.ExecutePermission.Demand(); // MDAC 81476
196 ValidateConnectionForExecute(null);
198 if (HasLocalTransactionFromAPI)
199 throw ADP.ParallelTransactionsNotSupported(Connection);
201 if (iso == IsolationLevel.Unspecified) {
202 iso = IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified.
205 SqlTransaction transaction = new SqlTransaction(this, Connection, iso, AvailableInternalTransaction);
206 transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect;
207 ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false);
208 transaction.InternalTransaction.RestoreBrokenConnection = false;
213 tdsReliabilitySection.Stop();
217 catch (System.OutOfMemoryException e) {
221 catch (System.StackOverflowException e) {
225 catch (System.Threading.ThreadAbortException e) {
227 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
231 SqlStatistics.StopTimer(statistics);
235 override public void ChangeDatabase(string database) {
236 SqlConnection.ExecutePermission.Demand(); // MDAC 80961
238 if (ADP.IsEmpty(database)) {
239 throw ADP.EmptyDatabaseName();
242 ValidateConnectionForExecute(null); //
244 ChangeDatabaseInternal(database); // do the real work...
247 abstract protected void ChangeDatabaseInternal(string database);
249 override protected void CleanupTransactionOnCompletion(SysTx.Transaction transaction) {
250 // Note: unlocked, potentially multi-threaded code, so pull delegate to local to
251 // ensure it doesn't change between test and call.
252 SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction;
253 if (null != delegatedTransaction) {
254 delegatedTransaction.TransactionEnded(transaction);
258 override protected DbReferenceCollection CreateReferenceCollection() {
259 return new SqlReferenceCollection();
262 override protected void Deactivate() {
263 if (Bid.AdvancedOn) {
264 Bid.Trace("<sc.SqlInternalConnection.Deactivate|ADV> %d# deactivating\n", base.ObjectID);
266 TdsParser bestEffortCleanupTarget = null;
267 RuntimeHelpers.PrepareConstrainedRegions();
271 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
273 RuntimeHelpers.PrepareConstrainedRegions();
275 tdsReliabilitySection.Start();
279 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
280 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
281 if (null != referenceCollection) {
282 referenceCollection.Deactivate();
285 // Invoke subclass-specific deactivation logic
286 InternalDeactivate();
290 tdsReliabilitySection.Stop();
294 catch (System.OutOfMemoryException) {
295 DoomThisConnection();
298 catch (System.StackOverflowException) {
299 DoomThisConnection();
302 catch (System.Threading.ThreadAbortException) {
303 DoomThisConnection();
304 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
307 catch (Exception e) {
309 if (!ADP.IsCatchableExceptionType(e)) {
313 // if an exception occurred, the inner connection will be
314 // marked as unusable and destroyed upon returning to the
316 DoomThisConnection();
318 ADP.TraceExceptionWithoutRethrow(e);
322 abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction);
324 override public void Dispose() {
329 protected void Enlist(SysTx.Transaction tx) {
330 // This method should not be called while the connection has a
331 // reference to an active delegated transaction.
332 // Manual enlistment via SqlConnection.EnlistTransaction
333 // should catch this case and throw an exception.
335 // Automatic enlistment isn't possible because
336 // Sys.Tx keeps the connection alive until the transaction is completed.
337 Debug.Assert (!IsNonPoolableTransactionRoot, "cannot defect an active delegated transaction!"); // potential race condition, but it's an assert
340 if (IsEnlistedInTransaction)
346 // When IsEnlistedInTransaction is false, it means we are in one of two states:
347 // 1. EnlistTransaction is null, so the connection is truly not enlisted in a transaction, or
348 // 2. Connection is enlisted in a SqlDelegatedTransaction.
350 // For #2, we have to consider whether or not the delegated transaction is active.
351 // If it is not active, we allow the enlistment in the NULL transaction.
353 // If it is active, technically this is an error.
354 // However, no exception is thrown as this was the precedent (and this case is silently ignored, no error, but no enlistment either).
355 // There are two mitigations for this:
356 // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has completed before allowing a different enlistment.
357 // 2. For debug builds, the assert at the beginning of this method checks for an enlistment in an active delegated transaction.
358 SysTx.Transaction enlistedTransaction = EnlistedTransaction;
359 if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != SysTx.TransactionStatus.Active)
365 // Only enlist if it's different...
366 else if (!tx.Equals(EnlistedTransaction)) { // WebData 20000024 - Must use Equals, not !=
371 private void EnlistNonNull(SysTx.Transaction tx) {
372 Debug.Assert(null != tx, "null transaction?");
374 if (Bid.AdvancedOn) {
375 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, transaction %d#.\n", base.ObjectID, tx.GetHashCode());
378 bool hasDelegatedTransaction = false;
380 if (IsYukonOrNewer) {
381 if (Bid.AdvancedOn) {
382 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, attempting to delegate\n", base.ObjectID);
385 // Promotable transactions are only supported on Yukon
387 SqlDelegatedTransaction delegatedTransaction = new SqlDelegatedTransaction(this, tx);
390 // NOTE: System.Transactions claims to resolve all
391 // potential race conditions between multiple delegate
392 // requests of the same transaction to different
393 // connections in their code, such that only one
394 // attempt to delegate will succeed.
396 // NOTE: PromotableSinglePhaseEnlist will eventually
397 // make a round trip to the server; doing this inside
398 // a lock is not the best choice. We presume that you
399 // aren't trying to enlist concurrently on two threads
400 // and leave it at that -- We don't claim any thread
401 // safety with regard to multiple concurrent requests
402 // to enlist the same connection in different
403 // transactions, which is good, because we don't have
406 // PromotableSinglePhaseEnlist may not actually promote
407 // the transaction when it is already delegated (this is
408 // the way they resolve the race condition when two
409 // threads attempt to delegate the same Lightweight
410 // Transaction) In that case, we can safely ignore
411 // our delegated transaction, and proceed to enlist
412 // in the promoted one.
414 // NOTE: Global Transactions is an Azure SQL DB only
415 // feature where the Transaction Manager (TM) is not
416 // MS-DTC. Sys.Tx added APIs to support Non MS-DTC
417 // promoter types/TM in .NET 4.6.1. Following directions
418 // from .NETFX shiproom, to avoid a "hard-dependency"
419 // (compile time) on Sys.Tx, we use reflection to invoke
420 // the new APIs. Further, the _isGlobalTransaction flag
421 // indicates that this is an Azure SQL DB Transaction
422 // that could be promoted to a Global Transaction (it's
423 // always false for on-prem Sql Server). The Promote()
424 // call in SqlDelegatedTransaction makes sure that the
425 // right Sys.Tx.dll is loaded and that Global Transactions
426 // are actually allowed for this Azure SQL DB.
428 if (_isGlobalTransaction) {
429 if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) {
430 // This could be a local Azure SQL DB transaction.
431 hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction);
434 hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke(tx, new object[] { delegatedTransaction, _globalTransactionTMID });
438 // This is an MS-DTC distributed transaction
439 hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction);
442 if (hasDelegatedTransaction) {
444 this.DelegatedTransaction = delegatedTransaction;
446 if (Bid.AdvancedOn) {
447 long transactionId = SqlInternalTransaction.NullTransactionId;
448 int transactionObjectID = 0;
449 if (null != CurrentTransaction) {
450 transactionId = CurrentTransaction.TransactionId;
451 transactionObjectID = CurrentTransaction.ObjectID;
453 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegated to transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
457 catch (SqlException e) {
458 // we do not want to eat the error if it is a fatal one
459 if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) {
463 // if the parser is null or its state is not openloggedin, the connection is no longer good.
464 SqlInternalConnectionTds tdsConnection = this as SqlInternalConnectionTds;
465 if (tdsConnection != null)
467 TdsParser parser = tdsConnection.Parser;
468 if (parser == null || parser.State != TdsParserState.OpenLoggedIn)
474 ADP.TraceExceptionWithoutRethrow(e);
476 // In this case, SqlDelegatedTransaction.Initialize
477 // failed and we don't necessarily want to reject
478 // things -- there may have been a legitimate reason
483 if (!hasDelegatedTransaction) {
484 if (Bid.AdvancedOn) {
485 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegation not possible, enlisting.\n", base.ObjectID);
488 byte[] cookie = null;
490 if (_isGlobalTransaction) {
491 if (SysTxForGlobalTransactions.GetPromotedToken == null) {
492 throw SQL.UnsupportedSysTxForGlobalTransactions();
495 cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(tx, null);
498 if (null == _whereAbouts) {
499 byte[] dtcAddress = GetDTCAddress();
501 if (null == dtcAddress) {
502 throw SQL.CannotGetDTCAddress();
504 _whereAbouts = dtcAddress;
507 cookie = GetTransactionCookie(tx, _whereAbouts);
510 // send cookie to server to finish enlistment
511 PropagateTransactionCookie(cookie);
513 _isEnlistedInTransaction = true;
515 if (Bid.AdvancedOn) {
516 long transactionId = SqlInternalTransaction.NullTransactionId;
517 int transactionObjectID = 0;
518 if (null != CurrentTransaction) {
519 transactionId = CurrentTransaction.TransactionId;
520 transactionObjectID = CurrentTransaction.ObjectID;
522 Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, enlisted with transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
526 EnlistedTransaction = tx; // Tell the base class about our enlistment
529 // If we're on a Yukon or newer server, and we we delegate the
530 // transaction successfully, we will have done a begin transaction,
531 // which produces a transaction id that we should execute all requests
532 // on. The TdsParser or SmiEventSink will store this information as
533 // the current transaction.
535 // Likewise, propagating a transaction to a Yukon or newer server will
536 // produce a transaction id that The TdsParser or SmiEventSink will
537 // store as the current transaction.
539 // In either case, when we're working with a Yukon or newer server
540 // we better have a current transaction by now.
542 Debug.Assert(!IsYukonOrNewer || null != CurrentTransaction, "delegated/enlisted transaction with null current transaction?");
545 internal void EnlistNull() {
546 if (Bid.AdvancedOn) {
547 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisting.\n", base.ObjectID);
550 // We were in a transaction, but now we are not - so send
551 // message to server with empty transaction - confirmed proper
552 // behavior from Sameet Agarwal
554 // The connection pooler maintains separate pools for enlisted
555 // transactions, and only when that transaction is committed or
556 // rolled back will those connections be taken from that
557 // separate pool and returned to the general pool of connections
558 // that are not affiliated with any transactions. When this
559 // occurs, we will have a new transaction of null and we are
560 // required to send an empty transaction payload to the server.
562 PropagateTransactionCookie(null);
564 _isEnlistedInTransaction = false;
565 EnlistedTransaction = null; // Tell the base class about our enlistment
567 if (Bid.AdvancedOn) {
568 Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisted.\n", base.ObjectID);
571 // The EnlistTransaction above will return an TransactionEnded event,
572 // which causes the TdsParser or SmiEventSink should to clear the
573 // current transaction.
575 // In either case, when we're working with a Yukon or newer server
576 // we better not have a current transaction at this point.
578 Debug.Assert(!IsYukonOrNewer || null == CurrentTransaction, "unenlisted transaction with non-null current transaction?"); // verify it!
581 override public void EnlistTransaction(SysTx.Transaction transaction) {
582 SqlConnection.VerifyExecutePermission();
584 ValidateConnectionForExecute(null);
586 // If a connection has a local transaction outstanding and you try
587 // to enlist in a DTC transaction, SQL Server will rollback the
588 // local transaction and then do the enlist (7.0 and 2000). So, if
589 // the user tries to do this, throw.
590 if (HasLocalTransaction) {
591 throw ADP.LocalTransactionPresent();
594 if (null != transaction && transaction.Equals(EnlistedTransaction)) {
595 // No-op if this is the current transaction
599 // If a connection is already enlisted in a DTC transaction and you
600 // try to enlist in another one, in 7.0 the existing DTC transaction
601 // would roll back and then the connection would enlist in the new
602 // one. In SQL 2000 & Yukon, when you enlist in a DTC transaction
603 // while the connection is already enlisted in a DTC transaction,
604 // the connection simply switches enlistments. Regardless, simply
605 // enlist in the user specified distributed transaction. This
606 // behavior matches OLEDB and ODBC.
608 TdsParser bestEffortCleanupTarget = null;
609 RuntimeHelpers.PrepareConstrainedRegions();
612 TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
614 RuntimeHelpers.PrepareConstrainedRegions();
616 tdsReliabilitySection.Start();
620 bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
625 tdsReliabilitySection.Stop();
629 catch (System.OutOfMemoryException e) {
633 catch (System.StackOverflowException e) {
637 catch (System.Threading.ThreadAbortException e) {
639 SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
644 abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest);
646 internal SqlDataReader FindLiveReader(SqlCommand command) {
647 SqlDataReader reader = null;
648 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
649 if (null != referenceCollection) {
650 reader = referenceCollection.FindLiveReader(command);
655 internal SqlCommand FindLiveCommand(TdsParserStateObject stateObj) {
656 SqlCommand command = null;
657 SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
658 if (null != referenceCollection) {
659 command = referenceCollection.FindLiveCommand(stateObj);
664 static internal TdsParser GetBestEffortCleanupTarget(SqlConnection connection) {
665 if (null != connection) {
666 SqlInternalConnectionTds innerConnection = (connection.InnerConnection as SqlInternalConnectionTds);
667 if (null != innerConnection) {
668 return innerConnection.Parser;
675 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
676 static internal void BestEffortCleanup(TdsParser target) {
677 if (null != target) {
678 target.BestEffortCleanup();
682 abstract protected byte[] GetDTCAddress();
684 static private byte[] GetTransactionCookie(SysTx.Transaction transaction, byte[] whereAbouts) {
685 byte[] transactionCookie = null;
686 if (null != transaction) {
687 transactionCookie = SysTx.TransactionInterop.GetExportCookie(transaction, whereAbouts);
689 return transactionCookie;
692 virtual protected void InternalDeactivate() {
695 // If wrapCloseInAction is defined, then the action it defines will be run with the connection close action passed in as a parameter
696 // The close action also supports being run asynchronously
697 internal void OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction = null) {
698 if (breakConnection) {
699 DoomThisConnection();
702 var connection = Connection;
703 if (null != connection) {
704 connection.OnError(exception, breakConnection, wrapCloseInAction);
706 else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) {
707 // It is an error, and should be thrown. Class of TdsEnums.MIN_ERROR_CLASS
708 // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message.
713 abstract protected void PropagateTransactionCookie(byte[] transactionCookie);
715 abstract internal void ValidateConnectionForExecute(SqlCommand command);