File: System\Transactions\Oletx\OletxEnlistment.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.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Transactions.DtcProxyShim;

namespace System.Transactions.Oletx;

internal sealed class OletxEnlistment : OletxBaseEnlistment, IPromotedEnlistment
{
    internal enum OletxEnlistmentState
    {
        Active,
        Phase0Preparing,
        Preparing,
        SinglePhaseCommitting,
        Prepared,
        Committing,
        Committed,
        Aborting,
        Aborted,
        InDoubt,
        Done
    }

    private Phase0EnlistmentShim? _phase0Shim;
    private readonly bool _canDoSinglePhase;
    private IEnlistmentNotificationInternal? _iEnlistmentNotification;
    // The information that comes from/goes to the proxy.
    private byte[]? _proxyPrepareInfoByteArray;

    private bool _isSinglePhase;
    private readonly Guid _transactionGuid = Guid.Empty;

    // Set to true if we receive an AbortRequest while we still have
    // another notification, like prepare, outstanding.  It indicates that
    // we need to fabricate a rollback to the app after it responds to Prepare.
    private bool _fabricateRollback;

    private bool _tmWentDown;
    private bool _aborting;

    private byte[]? _prepareInfoByteArray;

    internal Guid TransactionIdentifier => _transactionGuid;

    #region Constructor

    internal OletxEnlistment(
        bool canDoSinglePhase,
        IEnlistmentNotificationInternal enlistmentNotification,
        Guid transactionGuid,
        EnlistmentOptions enlistmentOptions,
        OletxResourceManager oletxResourceManager,
        OletxTransaction oletxTransaction)
        : base(oletxResourceManager, oletxTransaction)
    {
        // This will get set later by the creator of this object after it
        // has enlisted with the proxy.
        EnlistmentShim = null;
        _phase0Shim = null;

        _canDoSinglePhase = canDoSinglePhase;
        _iEnlistmentNotification = enlistmentNotification;
        State = OletxEnlistmentState.Active;
        _transactionGuid = transactionGuid;

        _proxyPrepareInfoByteArray = null;

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.EnlistmentCreated(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, EnlistmentType.Durable, enlistmentOptions);
        }

        // Always do this last in case anything earlier fails.
        AddToEnlistmentTable();
    }

    internal OletxEnlistment(
        IEnlistmentNotificationInternal enlistmentNotification,
        OletxTransactionStatus xactStatus,
        byte[] prepareInfoByteArray,
        OletxResourceManager oletxResourceManager)
        : base(oletxResourceManager, null)
    {
        // This will get set later by the creator of this object after it
        // has enlisted with the proxy.
        EnlistmentShim = null;
        _phase0Shim = null;

        _canDoSinglePhase = false;
        _iEnlistmentNotification = enlistmentNotification;
        State = OletxEnlistmentState.Active;

        // Do this before we do any tracing because it will affect the trace identifiers that we generate.
        Debug.Assert(prepareInfoByteArray != null,
            "OletxEnlistment.ctor - null oletxTransaction without a prepareInfoByteArray");

        int prepareInfoLength = prepareInfoByteArray.Length;
        _proxyPrepareInfoByteArray = new byte[prepareInfoLength];
        Array.Copy(prepareInfoByteArray, _proxyPrepareInfoByteArray, prepareInfoLength);

        _transactionGuid = new Guid(_proxyPrepareInfoByteArray.AsSpan(0, 16));
        TransactionGuidString = _transactionGuid.ToString();

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;

        // If this is being created as part of a Reenlist and we already know the
        // outcome, then tell the application.
        switch (xactStatus)
        {
            case OletxTransactionStatus.OLETX_TRANSACTION_STATUS_ABORTED:
                {
                    State = OletxEnlistmentState.Aborting;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Rollback);
                    }

                    _iEnlistmentNotification.Rollback(this);
                    break;
                }

            case OletxTransactionStatus.OLETX_TRANSACTION_STATUS_COMMITTED:
                {
                    State = OletxEnlistmentState.Committing;
                    // We are going to send the notification to the RM.  We need to put the
                    // enlistment on the reenlistPendingList.  We lock the reenlistList because
                    // we have decided that is the lock that protects both lists.  The entry will
                    // be taken off the reenlistPendingList when the enlistment has
                    // EnlistmentDone called on it.  The enlistment will call
                    // RemoveFromReenlistPending.
                    lock (oletxResourceManager.ReenlistList)
                    {
                        oletxResourceManager.ReenlistPendingList.Add(this);
                    }

                    if (etwLog.IsEnabled())
                    {
                        etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Commit);
                    }

                    _iEnlistmentNotification.Commit(this);
                    break;
                }

            case OletxTransactionStatus.OLETX_TRANSACTION_STATUS_PREPARED:
                {
                    State = OletxEnlistmentState.Prepared;
                    lock (oletxResourceManager.ReenlistList)
                    {
                        oletxResourceManager.ReenlistList.Add(this);
                        oletxResourceManager.StartReenlistThread();
                    }
                    break;
                }

            default:
                {
                    if (etwLog.IsEnabled())
                    {
                        etwLog.InternalError(SR.OletxEnlistmentUnexpectedTransactionStatus);
                    }

                    throw TransactionException.Create(
                        SR.OletxEnlistmentUnexpectedTransactionStatus, null, DistributedTxId);
                }
        }

        if (etwLog.IsEnabled())
        {
            etwLog.EnlistmentCreated(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, EnlistmentType.Durable, EnlistmentOptions.None);
        }

        // Always do this last in case anything prior to this fails.
        AddToEnlistmentTable();
    }
    #endregion

    internal IEnlistmentNotificationInternal? EnlistmentNotification => _iEnlistmentNotification;

    internal EnlistmentShim? EnlistmentShim { get; set; }

    internal Phase0EnlistmentShim? Phase0EnlistmentShim
    {
        get => _phase0Shim;
        set
        {
            lock (this)
            {
                // If this.aborting is set to true, then we must have already received a
                // Phase0Request.  This could happen if the transaction aborts after the
                // enlistment is made, but before we are given the shim.
                if (value != null && (_aborting || _tmWentDown))
                {
                    value.Phase0Done(false);
                }
                _phase0Shim = value;
            }
        }
    }

    internal OletxEnlistmentState State { get; set; } = OletxEnlistmentState.Active;

    internal byte[]? ProxyPrepareInfoByteArray => _proxyPrepareInfoByteArray;

    internal void FinishEnlistment()
    {
        lock (this)
        {
            // If we don't have a wrappedTransactionEnlistmentAsync, we may
            // need to remove ourselves from the reenlistPendingList in the
            // resource manager.
            if (EnlistmentShim == null)
            {
                OletxResourceManager.RemoveFromReenlistPending(this);
            }
            _iEnlistmentNotification = null;

            RemoveFromEnlistmentTable();
        }
    }

    internal void TMDownFromInternalRM(OletxTransactionManager oletxTm)
    {
        lock (this)
        {
            // If we don't have an oletxTransaction or the passed oletxTm matches that of my oletxTransaction, the TM went down.
            if (oletxTransaction == null || oletxTm == oletxTransaction.RealOletxTransaction.OletxTransactionManagerInstance)
            {
                _tmWentDown = true;
            }
        }
    }

    #region ITransactionResourceAsync methods

    // ITranactionResourceAsync.PrepareRequest
    public bool PrepareRequest(bool singlePhase, byte[] prepareInfo)
    {
        EnlistmentShim? localEnlistmentShim;
        OletxEnlistmentState localState = OletxEnlistmentState.Active;
        IEnlistmentNotificationInternal localEnlistmentNotification;
        bool enlistmentDone;

        lock (this)
        {
            if (OletxEnlistmentState.Active == State)
            {
                localState = State = OletxEnlistmentState.Preparing;
            }
            else
            {
                // We must have done the prepare work in Phase0, so just remember what state we are
                // in now.
                localState = State;
            }

            localEnlistmentNotification = _iEnlistmentNotification!;

            localEnlistmentShim = EnlistmentShim;

            oletxTransaction!.RealOletxTransaction.TooLateForEnlistments = true;
        }

        // If we went to Preparing state, send the app
        // a prepare request.
        if (OletxEnlistmentState.Preparing == localState)
        {
            _isSinglePhase = singlePhase;

            // Store the prepare info we are given.
            Debug.Assert(_proxyPrepareInfoByteArray == null, "Unexpected value in this.proxyPrepareInfoByteArray");
            long arrayLength = prepareInfo.Length;
            _proxyPrepareInfoByteArray = new byte[arrayLength];
            Array.Copy(prepareInfo, _proxyPrepareInfoByteArray, arrayLength);

            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;

            if (_isSinglePhase && _canDoSinglePhase)
            {
                ISinglePhaseNotificationInternal singlePhaseNotification = (ISinglePhaseNotificationInternal)localEnlistmentNotification;
                State = OletxEnlistmentState.SinglePhaseCommitting;
                // We don't call DecrementUndecidedEnlistments for Phase1 enlistments.
                if (etwLog.IsEnabled())
                {
                    etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.SinglePhaseCommit);
                }

                singlePhaseNotification.SinglePhaseCommit(this);
                enlistmentDone = true;
            }
            else
            {
                State = OletxEnlistmentState.Preparing;

                _prepareInfoByteArray = TransactionManager.GetRecoveryInformation(
                    OletxResourceManager.OletxTransactionManager.CreationNodeName,
                    prepareInfo);

                if (etwLog.IsEnabled())
                {
                    etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Prepare);
                }

                localEnlistmentNotification.Prepare(this);
                enlistmentDone = false;
            }
        }
        else if (OletxEnlistmentState.Prepared == localState)
        {
            // We must have done our prepare work during Phase0 so just vote Yes.
            try
            {
                localEnlistmentShim!.PrepareRequestDone(OletxPrepareVoteType.Prepared);
                enlistmentDone = false;
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);
                throw;
            }
        }
        else if (OletxEnlistmentState.Done == localState)
        {
            try
            {
                // This was an early vote.  Respond ReadOnly
                try
                {
                    localEnlistmentShim!.PrepareRequestDone(OletxPrepareVoteType.ReadOnly);
                    enlistmentDone = true;
                }
                finally
                {
                    FinishEnlistment();
                }
            }
            catch (COMException comException)
            {
                OletxTransactionManager.ProxyException(comException);
                throw;
            }
        }
        else
        {
            // Any other state means we should vote NO to the proxy.
            try
            {
                localEnlistmentShim!.PrepareRequestDone(OletxPrepareVoteType.Failed);
            }
            catch (COMException ex)
            {
                // No point in rethrowing this.  We are not on an app thread and we have already told
                // the app that the transaction is aborting.  When the app calls EnlistmentDone, we will
                // do the final release of the ITransactionEnlistmentAsync.
                TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                if (etwLog.IsEnabled())
                {
                    etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                }
            }

            enlistmentDone = true;
        }

        return enlistmentDone;
    }


    public void CommitRequest()
    {
        OletxEnlistmentState localState = OletxEnlistmentState.Active;
        IEnlistmentNotificationInternal? localEnlistmentNotification = null;
        EnlistmentShim? localEnlistmentShim = null;
        bool finishEnlistment = false;

        lock (this)
        {
            if (OletxEnlistmentState.Prepared == State)
            {
                localState = State = OletxEnlistmentState.Committing;
                localEnlistmentNotification = _iEnlistmentNotification;
            }
            else
            {
                // We must have received an EnlistmentDone already.
                localState = State;
                localEnlistmentShim = EnlistmentShim;
                finishEnlistment = true;
            }
        }

        if (localEnlistmentNotification != null)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Commit);
            }

            localEnlistmentNotification.Commit(this);
        }
        else if (localEnlistmentShim != null)
        {
            // We need to respond to the proxy now.
            try
            {
                localEnlistmentShim.CommitRequestDone();
            }
            catch (COMException ex)
            {
                // If the TM went down during our call, there is nothing special we have to do because
                // the App doesn't expect any more notifications. We do want to mark the enlistment
                // to finish, however.
                if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN ||
                    ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE)
                {
                    finishEnlistment = true;
                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                    }
                }
                else
                {
                    throw;
                }
            }
            finally
            {
                if (finishEnlistment)
                {
                    FinishEnlistment();
                }
            }
        }
    }

    public void AbortRequest()
    {
        OletxEnlistmentState localState = OletxEnlistmentState.Active;
        IEnlistmentNotificationInternal? localEnlistmentNotification = null;
        EnlistmentShim? localEnlistmentShim = null;
        bool finishEnlistment = false;

        lock (this)
        {
            if (State is OletxEnlistmentState.Active or OletxEnlistmentState.Prepared)
            {
                localState = State = OletxEnlistmentState.Aborting;
                localEnlistmentNotification = _iEnlistmentNotification;
            }
            else
            {
                // We must have received an EnlistmentDone already or we have
                // a notification outstanding (Phase0 prepare).
                localState = State;
                if (OletxEnlistmentState.Phase0Preparing == State)
                {
                    _fabricateRollback = true;
                }
                else
                {
                    finishEnlistment = true;
                }

                localEnlistmentShim = EnlistmentShim;
            }
        }

        if (localEnlistmentNotification != null)
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Rollback);
            }

            localEnlistmentNotification.Rollback(this);
        }
        else if (localEnlistmentShim != null)
        {
            // We need to respond to the proxy now.
            try
            {
                localEnlistmentShim.AbortRequestDone();
            }
            catch (COMException ex)
            {
                // If the TM went down during our call, there is nothing special we have to do because
                // the App doesn't expect any more notifications.  We do want to mark the enlistment
                // to finish, however.
                if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN ||
                    ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE)
                {
                    finishEnlistment = true;
                    TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
                    if (etwLog.IsEnabled())
                    {
                        etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                    }
                }
                else
                {
                    throw;
                }
            }
            finally
            {
                if (finishEnlistment)
                {
                    FinishEnlistment();
                }
            }
        }
    }

    public void TMDown()
    {
        // We aren't telling our enlistments about TMDown, only
        // resource managers.
        // Put this enlistment on the Reenlist list.  The Reenlist thread will get
        // started when the RMSink gets the TMDown notification.
        lock (OletxResourceManager.ReenlistList)
        {
            lock (this)
            {
                // Remember that we got the TMDown in case we get a Phase0Request after so we
                // can avoid doing a Prepare to the app.
                _tmWentDown = true;

                // Only move Prepared and Committing enlistments to the ReenlistList.  All others
                // do not require a Reenlist to figure out what to do.  We save off Committing
                // enlistments because the RM has not acknowledged the commit, so we can't
                // call RecoveryComplete on the proxy until that has happened.  The Reenlist thread
                // will loop until the reenlist list is empty and it will leave a Committing
                // enlistment on the list until it is done, but will NOT call Reenlist on the proxy.
                if (State is OletxEnlistmentState.Prepared or OletxEnlistmentState.Committing)
                {
                    OletxResourceManager.ReenlistList.Add(this);
                }
            }
        }
    }

    #endregion

    #region ITransactionPhase0NotifyAsync methods

    // ITransactionPhase0NotifyAsync
    public void Phase0Request(bool abortingHint)
    {
        IEnlistmentNotificationInternal? localEnlistmentNotification = null;
        OletxEnlistmentState localState = OletxEnlistmentState.Active;
        OletxCommittableTransaction? committableTx;
        bool commitNotYetCalled = false;

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

        committableTx = oletxTransaction!.RealOletxTransaction.CommittableTransaction;
        if (committableTx != null)
        {
            // We are dealing with the committable transaction.  If Commit or BeginCommit has NOT been
            // called, then we are dealing with a situation where the TM went down and we are getting
            // a bogus Phase0Request with abortHint = false (COMPlus bug 36760/36758).  This is an attempt
            // to not send the app a Prepare request when we know the transaction is going to abort.
            if (!committableTx.CommitCalled)
            {
                commitNotYetCalled = true;
            }
        }

        lock (this)
        {
            _aborting = abortingHint;

            // The app may have already called EnlistmentDone.  If this occurs, don't bother sending
            // the notification to the app and we don't need to tell the proxy.
            if (OletxEnlistmentState.Active == State)
            {
                // If we got an abort hint or we are the committable transaction and Commit has not yet been called or the TM went down,
                // we don't want to do any more work on the transaction.  The abort notifications will be sent by the phase 1
                // enlistment
                if (_aborting || commitNotYetCalled || _tmWentDown)
                {
                    // There is a possible race where we could get the Phase0Request before we are given the
                    // shim.  In that case, we will vote "no" when we are given the shim.
                    if (_phase0Shim != null)
                    {
                        try
                        {
                            _phase0Shim.Phase0Done(false);
                        }
                        // I am not going to check for XACT_E_PROTOCOL here because that check is a workaround for a bug
                        // that only shows up if abortingHint is false.
                        catch (COMException ex)
                        {
                            if (etwLog.IsEnabled())
                            {
                                etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                            }
                        }
                    }
                }
                else
                {
                    localState = State = OletxEnlistmentState.Phase0Preparing;
                    localEnlistmentNotification = _iEnlistmentNotification;
                }
            }
        }

        // Tell the application to do the work.
        if (localEnlistmentNotification != null)
        {
            if (OletxEnlistmentState.Phase0Preparing == localState)
            {
                byte[] txGuidArray = _transactionGuid.ToByteArray();
                byte[] rmGuidArray = OletxResourceManager.ResourceManagerIdentifier.ToByteArray();

                byte[] temp = new byte[txGuidArray.Length + rmGuidArray.Length];
                Thread.MemoryBarrier();
                _proxyPrepareInfoByteArray = temp;
                for (int index = 0; index < txGuidArray.Length; index++)
                {
                    _proxyPrepareInfoByteArray[index] =
                        txGuidArray[index];
                }

                for (int index = 0; index < rmGuidArray.Length; index++)
                {
                    _proxyPrepareInfoByteArray[txGuidArray.Length + index] =
                        rmGuidArray[index];
                }

                _prepareInfoByteArray = TransactionManager.GetRecoveryInformation(
                    OletxResourceManager.OletxTransactionManager.CreationNodeName,
                    _proxyPrepareInfoByteArray);

                if (etwLog.IsEnabled())
                {
                    etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Prepare);
                }

                localEnlistmentNotification.Prepare(this);
            }
            else
            {
                // We must have had a race between EnlistmentDone and the proxy telling
                // us Phase0Request.  Just return.
                if (etwLog.IsEnabled())
                {
                    etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(Phase0Request)}");
                }

                return;
            }

        }

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

    #endregion

    public void EnlistmentDone()
    {
        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(EnlistmentDone)}");
            etwLog.EnlistmentCallbackPositive(InternalTraceIdentifier, EnlistmentCallback.Done);
        }

        EnlistmentShim? localEnlistmentShim = null;
        Phase0EnlistmentShim? localPhase0Shim = null;
        OletxEnlistmentState localState = OletxEnlistmentState.Active;
        bool finishEnlistment;
        bool localFabricateRollback;

        lock (this)
        {
            localState = State;
            if (OletxEnlistmentState.Active == State)
            {
                // Early vote.  If we are doing Phase0, we need to unenlist.  Otherwise, just
                // remember.
                localPhase0Shim = Phase0EnlistmentShim;
                if (localPhase0Shim != null)
                {
                    // We are a Phase0 enlistment and we have a vote - decrement the undecided enlistment count.
                    // We only do this for Phase0 because we don't count Phase1 durable enlistments.
                    oletxTransaction!.RealOletxTransaction.DecrementUndecidedEnlistments();
                }
                finishEnlistment = false;
            }
            else if (OletxEnlistmentState.Preparing == State)
            {
                // Read only vote.  Tell the proxy and go to the Done state.
                localEnlistmentShim = EnlistmentShim;
                // We don't decrement the undecided enlistment count for Preparing because we only count
                // Phase0 enlistments and we are in Phase1 in Preparing state.
                finishEnlistment = true;
            }
            else if (OletxEnlistmentState.Phase0Preparing == State)
            {
                // Read only vote to Phase0.  Tell the proxy okay and go to the Done state.
                localPhase0Shim = Phase0EnlistmentShim;
                // We are a Phase0 enlistment and we have a vote - decrement the undecided enlistment count.
                // We only do this for Phase0 because we don't count Phase1 durable enlistments.
                oletxTransaction!.RealOletxTransaction.DecrementUndecidedEnlistments();

                // If we would have fabricated a rollback then we have already received an abort request
                // from proxy and will not receive any more notifications.  Otherwise more notifications
                // will be coming.
                if (_fabricateRollback)
                {
                    finishEnlistment = true;
                }
                else
                {
                    finishEnlistment = false;
                }
            }
            else if (State is OletxEnlistmentState.Committing
                     or OletxEnlistmentState.Aborting
                     or OletxEnlistmentState.SinglePhaseCommitting)
            {
                localEnlistmentShim = EnlistmentShim;
                finishEnlistment = true;
                // We don't decrement the undecided enlistment count for SinglePhaseCommitting because we only
                // do it for Phase0 enlistments.
            }
            else
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }

            // If this.fabricateRollback is true, it means that we are fabricating this
            // AbortRequest, rather than having the proxy tell us.  So we don't need
            // to respond to the proxy with AbortRequestDone.
            localFabricateRollback = _fabricateRollback;

            State = OletxEnlistmentState.Done;
        }

        try
        {
            if (localEnlistmentShim != null)
            {
                if (OletxEnlistmentState.Preparing == localState)
                {
                    localEnlistmentShim.PrepareRequestDone(OletxPrepareVoteType.ReadOnly);
                }
                else if (OletxEnlistmentState.Committing == localState)
                {
                    localEnlistmentShim.CommitRequestDone();
                }
                else if (OletxEnlistmentState.Aborting == localState)
                {
                    // If localFabricatRollback is true, it means that we are fabricating this
                    // AbortRequest, rather than having the proxy tell us.  So we don't need
                    // to respond to the proxy with AbortRequestDone.
                    if (!localFabricateRollback)
                    {
                        localEnlistmentShim.AbortRequestDone();
                    }
                }
                else if (OletxEnlistmentState.SinglePhaseCommitting == localState)
                {
                    localEnlistmentShim.PrepareRequestDone(OletxPrepareVoteType.SinglePhase);
                }
                else
                {
                    throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
                }
            }
            else if (localPhase0Shim != null)
            {
                if (localState == OletxEnlistmentState.Active)
                {
                    localPhase0Shim.Unenlist();
                }
                else if (localState == OletxEnlistmentState.Phase0Preparing)
                {
                    localPhase0Shim.Phase0Done(true);
                }
                else
                {
                    throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
                }
            }
        }
        catch (COMException ex)
        {
            // If we get an error talking to the proxy, there is nothing special we have to do because
            // the App doesn't expect any more notifications.  We do want to mark the enlistment
            // to finish, however.
            finishEnlistment = true;

            if (etwLog.IsEnabled())
            {
                etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
            }
        }
        finally
        {
            if (finishEnlistment)
            {
                FinishEnlistment();
            }
        }

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

    public EnlistmentTraceIdentifier EnlistmentTraceId
    {
        get
        {
            TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
            if (etwLog.IsEnabled())
            {
                etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(EnlistmentTraceId)}");
                etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(EnlistmentTraceId)}");
            }

            return InternalTraceIdentifier;
        }
    }

    public void Prepared()
    {
        EnlistmentShim? localEnlistmentShim = null;
        Phase0EnlistmentShim? localPhase0Shim = null;
        bool localFabricateRollback = false;

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(Prepared)}");
            etwLog.EnlistmentCallbackPositive(InternalTraceIdentifier, EnlistmentCallback.Prepared);
        }

        lock (this)
        {
            if (State == OletxEnlistmentState.Preparing)
            {
                localEnlistmentShim = EnlistmentShim;
            }
            else if (OletxEnlistmentState.Phase0Preparing == State)
            {
                // If the transaction is doomed or we have fabricateRollback is true because the
                // transaction aborted while the Phase0 Prepare request was outstanding,
                // release the WrappedTransactionPhase0EnlistmentAsync and remember that
                // we have a pending rollback.
                localPhase0Shim = Phase0EnlistmentShim;
                if (oletxTransaction!.RealOletxTransaction.Doomed || _fabricateRollback)
                {
                    // Set fabricateRollback in case we got here because the transaction is doomed.
                    _fabricateRollback = true;
                    localFabricateRollback = _fabricateRollback;
                }
            }
            else
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }

            State = OletxEnlistmentState.Prepared;
        }

        try
        {
            if (localEnlistmentShim != null)
            {
                localEnlistmentShim.PrepareRequestDone(OletxPrepareVoteType.Prepared);
            }
            else if (localPhase0Shim != null)
            {
                // We have a vote - decrement the undecided enlistment count.  We do
                // this after checking Doomed because ForceRollback will decrement also.
                // We also do this only for Phase0 enlistments.
                oletxTransaction!.RealOletxTransaction.DecrementUndecidedEnlistments();

                localPhase0Shim.Phase0Done(!localFabricateRollback);
            }
            else
            {
                // The TM must have gone down, thus causing our interface pointer to be
                // invalidated.  So we need to drive abort of the enlistment as if we
                // received an AbortRequest.
                localFabricateRollback = true;
            }

            if (localFabricateRollback)
            {
                AbortRequest();
            }
        }
        catch (COMException ex)
        {
            // If the TM went down during our call, the TMDown notification to the enlistment
            // and RM will put this enlistment on the ReenlistList, if appropriate.  The outcome
            // will be obtained by the ReenlistThread.
            if ((ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) && etwLog.IsEnabled())
            {
                etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
            }
            // In the case of Phase0, there is a bug in the proxy that causes an XACT_E_PROTOCOL
            // error if the TM goes down while the enlistment is still active.  The Phase0Request is
            // sent out with abortHint false, but the state of the proxy object is not changed, causing
            // Phase0Done request to fail with XACT_E_PROTOCOL.
            // For Prepared, we want to make sure the proxy aborts the transaction.  We don't need
            // to drive the abort to the application here because the Phase1 enlistment will do that.
            // In other words, treat this as if the proxy said Phase0Request( abortingHint = true ).
            else if (ex.ErrorCode == OletxHelper.XACT_E_PROTOCOL)
            {
                Phase0EnlistmentShim = null;

                if (etwLog.IsEnabled())
                {
                    etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                }
            }
            else
            {
                throw;
            }
        }

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

    public void ForceRollback()
        => ForceRollback(null);

    public void ForceRollback(Exception? e)
    {
        EnlistmentShim? localEnlistmentShim = null;
        Phase0EnlistmentShim? localPhase0Shim = null;

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(ForceRollback)}");
            etwLog.EnlistmentCallbackNegative(InternalTraceIdentifier, EnlistmentCallback.ForceRollback);
        }

        lock (this)
        {
            if (OletxEnlistmentState.Preparing == State)
            {
                localEnlistmentShim = EnlistmentShim;
            }
            else if (OletxEnlistmentState.Phase0Preparing == State)
            {
                localPhase0Shim = Phase0EnlistmentShim;
                if (localPhase0Shim != null)
                {
                    // We have a vote - decrement the undecided enlistment count.  We only do this
                    // if we are Phase0 enlistment.
                    oletxTransaction!.RealOletxTransaction.DecrementUndecidedEnlistments();
                }
            }
            else
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }

            State = OletxEnlistmentState.Aborted;
        }

        Interlocked.CompareExchange(ref oletxTransaction!.RealOletxTransaction.InnerException, e, null);

        try
        {
            localEnlistmentShim?.PrepareRequestDone(OletxPrepareVoteType.Failed);
        }
        catch (COMException ex)
        {
            // If the TM went down during our call, there is nothing special we have to do because
            // the App doesn't expect any more notifications.
            if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN ||
                ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                }
            }
            else
            {
                throw;
            }
        }
        finally
        {
            FinishEnlistment();
        }

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

    public void Committed()
    {
        EnlistmentShim? localEnlistmentShim = null;

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxSinglePhaseEnlistment.{nameof(Committed)}");
            etwLog.EnlistmentCallbackPositive(InternalTraceIdentifier, EnlistmentCallback.Committed);
        }

        lock (this)
        {
            if (!_isSinglePhase || OletxEnlistmentState.SinglePhaseCommitting != State)
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }
            State = OletxEnlistmentState.Committed;
            localEnlistmentShim = EnlistmentShim;
        }

        try
        {
            // This may be the result of a reenlist, which means we don't have a
            // reference to the proxy.
            localEnlistmentShim?.PrepareRequestDone(OletxPrepareVoteType.SinglePhase);
        }
        catch (COMException ex)
        {
            // If the TM went down during our call, there is nothing special we have to do because
            // the App doesn't expect any more notifications.
            if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN ||
                ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE)
            {
                if (etwLog.IsEnabled())
                {
                    etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
                }
            }
            else
            {
                throw;
            }
        }
        finally
        {
            FinishEnlistment();
        }

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

    public void Aborted()
        => Aborted(null);

    public void Aborted(Exception? e)
    {
        EnlistmentShim? localEnlistmentShim = null;

        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxSinglePhaseEnlistment.{nameof(Aborted)}");
            etwLog.EnlistmentCallbackNegative(InternalTraceIdentifier, EnlistmentCallback.Aborted);
        }

        lock (this)
        {
            if (!_isSinglePhase || OletxEnlistmentState.SinglePhaseCommitting != State)
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }
            State = OletxEnlistmentState.Aborted;

            localEnlistmentShim = EnlistmentShim;
        }

        Interlocked.CompareExchange(ref oletxTransaction!.RealOletxTransaction.InnerException, e, null);

        try
        {
            localEnlistmentShim?.PrepareRequestDone(OletxPrepareVoteType.Failed);
        }
        // If the TM went down during our call, there is nothing special we have to do because
        // the App doesn't expect any more notifications.
        catch (COMException ex) when (
            (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) && etwLog.IsEnabled())
        {
            etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
        }
        finally
        {
            FinishEnlistment();
        }

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

    public void InDoubt()
        => InDoubt(null);

    public void InDoubt(Exception? e)
    {
        EnlistmentShim? localEnlistmentShim = null;
        TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
        if (etwLog.IsEnabled())
        {
            etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxSinglePhaseEnlistment.{nameof(InDoubt)}");
            etwLog.EnlistmentCallbackNegative(InternalTraceIdentifier, EnlistmentCallback.InDoubt);
        }

        lock (this)
        {
            if (!_isSinglePhase || OletxEnlistmentState.SinglePhaseCommitting != State)
            {
                throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
            }
            State = OletxEnlistmentState.InDoubt;
            localEnlistmentShim = EnlistmentShim;
        }

        lock (oletxTransaction!.RealOletxTransaction)
        {
            oletxTransaction.RealOletxTransaction.InnerException ??= e;
        }

        try
        {
            localEnlistmentShim?.PrepareRequestDone(OletxPrepareVoteType.InDoubt);
        }
        // If the TM went down during our call, there is nothing special we have to do because
        // the App doesn't expect any more notifications.
        catch (COMException ex) when (
            (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) && etwLog.IsEnabled())
        {
            etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex);
        }
        finally
        {
            FinishEnlistment();
        }

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

    public byte[] GetRecoveryInformation()
    {
        if (_prepareInfoByteArray == null)
        {
            Debug.Fail("this.prepareInfoByteArray == null in RecoveryInformation()");
            throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId);
        }

        return _prepareInfoByteArray;
    }

    InternalEnlistment? IPromotedEnlistment.InternalEnlistment
    {
        get => base.InternalEnlistment;
        set => base.InternalEnlistment = value;
    }
}