2016-08-03 10:59:49 +00:00
//------------------------------------------------------------------------------
// <copyright file="SqlInternalTransaction.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
2017-08-21 15:34:15 +00:00
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="true" primary="false">Microsoft</owner>
2016-08-03 10:59:49 +00:00
//------------------------------------------------------------------------------
namespace System.Data.SqlClient {
using System.Data ;
using System.Data.Common ;
using System.Data.ProviderBase ;
using System.Data.Sql ;
using System.Data.SqlTypes ;
using System.Diagnostics ;
using System.Threading ;
internal enum TransactionState {
Pending = 0 ,
Active = 1 ,
Aborted = 2 ,
Committed = 3 ,
Unknown = 4 ,
}
internal enum TransactionType {
LocalFromTSQL = 1 ,
LocalFromAPI = 2 ,
Delegated = 3 ,
Distributed = 4 ,
Context = 5 , // only valid in proc.
} ;
sealed internal class SqlInternalTransaction {
internal const long NullTransactionId = 0 ;
private TransactionState _transactionState ;
private TransactionType _transactionType ;
private long _transactionId ; // passed in the MARS headers
private int _openResultCount ; // passed in the MARS headers
private SqlInternalConnection _innerConnection ;
private bool _disposing ; // used to prevent us from throwing exceptions while we're disposing
private WeakReference _parent ; // weak ref to the outer transaction object; needs to be weak to allow GC to occur.
private static int _objectTypeCount ; // Bid counter
internal readonly int _objectID = System . Threading . Interlocked . Increment ( ref _objectTypeCount ) ;
internal bool RestoreBrokenConnection { get ; set ; }
internal bool ConnectionHasBeenRestored { get ; set ; }
internal SqlInternalTransaction ( SqlInternalConnection innerConnection , TransactionType type , SqlTransaction outerTransaction ) : this ( innerConnection , type , outerTransaction , NullTransactionId ) {
}
internal SqlInternalTransaction ( SqlInternalConnection innerConnection , TransactionType type , SqlTransaction outerTransaction , long transactionId ) {
Bid . PoolerTrace ( "<sc.SqlInternalTransaction.ctor|RES|CPOOL> %d#, Created for connection %d#, outer transaction %d#, Type %d\n" ,
ObjectID ,
innerConnection . ObjectID ,
( null ! = outerTransaction ) ? outerTransaction . ObjectID : - 1 ,
( int ) type ) ;
_innerConnection = innerConnection ;
_transactionType = type ;
if ( null ! = outerTransaction ) {
_parent = new WeakReference ( outerTransaction ) ;
}
_transactionId = transactionId ;
RestoreBrokenConnection = false ;
ConnectionHasBeenRestored = false ;
}
internal bool HasParentTransaction {
get {
// Return true if we are an API started local transaction, or if we were a TSQL
// started local transaction and were then wrapped with a parent transaction as
// a result of a later API begin transaction.
bool result = ( ( TransactionType . LocalFromAPI = = _transactionType ) | |
( TransactionType . LocalFromTSQL = = _transactionType & & _parent ! = null ) ) ;
return result ;
}
}
internal bool IsAborted {
get {
return ( TransactionState . Aborted = = _transactionState ) ;
}
}
internal bool IsActive {
get {
return ( TransactionState . Active = = _transactionState ) ;
}
}
internal bool IsCommitted {
get {
return ( TransactionState . Committed = = _transactionState ) ;
}
}
internal bool IsCompleted {
get {
return ( TransactionState . Aborted = = _transactionState
| | TransactionState . Committed = = _transactionState
| | TransactionState . Unknown = = _transactionState ) ;
}
}
internal bool IsContext {
get {
bool result = ( TransactionType . Context = = _transactionType ) ;
return result ;
}
}
internal bool IsDelegated {
get {
bool result = ( TransactionType . Delegated = = _transactionType ) ;
return result ;
}
}
internal bool IsDistributed {
get {
bool result = ( TransactionType . Distributed = = _transactionType ) ;
return result ;
}
}
internal bool IsLocal {
get {
bool result = ( TransactionType . LocalFromTSQL = = _transactionType
| | TransactionType . LocalFromAPI = = _transactionType
| | TransactionType . Context = = _transactionType ) ;
return result ;
}
}
internal bool IsOrphaned {
get {
// An internal transaction is orphaned when its parent has been
// reclaimed by GC.
bool result ;
if ( null = = _parent ) {
// No parent, so we better be LocalFromTSQL. Should we even return in this case -
// since it could be argued this is invalid?
Debug . Assert ( false , "Why are we calling IsOrphaned with no parent?" ) ;
Debug . Assert ( _transactionType = = TransactionType . LocalFromTSQL , "invalid state" ) ;
result = false ;
}
else if ( null = = _parent . Target ) {
// We have an parent, but parent was GC'ed.
result = true ;
}
else {
// We have an parent, and parent is alive.
result = false ;
}
return result ;
}
}
internal bool IsZombied {
get {
return ( null = = _innerConnection ) ;
}
}
internal int ObjectID {
get {
return _objectID ;
}
}
internal int OpenResultsCount {
get {
return _openResultCount ;
}
}
internal SqlTransaction Parent {
get {
SqlTransaction result = null ;
// Should we protect against this, since this probably is an invalid state?
Debug . Assert ( null ! = _parent , "Why are we calling Parent with no parent?" ) ;
if ( null ! = _parent ) {
result = ( SqlTransaction ) _parent . Target ;
}
return result ;
}
}
internal long TransactionId {
get {
return _transactionId ;
}
set {
Debug . Assert ( NullTransactionId = = _transactionId , "setting transaction cookie while one is active?" ) ;
_transactionId = value ;
}
}
internal void Activate ( ) {
_transactionState = TransactionState . Active ;
}
private void CheckTransactionLevelAndZombie ( ) {
try {
if ( ! IsZombied & & GetServerTransactionLevel ( ) = = 0 ) {
// If not zombied, not closed, and not in transaction, zombie.
Zombie ( ) ;
}
}
catch ( Exception e ) {
//
if ( ! ADP . IsCatchableExceptionType ( e ) ) {
throw ;
}
ADP . TraceExceptionWithoutRethrow ( e ) ;
Zombie ( ) ; // If exception caught when trying to check level, zombie.
}
}
internal void CloseFromConnection ( ) {
SqlInternalConnection innerConnection = _innerConnection ;
Debug . Assert ( innerConnection ! = null , "How can we be here if the connection is null?" ) ;
Bid . PoolerTrace ( "<sc.SqlInteralTransaction.CloseFromConnection|RES|CPOOL> %d#, Closing\n" , ObjectID ) ;
bool processFinallyBlock = true ;
try {
innerConnection . ExecuteTransaction ( SqlInternalConnection . TransactionRequest . IfRollback , null , IsolationLevel . Unspecified , null , false ) ;
}
catch ( Exception e ) {
processFinallyBlock = ADP . IsCatchableExceptionType ( e ) ;
throw ;
}
finally {
TdsParser . ReliabilitySection . Assert ( "unreliable call to CloseFromConnection" ) ; // you need to setup for a thread abort somewhere before you call this method
if ( processFinallyBlock ) {
// Always ensure we're zombied; Yukon will send an EnvChange that
// will cause the zombie, but only if we actually go to the wire;
// Sphinx and Shiloh won't send the env change, so we have to handle
// them ourselves.
Zombie ( ) ;
}
}
}
internal void Commit ( ) {
IntPtr hscp ;
Bid . ScopeEnter ( out hscp , "<sc.SqlInternalTransaction.Commit|API> %d#" , ObjectID ) ;
if ( _innerConnection . IsLockedForBulkCopy ) {
throw SQL . ConnectionLockedForBcpEvent ( ) ;
}
_innerConnection . ValidateConnectionForExecute ( null ) ;
try {
// If this transaction has been completed, throw exception since it is unusable.
try {
// COMMIT ignores transaction names, and so there is no reason to pass it anything. COMMIT
// simply commits the transaction from the most recent BEGIN, nested or otherwise.
_innerConnection . ExecuteTransaction ( SqlInternalConnection . TransactionRequest . Commit , null , IsolationLevel . Unspecified , null , false ) ;
// SQL BU DT 291159 - perform full Zombie on pre-Yukon, but do not actually
// complete internal transaction until informed by server in the case of Yukon
// or later.
if ( ! IsZombied & & ! _innerConnection . IsYukonOrNewer ) {
// Since nested transactions are no longer allowed, set flag to false.
// This transaction has been completed.
Zombie ( ) ;
}
else {
ZombieParent ( ) ;
}
}
catch ( Exception e ) {
//
if ( ADP . IsCatchableExceptionType ( e ) ) {
CheckTransactionLevelAndZombie ( ) ;
}
throw ;
}
}
finally {
Bid . ScopeLeave ( ref hscp ) ;
}
}
internal void Completed ( TransactionState transactionState ) {
Debug . Assert ( TransactionState . Active < transactionState , "invalid transaction completion state?" ) ;
_transactionState = transactionState ;
Zombie ( ) ;
}
internal Int32 DecrementAndObtainOpenResultCount ( ) {
Int32 openResultCount = Interlocked . Decrement ( ref _openResultCount ) ;
if ( openResultCount < 0 ) {
throw SQL . OpenResultCountExceeded ( ) ;
}
return openResultCount ;
}
internal void Dispose ( ) {
this . Dispose ( true ) ;
System . GC . SuppressFinalize ( this ) ;
}
private /*protected override*/ void Dispose ( bool disposing ) {
Bid . PoolerTrace ( "<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing\n" , ObjectID ) ;
if ( disposing ) {
if ( null ! = _innerConnection ) {
// implicitly rollback if transaction still valid
_disposing = true ;
this . Rollback ( ) ;
}
}
}
private int GetServerTransactionLevel ( ) {
// This function is needed for those times when it is impossible to determine the server's
// transaction level, unless the user's arguments were parsed - which is something we don't want
// to do. An example when it is impossible to determine the level is after a rollback.
//
using ( SqlCommand transactionLevelCommand = new SqlCommand ( "set @out = @@trancount" , ( SqlConnection ) ( _innerConnection . Owner ) ) ) {
transactionLevelCommand . Transaction = Parent ;
SqlParameter parameter = new SqlParameter ( "@out" , SqlDbType . Int ) ;
parameter . Direction = ParameterDirection . Output ;
transactionLevelCommand . Parameters . Add ( parameter ) ;
//
transactionLevelCommand . RunExecuteReader ( 0 , RunBehavior . UntilDone , false /* returnDataStream */ , ADP . GetServerTransactionLevel ) ;
return ( int ) parameter . Value ;
}
}
internal Int32 IncrementAndObtainOpenResultCount ( ) {
Int32 openResultCount = Interlocked . Increment ( ref _openResultCount ) ;
if ( openResultCount < 0 ) {
throw SQL . OpenResultCountExceeded ( ) ;
}
return openResultCount ;
}
internal void InitParent ( SqlTransaction transaction ) {
Debug . Assert ( _parent = = null , "Why do we have a parent on InitParent?" ) ;
_parent = new WeakReference ( transaction ) ;
}
internal void Rollback ( ) {
IntPtr hscp ;
Bid . ScopeEnter ( out hscp , "<sc.SqlInternalTransaction.Rollback|API> %d#" , ObjectID ) ;
if ( _innerConnection . IsLockedForBulkCopy ) {
throw SQL . ConnectionLockedForBcpEvent ( ) ;
}
_innerConnection . ValidateConnectionForExecute ( null ) ;
try {
try {
// If no arg is given to ROLLBACK it will rollback to the outermost begin - rolling back
// all nested transactions as well as the outermost transaction.
_innerConnection . ExecuteTransaction ( SqlInternalConnection . TransactionRequest . IfRollback , null , IsolationLevel . Unspecified , null , false ) ;
// Since Rollback will rollback to outermost begin, no need to check
// server transaction level. This transaction has been completed.
Zombie ( ) ;
}
catch ( Exception e ) {
//
if ( ADP . IsCatchableExceptionType ( e ) ) {
CheckTransactionLevelAndZombie ( ) ;
if ( ! _disposing ) {
throw ;
}
}
else {
throw ;
}
}
}
finally {
Bid . ScopeLeave ( ref hscp ) ;
}
}
internal void Rollback ( string transactionName ) {
IntPtr hscp ;
Bid . ScopeEnter ( out hscp , "<sc.SqlInternalTransaction.Rollback|API> %d#, transactionName='%ls'" , ObjectID , transactionName ) ;
if ( _innerConnection . IsLockedForBulkCopy ) {
throw SQL . ConnectionLockedForBcpEvent ( ) ;
}
_innerConnection . ValidateConnectionForExecute ( null ) ;
try {
// ROLLBACK takes either a save point name or a transaction name. It will rollback the
// transaction to either the save point with the save point name or begin with the
// transacttion name. NOTE: for simplicity it is possible to give all save point names
// the same name, and ROLLBACK will simply rollback to the most recent save point with the
// save point name.
if ( ADP . IsEmpty ( transactionName ) )
throw SQL . NullEmptyTransactionName ( ) ;
try {
_innerConnection . ExecuteTransaction ( SqlInternalConnection . TransactionRequest . Rollback , transactionName , IsolationLevel . Unspecified , null , false ) ;
if ( ! IsZombied & & ! _innerConnection . IsYukonOrNewer ) {
// Check if Zombied before making round-trip to server.
// Against Yukon we receive an envchange on the ExecuteTransaction above on the
// parser that calls back into SqlTransaction for the Zombie() call.
CheckTransactionLevelAndZombie ( ) ;
}
}
catch ( Exception e ) {
//
if ( ADP . IsCatchableExceptionType ( e ) ) {
CheckTransactionLevelAndZombie ( ) ;
}
throw ;
}
}
finally {
Bid . ScopeLeave ( ref hscp ) ;
}
}
internal void Save ( string savePointName ) {
IntPtr hscp ;
Bid . ScopeEnter ( out hscp , "<sc.SqlInternalTransaction.Save|API> %d#, savePointName='%ls'" , ObjectID , savePointName ) ;
_innerConnection . ValidateConnectionForExecute ( null ) ;
try {
// ROLLBACK takes either a save point name or a transaction name. It will rollback the
// transaction to either the save point with the save point name or begin with the
// transacttion name. So, to rollback a nested transaction you must have a save point.
// SAVE TRANSACTION MUST HAVE AN ARGUMENT!!! Save Transaction without an arg throws an
// exception from the server. So, an overload for SaveTransaction without an arg doesn't make
// sense to have. Save Transaction does not affect the transaction level.
if ( ADP . IsEmpty ( savePointName ) )
throw SQL . NullEmptyTransactionName ( ) ;
try {
_innerConnection . ExecuteTransaction ( SqlInternalConnection . TransactionRequest . Save , savePointName , IsolationLevel . Unspecified , null , false ) ;
}
catch ( Exception e ) {
//
if ( ADP . IsCatchableExceptionType ( e ) ) {
CheckTransactionLevelAndZombie ( ) ;
}
throw ;
}
}
finally {
Bid . ScopeLeave ( ref hscp ) ;
}
}
internal void Zombie ( ) {
// Called by several places in the code to ensure that the outer
// transaction object has been zombied and the parser has broken
// it's reference to us.
// NOTE: we'll be called from the TdsParser when it gets appropriate
// ENVCHANGE events that indicate the transaction has completed, however
// we cannot rely upon those events occuring in the case of pre-Yukon
// servers (and when we don't go to the wire because the connection
// is broken) so we can also be called from the Commit/Rollback/Save
// methods to handle that case as well.
// There are two parts to a full zombie:
// 1) Zombie parent and disconnect outer transaction from internal transaction
// 2) Disconnect internal transaction from connection and parser
// Number 1 needs to be done whenever a SqlTransaction object is completed. Number
// 2 is only done when a transaction is actually completed. Since users can begin
// transactions both in and outside of the API, and since nested begins are not actual
// transactions we need to distinguish between #1 and #2. See SQL BU DT 291159
// for further details.
ZombieParent ( ) ;
SqlInternalConnection innerConnection = _innerConnection ;
_innerConnection = null ;
if ( null ! = innerConnection ) {
innerConnection . DisconnectTransaction ( this ) ;
}
}
private void ZombieParent ( ) {
if ( null ! = _parent ) {
SqlTransaction parent = ( SqlTransaction ) _parent . Target ;
if ( null ! = parent ) {
parent . Zombie ( ) ;
}
_parent = null ;
}
}
internal string TraceString ( ) {
return String . Format ( /*IFormatProvider*/ null , "(ObjId={0}, tranId={1}, state={2}, type={3}, open={4}, disp={5}" ,
ObjectID , _transactionId , _transactionState , _transactionType , _openResultCount , _disposing ) ;
}
}
}