File: System\Transactions\Oletx\OletxTransaction.cs
Web Access
Project: src\src\runtime\src\libraries\System.Transactions.Local\src\System.Transactions.Local.csproj (System.Transactions.Local)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Threading;
using System.Transactions.DtcProxyShim;

namespace System.Transactions.Oletx
{
    /// <summary>
    /// A Transaction object represents a single transaction.  It is created by TransactionManager
    /// objects through CreateTransaction or through deserialization.  Alternatively, the static Create
    /// method is provided, which creates a "default" TransactionManager and requests that it create
    /// a new transaction with default values.  A transaction can only be committed by
    /// the client application that created the transaction.  If a client application wishes to allow
    /// access to the transaction by multiple threads, but wants to prevent those other threads from
    /// committing the transaction, the application can make a "clone" of the transaction.  Transaction
    /// clones have the same capabilities as the original transaction, except for the ability to commit
    /// the transaction.
    /// </summary>
    [Serializable]
    internal class OletxTransaction : ISerializable
#pragma warning disable SYSLIB0050 // IObjectReference is obsolete
#pragma warning disable SA1001 // CommasMustBeSpacedCorrectly
        , IObjectReference
#pragma warning restore SA1001
#pragma warning restore SYSLIB0050
    {
        // We have a strong reference on realOletxTransaction which does the real work
        internal RealOletxTransaction RealOletxTransaction;

        // String that is used as a name for the propagationToken
        // while serializing and deserializing this object
        protected const string PropagationTokenString = "OletxTransactionPropagationToken";

        // When an OletxTransaction is being created via deserialization, this member is
        // filled with the propagation token from the serialization info.  Later, when
        // GetRealObject is called, this array is used to decide whether or not a new
        // transation needs to be created and if so, to create the transaction.
        private readonly byte[]? _propagationTokenForDeserialize;

        protected int Disposed;

        // In GetRealObject, we ask LTM if it has a promoted transaction with the same ID.  If it does,
        // we need to remember that transaction because GetRealObject is called twice during
        // deserialization.  In this case, GetRealObject returns the LTM transaction, not this OletxTransaction.
        // The OletxTransaction will get GC'd because there will be no references to it.
        internal Transaction? SavedLtmPromotedTransaction;

        private TransactionTraceIdentifier _traceIdentifier = TransactionTraceIdentifier.Empty;

        // Property
        internal RealOletxTransaction RealTransaction
            => RealOletxTransaction;

        internal Guid Identifier
        {
            get
            {
                TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Identifier)}");
                }

                Guid returnValue = RealOletxTransaction.Identifier;

                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Identifier)}");
                }

                return returnValue;
            }
        }

        internal Guid DistributedTxId
        {
            get
            {
                Guid returnValue = Guid.Empty;

                if (RealOletxTransaction != null && RealOletxTransaction.InternalTransaction != null)
                {
                    returnValue = RealOletxTransaction.InternalTransaction.DistributedTxId;
                }

                return returnValue;
            }
        }

        internal TransactionStatus Status
        {
            get
            {
                TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Status)}");
                }

                TransactionStatus returnValue = RealOletxTransaction.Status;

                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Status)}");
                }

                return returnValue;
            }
        }

        internal Exception? InnerException
            => RealOletxTransaction.InnerException;

        internal OletxTransaction(RealOletxTransaction realOletxTransaction)
        {
            RealOletxTransaction = realOletxTransaction;

            // Tell the realOletxTransaction that we are here.
            RealOletxTransaction.OletxTransactionCreated();
        }

        protected OletxTransaction(SerializationInfo? serializationInfo, StreamingContext context)
        {
            ArgumentNullException.ThrowIfNull(serializationInfo);

            // Simply store the propagation token from the serialization info.  GetRealObject will
            // decide whether or not we will use it.
            _propagationTokenForDeserialize = (byte[])serializationInfo.GetValue(PropagationTokenString, typeof(byte[]))!;

            if (_propagationTokenForDeserialize.Length < 24)
            {
                throw new ArgumentException(SR.InvalidArgument, nameof(serializationInfo));
            }

            RealOletxTransaction = null!;
        }

#pragma warning disable SYSLIB0050 // IObjectReference is obsolete
        public object GetRealObject(StreamingContext context)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(IObjectReference)}.{nameof(GetRealObject)}");
            }

            if (_propagationTokenForDeserialize == null)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.InternalError(SR.UnableToDeserializeTransaction);
                }

                throw TransactionException.Create(SR.UnableToDeserializeTransactionInternalError, null);
            }

            // This may be a second call.  If so, just return.
            if (SavedLtmPromotedTransaction != null)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(IObjectReference)}.{nameof(GetRealObject)}");
                }

                return SavedLtmPromotedTransaction;
            }

            Transaction returnValue = TransactionInterop.GetTransactionFromTransmitterPropagationToken(_propagationTokenForDeserialize);
            Debug.Assert(returnValue != null, "OletxTransaction.GetRealObject - GetTxFromPropToken returned null");

            SavedLtmPromotedTransaction = returnValue;

            if (etwLog.IsEnabled())
            {
                etwLog.TransactionDeserialized(returnValue._internalTransaction.PromotedTransaction!.TransactionTraceId);
            }

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(IObjectReference)}.{nameof(GetRealObject)}");
            }

            return returnValue;
        }
#pragma warning restore SYSLIB0050

        /// <summary>
        /// Implementation of IDisposable.Dispose. Releases managed, and unmanaged resources
        /// associated with the Transaction object.
        /// </summary>
        internal void Dispose()
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(IDisposable)}.{nameof(Dispose)}");
            }

            int localDisposed = Interlocked.CompareExchange(ref Disposed, 1, 0);
            if (localDisposed == 0)
            {
                RealOletxTransaction.OletxTransactionDisposed();
            }
            GC.SuppressFinalize(this);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(IDisposable)}.{nameof(Dispose)}");
            }
        }

        // Specific System.Transactions implementation

        /// <summary>
        /// Initiates commit processing of the transaction.  The caller must have created the transaction
        /// as a new transaction through TransactionManager.CreateTransaction or Transaction.Create.
        ///
        /// If the transaction is already aborted due to some other participant making a Rollback call,
        /// the transaction timeout period expiring, or some sort of network failure, an exception will
        /// be raised.
        /// </summary>
        /// <remarks>
        /// Initiates rollback processing of the transaction.  This method can be called on any instance
        /// of a Transaction class, regardless of how the Transaction was obtained.  It is possible for this
        /// method to be called "too late", after the outcome of the transaction has already been determined.
        /// In this case, an exception is raised.
        /// </remarks>
        internal void Rollback()
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Rollback)}");
                etwLog.TransactionRollback(TraceSourceType.TraceSourceOleTx, TransactionTraceId, "Transaction");
            }

            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            RealOletxTransaction.Rollback();

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(Rollback)}");
            }
        }

        internal IPromotedEnlistment EnlistVolatile(
            ISinglePhaseNotificationInternal singlePhaseNotification,
            EnlistmentOptions enlistmentOptions)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(EnlistVolatile)}(({nameof(ISinglePhaseNotificationInternal)}");
            }

            Debug.Assert(singlePhaseNotification != null, "Argument is null");
            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            if (RealOletxTransaction == null || RealOletxTransaction.TooLateForEnlistments)
            {
                throw TransactionException.Create(SR.TooLate, null, DistributedTxId);
            }

            IPromotedEnlistment enlistment = RealOletxTransaction.EnlistVolatile(
                singlePhaseNotification,
                enlistmentOptions,
                this);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(EnlistVolatile)}(({nameof(ISinglePhaseNotificationInternal)}");
            }

            return enlistment;
        }

        internal IPromotedEnlistment EnlistVolatile(
            IEnlistmentNotificationInternal enlistmentNotification,
            EnlistmentOptions enlistmentOptions)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(EnlistVolatile)}({nameof(IEnlistmentNotificationInternal)}");
            }

            Debug.Assert(enlistmentNotification != null, "Argument is null");
            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            if (RealOletxTransaction == null || RealOletxTransaction.TooLateForEnlistments)
            {
                throw TransactionException.Create(SR.TooLate, null, DistributedTxId);
            }

            IPromotedEnlistment enlistment = RealOletxTransaction.EnlistVolatile(
                enlistmentNotification,
                enlistmentOptions,
                this);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(EnlistVolatile)}({nameof(IEnlistmentNotificationInternal)}");
            }

            return enlistment;
        }

        internal IPromotedEnlistment EnlistDurable(
            Guid resourceManagerIdentifier,
            ISinglePhaseNotificationInternal singlePhaseNotification,
            bool canDoSinglePhase,
            EnlistmentOptions enlistmentOptions)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(
                    TraceSourceType.TraceSourceOleTx,
                    this,
                    $"{nameof(OletxTransaction)}.{nameof(EnlistDurable)}({nameof(ISinglePhaseNotificationInternal)})");
            }

            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            if (RealOletxTransaction == null || RealOletxTransaction.TooLateForEnlistments)
            {
                throw TransactionException.Create(SR.TooLate, null, DistributedTxId);
            }

            // get the Oletx TM from the real class
            OletxTransactionManager oletxTM = RealOletxTransaction.OletxTransactionManagerInstance;

            // get the resource manager from the Oletx TM
            OletxResourceManager rm = oletxTM.FindOrRegisterResourceManager(resourceManagerIdentifier);

            // ask the rm to do the durable enlistment
            OletxEnlistment enlistment = rm.EnlistDurable(
                this,
                canDoSinglePhase,
                singlePhaseNotification,
                enlistmentOptions);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(
                    TraceSourceType.TraceSourceOleTx,
                    this,
                    $"{nameof(OletxTransaction)}.{nameof(EnlistDurable)}({nameof(ISinglePhaseNotificationInternal)})");
            }

            return enlistment;
        }


        internal OletxDependentTransaction DependentClone(bool delayCommit)
        {
            OletxDependentTransaction dependentClone;

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(DependentClone)}");
            }

            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            if (TransactionStatus.Aborted == Status)
            {
                throw TransactionAbortedException.Create(
                    SR.TransactionAborted, RealOletxTransaction.InnerException, DistributedTxId);
            }
            if (TransactionStatus.InDoubt == Status)
            {
                throw TransactionInDoubtException.Create(
                    SR.TransactionIndoubt, RealOletxTransaction.InnerException, DistributedTxId);
            }
            if (TransactionStatus.Active != Status)
            {
                throw TransactionException.Create(SR.TransactionAlreadyOver, null, DistributedTxId);
            }

            dependentClone = new OletxDependentTransaction(RealOletxTransaction, delayCommit);

            if (etwLog.IsEnabled())
            {
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(DependentClone)}");
            }

            return dependentClone;

        }

        internal TransactionTraceIdentifier TransactionTraceId
        {
            get
            {
                if (_traceIdentifier == TransactionTraceIdentifier.Empty)
                {
                    lock (RealOletxTransaction)
                    {
                        if (_traceIdentifier == TransactionTraceIdentifier.Empty)
                        {
                            try
                            {
                                TransactionTraceIdentifier temp = new(RealOletxTransaction.Identifier.ToString(), 0);
                                Thread.MemoryBarrier();
                                _traceIdentifier = temp;
                            }
                            catch (TransactionException ex)
                            {
                                // realOletxTransaction.Identifier throws a TransactionException if it can't determine the guid of the
                                // transaction because the transaction was already committed or aborted before the RealOletxTransaction was
                                // created.  If that happens, we don't want to throw just because we are trying to trace.  So just use
                                // the TransactionTraceIdentifier.Empty.

                                TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                                if (etwLog.IsEnabled())
                                {
                                    etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                                }
                            }

                        }
                    }
                }
                return _traceIdentifier;
            }
        }

        public void GetObjectData(SerializationInfo serializationInfo, StreamingContext context)
        {
            ArgumentNullException.ThrowIfNull(serializationInfo);

            byte[] propagationToken;

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(GetObjectData)}");
            }

            Debug.Assert(Disposed == 0, "OletxTransction object is disposed");

            propagationToken = TransactionInterop.GetTransmitterPropagationToken(this);

            serializationInfo.SetType(typeof(OletxTransaction));
            serializationInfo.AddValue(PropagationTokenString, propagationToken);

            if (etwLog.IsEnabled())
            {
                etwLog.TransactionSerialized(TransactionTraceId);
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxTransaction)}.{nameof(GetObjectData)}");
            }
        }

        public virtual IsolationLevel IsolationLevel
            => RealOletxTransaction.TransactionIsolationLevel;
    }

    // Internal class used by OletxTransaction class which is public
    internal sealed class RealOletxTransaction
    {
        // Transaction manager
        internal OletxTransactionManager OletxTransactionManagerInstance { get; }

        private readonly TransactionShim? _transactionShim;

        // guid related to transaction
        internal Guid TxGuid { get; private set; }

        // Isolation level of the transaction
        internal IsolationLevel TransactionIsolationLevel { get; private set; }

        // Record the exception that caused the transaction to abort.
        internal Exception? InnerException;

        // Store status
        internal TransactionStatus Status { get; private set; }

        // This is the count of undisposed OletxTransaction objects that reference
        // this RealOletxTransaction.  This is incremented when an OletxTransaction is created
        // and decremented when OletxTransactionDisposed is
        // called.  When it is decremented to zero, the transactionShim
        // field is "released", thus releasing the unmanged proxy interface
        // pointer.
        private int _undisposedOletxTransactionCount;

        // The list of containers for phase0 volatile enlistment multiplexing so we only enlist with the proxy once per wave.
        // The last one on the list is the "current" one.
        internal ArrayList? Phase0EnlistVolatilementContainerList;

        // The container for phase1 volatile enlistment multiplexing so we only enlist with the proxy once.
        internal OletxPhase1VolatileEnlistmentContainer? Phase1EnlistVolatilementContainer;

        // Used to get outcomes of transactions with a voter.
        private readonly OutcomeEnlistment? _outcomeEnlistment;

        // This is a count of volatile and Phase0 durable enlistments on this transaction that have not yet voted.
        // This is incremented when an enlistment is made and decremented when the
        // enlistment votes.  It is checked in Rollback.  If the count is greater than 0,
        // then the doomed field is set to true and the Rollback is allowed.  If the count
        // is zero in Rollback, the rollback is rejected with a "too late" exception.
        // All checking and modification of this field needs to be done under a lock( this ).
        private int _undecidedEnlistmentCount;

        // If true, indicates that the transaction should NOT commit.  This is set to
        // true if Rollback is called when there are outstanding enlistments.  This is
        // checked when enlistments vote Prepared.  If true, then the enlistment's vote
        // is turned into a ForceRollback.  All checking and modification of this field
        // needs to be done under a lock (this).
        internal bool Doomed { get; private set; }

        // This property is used to allocate enlistment identifiers for enlistment trace identifiers.
        // It is only incremented when a new enlistment is created for this instance of RealOletxTransaction.
        // Enlistments on all clones of this Real transaction use this value.
        internal int _enlistmentCount;

        private readonly DateTime _creationTime;
        private readonly DateTime _lastStateChangeTime;
        private TransactionTraceIdentifier _traceIdentifier = TransactionTraceIdentifier.Empty;

        // This field is set directly from the OletxCommittableTransaction constructor.  It will be null
        // for non-root RealOletxTransactions.
        internal OletxCommittableTransaction? CommittableTransaction;

        // This is an internal OletxTransaction.  It is created as part of the RealOletxTransaction constructor.
        // It is used by the DependentCloneEnlistments when creating their volatile enlistments.
        internal OletxTransaction InternalClone;

        // This is set initialized to false.  It is set to true when the OletxPhase1VolatileContainer gets a VoteRequest or
        // when any OletxEnlistment attached to this transaction gets a PrepareRequest.  At that point, it is too late for any
        // more enlistments.
        internal bool TooLateForEnlistments { get; set; }

        // This is the InternalTransaction that instigated creation of this RealOletxTransaction.  When we get the outcome
        // of the transaction, we use this to notify the InternalTransaction of the outcome.  We do this to avoid the LTM
        // always creating a volatile enlistment just to get the outcome.
        internal InternalTransaction? InternalTransaction { get; set; }

        internal Guid Identifier
        {
            get
            {
                // The txGuid will be empty if the oletx transaction was already committed or aborted when we
                // tried to create the RealOletxTransaction.  We still allow creation of the RealOletxTransaction
                // for COM+ interop purposes, but we can't get the guid or the status of the transaction.
                if (TxGuid.Equals(Guid.Empty))
                {
                    throw TransactionException.Create(SR.CannotGetTransactionIdentifier, null);
                }

                return TxGuid;
            }
        }

        internal Guid DistributedTxId
        {
            get
            {
                Guid returnValue = Guid.Empty;

                if (InternalTransaction != null)
                {
                    returnValue = InternalTransaction.DistributedTxId;
                }

                return returnValue;
            }
        }

        internal void IncrementUndecidedEnlistments()
        {
            // Avoid taking a lock on the transaction here.  Decrement
            // will be called by a thread owning a lock on enlistment
            // containers.  When creating new enlistments the transaction
            // will attempt to get a lock on the container when it
            // already holds a lock on the transaction.  This can result
            // in a deadlock.
            Interlocked.Increment(ref _undecidedEnlistmentCount);
        }

        internal void DecrementUndecidedEnlistments()
        {
            // Avoid taking a lock on the transaction here.  Decrement
            // will be called by a thread owning a lock on enlistment
            // containers.  When creating new enlistments the transaction
            // will attempt to get a lock on the container when it
            // already holds a lock on the transaction.  This can result
            // in a deadlock.
            Interlocked.Decrement(ref _undecidedEnlistmentCount);
        }

        internal int UndecidedEnlistments
            => _undecidedEnlistmentCount;

        internal TransactionShim TransactionShim
        {
            get
            {
                TransactionShim? shim = _transactionShim;
                if (shim == null)
                {
                    throw TransactionInDoubtException.Create(SR.TransactionIndoubt, null, DistributedTxId);
                }

                return shim;
            }
        }

        // Common constructor used by all types of constructors
        // Create a clean and fresh transaction.
        internal RealOletxTransaction(
            OletxTransactionManager transactionManager,
            TransactionShim? transactionShim,
            OutcomeEnlistment? outcomeEnlistment,
            Guid identifier,
            OletxTransactionIsolationLevel oletxIsoLevel)
        {
            bool successful = false;

            try
            {
                // initialize the member fields
                OletxTransactionManagerInstance = transactionManager;
                _transactionShim = transactionShim;
                _outcomeEnlistment = outcomeEnlistment;
                TxGuid = identifier;
                TransactionIsolationLevel = OletxTransactionManager.ConvertIsolationLevelFromProxyValue(oletxIsoLevel);
                Status = TransactionStatus.Active;
                _undisposedOletxTransactionCount = 0;
                Phase0EnlistVolatilementContainerList = null;
                Phase1EnlistVolatilementContainer = null;
                TooLateForEnlistments = false;
                InternalTransaction = null;

                _creationTime = DateTime.UtcNow;
                _lastStateChangeTime = _creationTime;

                // Connect this object with the OutcomeEnlistment.
                InternalClone = new OletxTransaction(this);

                // We have have been created without an outcome enlistment if it was too late to create
                // a clone from the ITransactionNative that we were created from.
                if (_outcomeEnlistment != null)
                {
                    _outcomeEnlistment.SetRealTransaction(this);
                }
                else
                {
                    Status = TransactionStatus.InDoubt;
                }

                successful = true;
            }
            finally
            {
                if (!successful)
                {
                    if (_outcomeEnlistment != null)
                    {
                        _outcomeEnlistment.UnregisterOutcomeCallback();
                        _outcomeEnlistment = null;
                    }
                }
            }
        }

        internal OletxVolatileEnlistmentContainer AddDependentClone(bool delayCommit)
        {
            Phase0EnlistmentShim? phase0Shim = null;
            VoterBallotShim? voterShim = null;
            bool needVoterEnlistment = false;
            bool needPhase0Enlistment = false;
            OletxVolatileEnlistmentContainer? returnValue = null;
            OletxPhase0VolatileEnlistmentContainer? localPhase0VolatileContainer = null;
            OletxPhase1VolatileEnlistmentContainer? localPhase1VolatileContainer = null;
            bool phase0ContainerLockAcquired = false;

            // Yes, we are talking to the proxy while holding the lock on the RealOletxTransaction.
            // If we don't then things get real sticky with other threads allocating containers.
            // We only do this the first time we get a depenent clone of a given type (delay vs. non-delay).
            // After that, we don't create a new container, except for Phase0 if we need to create one
            // for a second wave.
            try
            {
                lock (this)
                {
                    if (delayCommit)
                    {
                        // Not using a MemoryBarrier because all access to this member variable is under a lock of the
                        // object.
                        Phase0EnlistVolatilementContainerList ??= new ArrayList(1);

                        // We may have failed the proxy enlistment for the first container, but we would have
                        // allocated the list.  That is why we have this check here.
                        if (Phase0EnlistVolatilementContainerList.Count == 0)
                        {
                            localPhase0VolatileContainer = new OletxPhase0VolatileEnlistmentContainer(this);
                            needPhase0Enlistment = true;
                        }
                        else
                        {
                            localPhase0VolatileContainer = Phase0EnlistVolatilementContainerList[^1] as OletxPhase0VolatileEnlistmentContainer;

                            if (localPhase0VolatileContainer != null)
                            {
                                TakeContainerLock(localPhase0VolatileContainer, ref phase0ContainerLockAcquired);
                            }

                            if (!localPhase0VolatileContainer!.NewEnlistmentsAllowed)
                            {
                                //It is OK to release the lock at this time because we are creating a new container that has not yet
                                //been enlisted with DTC. So there is no race to worry about
                                ReleaseContainerLock(localPhase0VolatileContainer, ref phase0ContainerLockAcquired);

                                localPhase0VolatileContainer = new OletxPhase0VolatileEnlistmentContainer(this);
                                needPhase0Enlistment = true;
                            }
                            else
                            {
                                needPhase0Enlistment = false;
                            }
                        }
                    }
                    else // ! delayCommit
                    {
                        if (Phase1EnlistVolatilementContainer == null)
                        {
                            localPhase1VolatileContainer = new OletxPhase1VolatileEnlistmentContainer(this);
                            needVoterEnlistment = true;
                        }
                        else
                        {
                            needVoterEnlistment = false;
                            localPhase1VolatileContainer = Phase1EnlistVolatilementContainer;
                        }
                    }

                    try
                    {
                        //At this point, we definitely need the lock on the phase0 container so that it doesnt race with shim notifications from unmanaged code
                        //corrupting state while we are in the middle of an AddDependentClone processing
                        if (localPhase0VolatileContainer != null)
                        {
                            TakeContainerLock(localPhase0VolatileContainer, ref phase0ContainerLockAcquired);
                        }

                        // If enlistDuringPrepareRequired is true, we need to ask the proxy to create a Phase0 enlistment.
                        if (needPhase0Enlistment)
                        {
                            _transactionShim!.Phase0Enlist(localPhase0VolatileContainer!, out phase0Shim);
                            localPhase0VolatileContainer!.Phase0EnlistmentShim = phase0Shim;
                        }

                        if (needVoterEnlistment)
                        {
                            // We need to use shims if native threads are not allowed to enter managed code.
                            OletxTransactionManagerInstance.DtcTransactionManagerLock.AcquireReaderLock(-1);
                            try
                            {
                                _transactionShim!.CreateVoter(localPhase1VolatileContainer!, out voterShim);
                            }
                            finally
                            {
                                OletxTransactionManagerInstance.DtcTransactionManagerLock.ReleaseReaderLock();
                            }

                            localPhase1VolatileContainer!.VoterBallotShim = voterShim;
                        }

                        if (delayCommit)
                        {
                            // if we needed a Phase0 enlistment, we need to add the container to the
                            // list.
                            if (needPhase0Enlistment)
                            {
                                Phase0EnlistVolatilementContainerList!.Add(localPhase0VolatileContainer);
                            }
                            localPhase0VolatileContainer!.AddDependentClone();
                            returnValue = localPhase0VolatileContainer;
                        }
                        else
                        {
                            // If we needed a voter enlistment, we need to save the container as THE
                            // phase1 container for this transaction.
                            if (needVoterEnlistment)
                            {
                                Debug.Assert(Phase1EnlistVolatilementContainer == null,
                                    "RealOletxTransaction.AddDependentClone - phase1VolContainer not null when expected");
                                Phase1EnlistVolatilementContainer = localPhase1VolatileContainer;
                            }
                            localPhase1VolatileContainer!.AddDependentClone();
                            returnValue = localPhase1VolatileContainer;
                        }

                    }
                    catch (COMException comException)
                    {
                        OletxTransactionManager.ProxyException(comException);
                        throw;
                    }
                }
            }
            finally
            {
                //First release the lock on the phase 0 container if it was acquired. Any work on localPhase0VolatileContainer
                //that needs its state to be consistent while processing should do so before this statement is executed.
                if (localPhase0VolatileContainer != null)
                {
                    ReleaseContainerLock(localPhase0VolatileContainer, ref phase0ContainerLockAcquired);
                }
            }
            return returnValue;
        }

        private static void ReleaseContainerLock(OletxPhase0VolatileEnlistmentContainer localPhase0VolatileContainer, ref bool phase0ContainerLockAcquired)
        {
            if (phase0ContainerLockAcquired)
            {
                Monitor.Exit(localPhase0VolatileContainer);
                phase0ContainerLockAcquired = false;
            }
        }

        private static void TakeContainerLock(OletxPhase0VolatileEnlistmentContainer localPhase0VolatileContainer, ref bool phase0ContainerLockAcquired)
        {
            if (!phase0ContainerLockAcquired)
            {
                Monitor.Enter(localPhase0VolatileContainer);
                phase0ContainerLockAcquired = true;
            }
        }

        internal IPromotedEnlistment CommonEnlistVolatile(
            IEnlistmentNotificationInternal enlistmentNotification,
            EnlistmentOptions enlistmentOptions,
            OletxTransaction oletxTransaction)
        {
            OletxVolatileEnlistment? enlistment = null;
            bool needVoterEnlistment = false;
            bool needPhase0Enlistment = false;
            OletxPhase0VolatileEnlistmentContainer? localPhase0VolatileContainer = null;
            OletxPhase1VolatileEnlistmentContainer? localPhase1VolatileContainer = null;
            VoterBallotShim? voterShim = null;
            Phase0EnlistmentShim? phase0Shim = null;

            // Yes, we are talking to the proxy while holding the lock on the RealOletxTransaction.
            // If we don't then things get real sticky with other threads allocating containers.
            // We only do this the first time we get a depenent clone of a given type (delay vs. non-delay).
            // After that, we don't create a new container, except for Phase0 if we need to create one
            // for a second wave.
            lock (this)
            {
                enlistment = new OletxVolatileEnlistment(
                    enlistmentNotification,
                    enlistmentOptions,
                    oletxTransaction);

                if ((enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0)
                {
                    // Not using a MemoryBarrier because all access to this member variable is done when holding
                    // a lock on the object.
                    Phase0EnlistVolatilementContainerList ??= new ArrayList(1);
                    // We may have failed the proxy enlistment for the first container, but we would have
                    // allocated the list.  That is why we have this check here.
                    if (Phase0EnlistVolatilementContainerList.Count == 0)
                    {
                        localPhase0VolatileContainer = new OletxPhase0VolatileEnlistmentContainer(this);
                        needPhase0Enlistment = true;
                    }
                    else
                    {
                        localPhase0VolatileContainer = Phase0EnlistVolatilementContainerList[^1] as OletxPhase0VolatileEnlistmentContainer;
                        if (!localPhase0VolatileContainer!.NewEnlistmentsAllowed)
                        {
                            localPhase0VolatileContainer = new OletxPhase0VolatileEnlistmentContainer(this);
                            needPhase0Enlistment = true;
                        }
                        else
                        {
                            needPhase0Enlistment = false;
                        }
                    }
                }
                else  // not EDPR = TRUE - may need a voter...
                {
                    if (Phase1EnlistVolatilementContainer == null)
                    {
                        needVoterEnlistment = true;
                        localPhase1VolatileContainer = new OletxPhase1VolatileEnlistmentContainer(this);
                    }
                    else
                    {
                        needVoterEnlistment = false;
                        localPhase1VolatileContainer = Phase1EnlistVolatilementContainer;
                    }
                }

                try
                {
                    // If enlistDuringPrepareRequired is true, we need to ask the proxy to create a Phase0 enlistment.
                    if (needPhase0Enlistment)
                    {
                        lock (localPhase0VolatileContainer!)
                        {
                            _transactionShim!.Phase0Enlist(localPhase0VolatileContainer, out phase0Shim);

                            localPhase0VolatileContainer.Phase0EnlistmentShim = phase0Shim;
                        }
                    }

                    if (needVoterEnlistment)
                    {
                        _transactionShim!.CreateVoter(localPhase1VolatileContainer!, out voterShim);

                        localPhase1VolatileContainer!.VoterBallotShim = voterShim;
                    }

                    if ((enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0)
                    {
                        localPhase0VolatileContainer!.AddEnlistment(enlistment);
                        if (needPhase0Enlistment)
                        {
                            Phase0EnlistVolatilementContainerList!.Add(localPhase0VolatileContainer);
                        }
                    }
                    else
                    {
                        localPhase1VolatileContainer!.AddEnlistment(enlistment);

                        if (needVoterEnlistment)
                        {
                            Debug.Assert(Phase1EnlistVolatilementContainer == null,
                                "RealOletxTransaction.CommonEnlistVolatile - phase1VolContainer not null when expected.");
                            Phase1EnlistVolatilementContainer = localPhase1VolatileContainer;
                        }
                    }
                }
                catch (COMException comException)
                {
                    OletxTransactionManager.ProxyException(comException);
                    throw;
                }
            }

            return enlistment;
        }

        internal IPromotedEnlistment EnlistVolatile(
            ISinglePhaseNotificationInternal enlistmentNotification,
            EnlistmentOptions enlistmentOptions,
            OletxTransaction oletxTransaction)
            => CommonEnlistVolatile(
                enlistmentNotification,
                enlistmentOptions,
                oletxTransaction);

        internal IPromotedEnlistment EnlistVolatile(
            IEnlistmentNotificationInternal enlistmentNotification,
            EnlistmentOptions enlistmentOptions,
            OletxTransaction oletxTransaction)
            => CommonEnlistVolatile(
                enlistmentNotification,
                enlistmentOptions,
                oletxTransaction);

        internal void Commit()
        {
            try
            {
                _transactionShim!.Commit();
            }
            catch (COMException comException)
            {
                if (comException.ErrorCode == OletxHelper.XACT_E_ABORTED ||
                    comException.ErrorCode == OletxHelper.XACT_E_INDOUBT)
                {
                    Interlocked.CompareExchange(ref InnerException, comException, null);

                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, comException);
                    }
                }
                else if (comException.ErrorCode == OletxHelper.XACT_E_ALREADYINPROGRESS)
                {
                    throw TransactionException.Create(SR.TransactionAlreadyOver, comException);
                }
                else
                {
                    OletxTransactionManager.ProxyException(comException);
                    throw;
                }
            }
        }

        internal void Rollback()
        {
            Guid tempGuid = Guid.Empty;

            lock (this)
            {
                // if status is not active and not aborted, then throw an exception
                if (TransactionStatus.Aborted != Status &&
                    TransactionStatus.Active != Status)
                {
                    throw TransactionException.Create(SR.TransactionAlreadyOver, null, DistributedTxId);
                }

                // If the transaciton is already aborted, we can get out now.  Calling Rollback on an already aborted transaction
                // is legal.
                if (TransactionStatus.Aborted == Status)
                {
                    return;
                }

                // If there are still undecided enlistments, we can doom the transaction.
                // We can safely make this check because we ALWAYS have a Phase1 Volatile enlistment to
                // get the outcome.  If we didn't have that enlistment, we would not be able to do this
                // because not all instances of RealOletxTransaction would have enlistments.
                if (_undecidedEnlistmentCount > 0)
                {
                    Doomed = true;
                }
                else if (TooLateForEnlistments)
                {
                    // It's too late for rollback to be called here.
                    throw TransactionException.Create(SR.TransactionAlreadyOver, null, DistributedTxId);
                }

                // Tell the volatile enlistment containers to vote no now if they have outstanding
                // notifications.
                if (Phase0EnlistVolatilementContainerList != null)
                {
                    foreach (OletxPhase0VolatileEnlistmentContainer phase0VolatileContainer in Phase0EnlistVolatilementContainerList)
                    {
                        phase0VolatileContainer.RollbackFromTransaction();
                    }
                }
                Phase1EnlistVolatilementContainer?.RollbackFromTransaction();
            }

            try
            {
                _transactionShim!.Abort();
            }
            catch (COMException comException)
            {
                // If the ErrorCode is XACT_E_ALREADYINPROGRESS and the transaciton is already doomed, we must be
                // the root transaction and we have already called Commit - ignore the exception.  The
                // Rollback is allowed and one of the enlistments that hasn't voted yet will make sure it is
                // aborted.
                if (comException.ErrorCode == OletxHelper.XACT_E_ALREADYINPROGRESS)
                {
                    if (Doomed)
                    {
                        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                        if (etwLog.IsEnabled())
                        {
                            etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, comException);
                        }
                    }
                    else
                    {
                        throw TransactionException.Create(SR.TransactionAlreadyOver, comException, DistributedTxId);
                    }
                }
                else
                {
                    // Otherwise, throw the exception out to the app.
                    OletxTransactionManager.ProxyException(comException);

                    throw;
                }
            }
        }

        internal void OletxTransactionCreated()
            => Interlocked.Increment(ref _undisposedOletxTransactionCount);

        internal void OletxTransactionDisposed()
        {
            int localCount = Interlocked.Decrement(ref _undisposedOletxTransactionCount);
            Debug.Assert(localCount >= 0, "RealOletxTransction.undisposedOletxTransationCount < 0");
        }

        internal void FireOutcome(TransactionStatus statusArg)
        {
            lock (this)
            {
                if (statusArg == TransactionStatus.Committed)
                {
                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.TransactionCommitted(TraceSourceType.TraceSourceOleTx, TransactionTraceId);
                    }

                    Status = TransactionStatus.Committed;
                }
                else if (statusArg == TransactionStatus.Aborted)
                {
                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.TransactionAborted(TraceSourceType.TraceSourceOleTx, TransactionTraceId);
                    }

                    Status = TransactionStatus.Aborted;
                }
                else
                {
                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.TransactionInDoubt(TraceSourceType.TraceSourceOleTx, TransactionTraceId);
                    }

                    Status = TransactionStatus.InDoubt;
                }
            }

#pragma warning disable IDE0031 // Null check can be simplified
            // Let the InternalTransaciton know about the outcome.
            if (InternalTransaction != null)
            {
                InternalTransaction.DistributedTransactionOutcome(InternalTransaction, Status);
            }
#pragma warning restore IDE0031
        }

        internal TransactionTraceIdentifier TransactionTraceId
        {
            get
            {
                if (TransactionTraceIdentifier.Empty == _traceIdentifier)
                {
                    lock (this)
                    {
                        if (_traceIdentifier == TransactionTraceIdentifier.Empty)
                        {
                            if (TxGuid != Guid.Empty)
                            {
                                TransactionTraceIdentifier temp = new(TxGuid.ToString(), 0);
                                Thread.MemoryBarrier();
                                _traceIdentifier = temp;
                            }
                            else
                            {
                                // We don't have a txGuid if we couldn't determine the guid of the
                                // transaction because the transaction was already committed or aborted before the RealOletxTransaction was
                                // created.  If that happens, we don't want to throw just because we are trying to trace.  So just use the
                                // TransactionTraceIdentifier.Empty.
                            }
                        }
                    }
                }
                return _traceIdentifier;
            }
        }

        internal void TMDown()
        {
            lock (this)
            {
                // Tell the volatile enlistment containers that the TM went down.
                if (Phase0EnlistVolatilementContainerList != null)
                {
                    foreach (OletxPhase0VolatileEnlistmentContainer phase0VolatileContainer in Phase0EnlistVolatilementContainerList)
                    {
                        phase0VolatileContainer.TMDown();
                    }
                }
            }
            // Tell the outcome enlistment the TM went down.  We are doing this outside the lock
            // because this may end up making calls out to user code through enlistments.
            _outcomeEnlistment!.TMDown();
        }
    }

    internal sealed class OutcomeEnlistment
    {
        private WeakReference? _weakRealTransaction;

        internal Guid TransactionIdentifier { get; private set; }

        private bool _haveIssuedOutcome;

        private TransactionStatus _savedStatus;

        internal OutcomeEnlistment()
        {
            _haveIssuedOutcome = false;
            _savedStatus = TransactionStatus.InDoubt;
        }

        internal void SetRealTransaction(RealOletxTransaction realTx)
        {
            bool localHaveIssuedOutcome = false;
            TransactionStatus localStatus = TransactionStatus.InDoubt;

            lock (this)
            {
                localHaveIssuedOutcome = _haveIssuedOutcome;
                localStatus = _savedStatus;

                // We want to do this while holding the lock.
                if (!localHaveIssuedOutcome)
                {
                    // We don't use MemoryBarrier here because all access to these member variables is done while holding
                    // a lock on the object.

                    // We are going to use a weak reference so the transaction object can get garbage
                    // collected before we receive the outcome.
                    _weakRealTransaction = new WeakReference(realTx);

                    // Save the transaction guid so that the transaction can be removed from the
                    // TransactionTable
                    TransactionIdentifier = realTx.TxGuid;
                }
            }

            // We want to do this outside the lock because we are potentially calling out to user code.
            if (localHaveIssuedOutcome)
            {
                realTx.FireOutcome(localStatus);

                // We may be getting this notification while there are still volatile prepare notifications outstanding.  Tell the
                // container to drive the aborted notification in that case.
                if (localStatus is TransactionStatus.Aborted or TransactionStatus.InDoubt &&
                   realTx.Phase1EnlistVolatilementContainer != null)
                {
                    realTx.Phase1EnlistVolatilementContainer.OutcomeFromTransaction(localStatus);
                }
            }
        }

        internal void UnregisterOutcomeCallback()
        {
            _weakRealTransaction = null;
        }

        private void InvokeOutcomeFunction(TransactionStatus status)
        {
            WeakReference? localTxWeakRef;

            // In the face of TMDown notifications, we may have already issued
            // the outcome of the transaction.
            lock (this)
            {
                if (_haveIssuedOutcome)
                {
                    return;
                }
                _haveIssuedOutcome = true;
                _savedStatus = status;
                localTxWeakRef = _weakRealTransaction;
            }

            // It is possible for the weakRealTransaction member to be null if some exception was thrown
            // during the RealOletxTransaction constructor after the OutcomeEnlistment object was created.
            // In the finally block of the constructor, it calls UnregisterOutcomeCallback, which will
            // null out weakRealTransaction.  If this is the case, there is nothing to do.
            if (localTxWeakRef != null)
            {
                if (localTxWeakRef.Target is RealOletxTransaction realOletxTransaction)
                {
                    realOletxTransaction.FireOutcome(status);

                    // The container list won't be changing on us now because the transaction status has changed such that
                    // new enlistments will not be created.
                    // Tell the Phase0Volatile containers, if any, about the outcome of the transaction.
                    // I am not protecting the access to phase0EnlistVolatilementContainerList with a lock on "this"
                    // because it is too late for these to be allocated anyway.
                    if (realOletxTransaction.Phase0EnlistVolatilementContainerList != null)
                    {
                        foreach (OletxPhase0VolatileEnlistmentContainer phase0VolatileContainer in realOletxTransaction.Phase0EnlistVolatilementContainerList)
                        {
                            phase0VolatileContainer.OutcomeFromTransaction(status);
                        }
                    }

                    // We may be getting this notification while there are still volatile prepare notifications outstanding.  Tell the
                    // container to drive the aborted notification in that case.
                    if (status is TransactionStatus.Aborted or TransactionStatus.InDoubt &&
                           realOletxTransaction.Phase1EnlistVolatilementContainer != null)
                    {
                        realOletxTransaction.Phase1EnlistVolatilementContainer.OutcomeFromTransaction(status);
                    }
                }

                localTxWeakRef.Target = null;
            }
        }

        //
        // We need to figure out if the transaction is InDoubt as a result of TMDown.  This
        // can happen for a number of reasons.  For instance we have responded prepared
        // to all of our enlistments or we have no enlistments.
        //
        internal static bool TransactionIsInDoubt(RealOletxTransaction realTx)
        {
            if (realTx.CommittableTransaction is { CommitCalled: false })
            {
                // If this is a committable transaction and commit has not been called
                // then we know the outcome.
                return false;
            }

            return realTx.UndecidedEnlistments == 0;
        }

        internal void TMDown()
        {
            // Assume that we don't know because that is the safest answer.
            bool transactionIsInDoubt = true;
            RealOletxTransaction? realOletxTransaction = null;
            lock (this)
            {
                if (_weakRealTransaction != null)
                {
                    realOletxTransaction = _weakRealTransaction.Target as RealOletxTransaction;
                }
            }

            if (realOletxTransaction != null)
            {
                lock (realOletxTransaction)
                {
                    transactionIsInDoubt = TransactionIsInDoubt(realOletxTransaction);
                }
            }

            // If we have already voted, then we can't tell what the outcome
            // is.  We do this outside the lock because it may end up invoking user
            // code when it calls into the enlistments later on the stack.
            if (transactionIsInDoubt)
            {
                InDoubt();
            }
            // We have not yet voted, so just say it aborted.
            else
            {
                Aborted();
            }
        }

        #region ITransactionOutcome Members

        public void Committed()
            => InvokeOutcomeFunction(TransactionStatus.Committed);

        public void Aborted()
            => InvokeOutcomeFunction(TransactionStatus.Aborted);

        public void InDoubt()
            => InvokeOutcomeFunction(TransactionStatus.InDoubt);

        #endregion
    }
}