diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs b/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs index 81b31db1cff99..bf53b2717ea2e 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs @@ -46,5 +46,6 @@ internal static partial class Libraries internal const string MsQuic = "msquic.dll"; internal const string HostPolicy = "hostpolicy.dll"; internal const string Ucrtbase = "ucrtbase.dll"; + internal const string Xolehlp = "xolehlp.dll"; } } diff --git a/src/libraries/System.Transactions.Local/src/Resources/Strings.resx b/src/libraries/System.Transactions.Local/src/Resources/Strings.resx index a11f02e372d5c..4880e4f544a4b 100644 --- a/src/libraries/System.Transactions.Local/src/Resources/Strings.resx +++ b/src/libraries/System.Transactions.Local/src/Resources/Strings.resx @@ -1,4 +1,64 @@ - + + + @@ -57,41 +117,310 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - TransactionScope with TransactionScopeAsyncFlowOption.Enabled option is not supported when the TransactionScope is used within Enterprise Service context with Automatic or Full EnterpriseServicesInteropOption enabled in parent scope. - The IAsyncResult parameter must be the same parameter returned by BeginCommit. - Resource Manager Identifiers cannot be Guid.Empty. - Transactions with IsolationLevel Snapshot cannot be promoted. - Current cannot be set directly when Com+ Interop is enabled. - The delegate for an external current can only be set once. - The current TransactionScope is already complete. You should dispose the TransactionScope. - The operation is not valid for the current state of the enlistment. - Com+ Interop features cannot be supported. - Internal Error - The argument is invalid. - The specified IPromotableSinglePhaseNotification is not the same as the one provided to EnlistPromotableSinglePhase. - Transaction Manager in the Recovery Information does not match the configured transaction manager. - A TransactionScope must be disposed on the same thread that it was created. - There was an error promoting the transaction to a distributed transaction. - The Promote method returned an invalid value for the distributed transaction. - The transaction returned from Promote already exists as a distributed transaction. - It is too late to add enlistments to this transaction. - Transaction Timeout - The transaction has aborted. - DependentTransaction.Complete or CommittableTransaction.Commit has already been called for this transaction. - The transaction is in doubt. - Communication with the underlying transaction manager has failed. - The current TransactionScope is already complete. - Transaction.Current has changed inside of the TransactionScope. - TransactionScope nested incorrectly. - The transaction specified for TransactionScope has a different IsolationLevel than the value requested for the scope. - TransactionScope timer object is invalid. - The operation is not valid for the state of the transaction. - There was an unexpected failure of QueueUserWorkItem. - There was an unexpected failure of a timer. - The RecoveryInformation provided is not recognized by this version of System.Transactions. - Volatile enlistments do not generate recovery information. - {0} Distributed Transaction ID is {1} - The specified PromoterType is invalid. - There is a promotable enlistment for the transaction which has a PromoterType value that is not recognized by System.Transactions. {0} - This platform does not support distributed transactions. + + TransactionScope with TransactionScopeAsyncFlowOption.Enabled option is not supported when the TransactionScope is used within Enterprise Service context with Automatic or Full EnterpriseServicesInteropOption enabled in parent scope. + + + The IAsyncResult parameter must be the same parameter returned by BeginCommit. + + + Resource Manager Identifiers cannot be Guid.Empty. + + + Transactions with IsolationLevel Snapshot cannot be promoted. + + + Current cannot be set directly when Com+ Interop is enabled. + + + The delegate for an external current can only be set once. + + + The current TransactionScope is already complete. You should dispose the TransactionScope. + + + The operation is not valid for the current state of the enlistment. + + + Com+ Interop features cannot be supported. + + + Internal Error + + + The argument is invalid. + + + The specified IPromotableSinglePhaseNotification is not the same as the one provided to EnlistPromotableSinglePhase. + + + Transaction Manager in the Recovery Information does not match the configured transaction manager. + + + A TransactionScope must be disposed on the same thread that it was created. + + + There was an error promoting the transaction to a distributed transaction. + + + The Promote method returned an invalid value for the distributed transaction. + + + The transaction returned from Promote already exists as a distributed transaction. + + + It is too late to add enlistments to this transaction. + + + Transaction Timeout + + + The transaction has aborted. + + + DependentTransaction.Complete or CommittableTransaction.Commit has already been called for this transaction. + + + The transaction is in doubt. + + + Communication with the underlying transaction manager has failed. + + + The current TransactionScope is already complete. + + + Transaction.Current has changed inside of the TransactionScope. + + + TransactionScope nested incorrectly. + + + The transaction specified for TransactionScope has a different IsolationLevel than the value requested for the scope. + + + TransactionScope timer object is invalid. + + + The operation is not valid for the state of the transaction. + + + There was an unexpected failure of QueueUserWorkItem. + + + There was an unexpected failure of a timer. + + + The RecoveryInformation provided is not recognized by this version of System.Transactions. + + + Volatile enlistments do not generate recovery information. + + + {0} Distributed Transaction ID is {1} + + + The specified PromoterType is invalid. + + + There is a promotable enlistment for the transaction which has a PromoterType value that is not recognized by System.Transactions. {0} + + + This platform does not support distributed transactions. + + + [Distributed] + + + Configured DefaultTimeout is greater than configured DefaultMaximumTimeout - adjusting down to DefaultMaximumTimeout + + + Method Entered + + + Method Exited + + + The operation is invalid because the document is empty. + + + Cannot add an element to a closed document. + + + The text node already has a value. + + + Enlistment Created + + + Transaction Promoted + + + Enlistment Notification Call + + + Enlistment Callback Positive + + + Enlistment Callback Negative + + + Cannot close an element on a closed document. + + + Internal Error + + + InvalidOperationException Thrown + + + Exception Consumed + + + TransactionException Thrown + + + Transaction Deserialized + + + Transaction Serialized + + + Transaction Manager Instance Created + + + TransactionManager.Reenlist Called + + + Transaction Created + + + CommittableTransaction.Commit Called + + + Transaction Committed + + + Transaction Aborted + + + Transaction InDoubt + + + TransactionScope Created + + + TransactionScope Disposed + + + MSDTC Proxy cannot support specification of different node names in the same process. + + + Cannot support specification of node name for the distributed transaction manager through System.Transactions due to MSDTC Proxy version. + + + Failed to create trace source. + + + Failed to initialize trace source. + + + TransactionManager.RecoveryComplete Called + + + Clone Created + + + Dependent Clone Completed + + + Dependent Clone Created + + + TransactionScope Timeout + + + Transaction.Rollback Called + + + Trace Event Type: {0}\nTrace Code: {1}\nTrace Description {2}\nObject: {3} + + + Internal Error - Unexpected transaction status value in enlistment constructor. + + + Unable to deserialize the transaction. + + + Internal Error - Unable to deserialize the transaction. + + + The transaction has already been implicitly or explicitly committed or aborted. + + + Process Name: {0}\nProcess Id: {1}\nCode: {2}\nDescription: {3} + + + Source: {0} + + + Exception: {0} + + + Event ID: {0} + + + Other information : {0} + + + TransactionScope Current Transaction Changed + + + TransactionScope Nested Incorrectly + + + TransactionScope Incomplete + + + Distributed transaction manager is unable to create the NotificationShimFactory object. + + + Unable to obtain the transaction identifier. + + + Calling TransactionManager.Reenlist is not allowed after TransactionManager.RecoveryComplete is called for a given resource manager identifier. + + + RecoveryComplete must not be called twice by the same resource manager identifier instance. + + + MSDTC Transaction Manager is unavailable. + + + Network access for Distributed Transaction Manager (MSDTC) has been disabled. Please enable DTC for network access in the security configuration for MSDTC using the Component Services Administrative tool. + + + [Lightweight] + + + AppDomain unloading. + + + Unhandled Exception + + + The resourceManagerIdentifier does not match the contents of the specified recovery information. + + + The distributed transaction manager does not allow any more durable enlistments on the transaction. + + + Failed to trace event: {0}. + + + [Base] + + + Distributed transactions are currently unsupported on 32-bit version of the .NET runtime. + \ No newline at end of file diff --git a/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj b/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj index 9b7d1861a6727..86a68905ab6ed 100644 --- a/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj +++ b/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj @@ -2,17 +2,20 @@ true true - $(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent) + CA1805;IDE0059;CS1591 + $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) + false - + @@ -25,7 +28,6 @@ - @@ -40,7 +42,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -48,5 +116,6 @@ + diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/CommittableTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/CommittableTransaction.cs index 20fc0d0e4eab8..dfeace8bd4a53 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/CommittableTransaction.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/CommittableTransaction.cs @@ -5,6 +5,8 @@ using System.Runtime.Versioning; using System.Threading; +#pragma warning disable CS1591 + namespace System.Transactions { [UnsupportedOSPlatform("browser")] @@ -37,7 +39,7 @@ internal CommittableTransaction(IsolationLevel isoLevel, TimeSpan timeout) : bas TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionCreated(this, "CommittableTransaction"); + etwLog.TransactionCreated(TraceSourceType.TraceSourceLtm, TransactionTraceId, "CommittableTransaction"); } } @@ -47,7 +49,7 @@ public IAsyncResult BeginCommit(AsyncCallback? asyncCallback, object? asyncState if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceLtm, this); - etwLog.TransactionCommit(this, "CommittableTransaction"); + etwLog.TransactionCommit(TraceSourceType.TraceSourceLtm, TransactionTraceId, "CommittableTransaction"); } ObjectDisposedException.ThrowIf(Disposed, this); @@ -81,7 +83,7 @@ public void Commit() if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceLtm, this); - etwLog.TransactionCommit(this, "CommittableTransaction"); + etwLog.TransactionCommit(TraceSourceType.TraceSourceLtm, TransactionTraceId, "CommittableTransaction"); } ObjectDisposedException.ThrowIf(Disposed, this); @@ -113,7 +115,6 @@ public void Commit() { etwLog.MethodExit(TraceSourceType.TraceSourceLtm, this); } - } internal override void InternalDispose() diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DependentTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DependentTransaction.cs index 7f11478f773a3..9a5c890a076c6 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/DependentTransaction.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DependentTransaction.cs @@ -61,7 +61,7 @@ public void Complete() if (etwLog.IsEnabled()) { - etwLog.TransactionDependentCloneComplete(this, "DependentTransaction"); + etwLog.TransactionDependentCloneComplete(TraceSourceType.TraceSourceLtm, TransactionTraceId, "DependentTransaction"); etwLog.MethodExit(TraceSourceType.TraceSourceLtm, this); } } diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DistributedTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DistributedTransaction.cs deleted file mode 100644 index 59d7fea1829f7..0000000000000 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/DistributedTransaction.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.Serialization; - -namespace System.Transactions.Distributed -{ - internal sealed class DistributedTransactionManager - { - internal object? NodeName { get; set; } - - internal static IPromotedEnlistment ReenlistTransaction(Guid resourceManagerIdentifier, byte[] resourceManagerRecoveryInformation, RecoveringInternalEnlistment internalEnlistment) - { - throw DistributedTransaction.NotSupported(); - } - - internal static DistributedCommittableTransaction CreateTransaction(TransactionOptions options) - { - throw DistributedTransaction.NotSupported(); - } - - internal static void ResourceManagerRecoveryComplete(Guid resourceManagerIdentifier) - { - throw DistributedTransaction.NotSupported(); - } - - internal static byte[] GetWhereabouts() - { - throw DistributedTransaction.NotSupported(); - } - - internal static Transaction GetTransactionFromDtcTransaction(IDtcTransaction transactionNative) - { - throw DistributedTransaction.NotSupported(); - } - - internal static DistributedTransaction GetTransactionFromExportCookie(byte[] cookie, Guid txId) - { - throw DistributedTransaction.NotSupported(); - } - - internal static DistributedTransaction GetDistributedTransactionFromTransmitterPropagationToken(byte[] propagationToken) - { - throw DistributedTransaction.NotSupported(); - } - } - - /// - /// A Transaction object represents a single transaction. It is created by TransactionManager - /// objects through CreateTransaction or through deserialization. Alternatively, the static Create - /// methods 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. - /// - internal class DistributedTransaction : ISerializable, IObjectReference - { - internal DistributedTransaction() - { - } - - protected DistributedTransaction(SerializationInfo serializationInfo, StreamingContext context) - { - //if (serializationInfo == null) - //{ - // throw new ArgumentNullException(nameof(serializationInfo)); - //} - - //throw NotSupported(); - throw new PlatformNotSupportedException(); - } - - internal Exception? InnerException { get; set; } - internal Guid Identifier { get; set; } - internal RealDistributedTransaction? RealTransaction { get; set; } - internal TransactionTraceIdentifier TransactionTraceId { get; set; } - internal IsolationLevel IsolationLevel { get; set; } - internal Transaction? SavedLtmPromotedTransaction { get; set; } - - internal static IPromotedEnlistment EnlistVolatile(InternalEnlistment internalEnlistment, EnlistmentOptions enlistmentOptions) - { - throw NotSupported(); - } - - internal static IPromotedEnlistment EnlistDurable(Guid resourceManagerIdentifier, DurableInternalEnlistment internalEnlistment, bool v, EnlistmentOptions enlistmentOptions) - { - throw NotSupported(); - } - - internal static void Rollback() - { - throw NotSupported(); - } - - internal static DistributedDependentTransaction DependentClone(bool v) - { - throw NotSupported(); - } - - internal static IPromotedEnlistment EnlistVolatile(VolatileDemultiplexer volatileDemux, EnlistmentOptions enlistmentOptions) - { - throw NotSupported(); - } - - internal static byte[] GetExportCookie(byte[] whereaboutsCopy) - { - throw NotSupported(); - } - - public object GetRealObject(StreamingContext context) - { - throw NotSupported(); - } - - internal static byte[] GetTransmitterPropagationToken() - { - throw NotSupported(); - } - - internal static IDtcTransaction GetDtcTransaction() - { - throw NotSupported(); - } - - void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext context) - { - //if (serializationInfo == null) - //{ - // throw new ArgumentNullException(nameof(serializationInfo)); - //} - - //throw NotSupported(); - - throw new PlatformNotSupportedException(); - } - - internal static Exception NotSupported() - { - return new PlatformNotSupportedException(SR.DistributedNotSupported); - } - - internal sealed class RealDistributedTransaction - { - internal InternalTransaction? InternalTransaction { get; set; } - } - } - - internal sealed class DistributedDependentTransaction : DistributedTransaction - { - internal static void Complete() - { - throw NotSupported(); - } - } - - internal sealed class DistributedCommittableTransaction : DistributedTransaction - { - internal static void BeginCommit(InternalTransaction tx) - { - throw NotSupported(); - } - } -} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IPrepareInfo.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IPrepareInfo.cs new file mode 100644 index 0000000000000..0b6c1158f59b3 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IPrepareInfo.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686533(v=vs.85) +[ComImport, Guid("80c7bfd0-87ee-11ce-8081-0080c758527e"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IPrepareInfo +{ + void GetPrepareInfoSize(out uint pcbPrepInfo); + + void GetPrepareInfo([MarshalAs(UnmanagedType.LPArray), Out] byte[] pPrepInfo); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManager.cs new file mode 100644 index 0000000000000..4cec70b044228 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManager.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms681790(v=vs.85) +[ComImport, Guid(Guids.IID_IResourceManager), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IResourceManager +{ + internal void Enlist( + [MarshalAs(UnmanagedType.Interface)] ITransaction pTransaction, + [MarshalAs(UnmanagedType.Interface)] ITransactionResourceAsync pRes, + out Guid pUOW, + out OletxTransactionIsolationLevel pisoLevel, + [MarshalAs(UnmanagedType.Interface)] out ITransactionEnlistmentAsync ppEnlist); + + internal void Reenlist( + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pPrepInfo, + uint cbPrepInfom, + uint lTimeout, + [MarshalAs(UnmanagedType.I4)] out OletxXactStat pXactStat); + + void ReenlistmentComplete(); + + void GetDistributedTransactionManager( + in Guid riid, + [MarshalAs(UnmanagedType.Interface)] out object ppvObject); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerFactory2.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerFactory2.cs new file mode 100644 index 0000000000000..b788a17032a5d --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerFactory2.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686489(v=vs.85) +[ComImport, Guid("6B369C21-FBD2-11d1-8F47-00C04F8EE57D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IResourceManagerFactory2 +{ + internal void Create( + Guid pguidRM, + [MarshalAs(UnmanagedType.LPStr)] string pszRMName, + [MarshalAs(UnmanagedType.Interface)] IResourceManagerSink pIResMgrSink, + [MarshalAs(UnmanagedType.Interface)] out IResourceManager rm); + + internal void CreateEx( + Guid pguidRM, + [MarshalAs(UnmanagedType.LPStr)] string pszRMName, + [MarshalAs(UnmanagedType.Interface)] IResourceManagerSink pIResMgrSink, + Guid riidRequested, + [MarshalAs(UnmanagedType.Interface)] out object rm); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerSink.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerSink.cs new file mode 100644 index 0000000000000..0e872188cfb6b --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/IResourceManagerSink.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686073(v=vs.85) +[ComImport, Guid("0D563181-DEFB-11CE-AED1-00AA0051E2C4"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IResourceManagerSink +{ + void TMDown(); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITmNodeName.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITmNodeName.cs new file mode 100644 index 0000000000000..4816352f86164 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITmNodeName.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms687122(v=vs.85) +[ComImport, Guid("30274F88-6EE4-474e-9B95-7807BC9EF8CF"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITmNodeName +{ + internal void GetNodeNameSize(out uint pcbNodeNameSize); + + internal void GetNodeName(uint cbNodeNameBufferSize, [MarshalAs(UnmanagedType.LPWStr)] out string pcbNodeSize); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransaction.cs new file mode 100644 index 0000000000000..e516d2c0d038f --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransaction.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686531(v=vs.85) +[ComImport, Guid(Guids.IID_ITransaction), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransaction +{ + void Commit([MarshalAs(UnmanagedType.Bool)] bool fRetainingt, [MarshalAs(UnmanagedType.U4)] OletxXacttc grfTC, uint grfRM); + + void Abort(IntPtr reason, [MarshalAs(UnmanagedType.Bool)] bool retaining, [MarshalAs(UnmanagedType.Bool)] bool async); + + void GetTransactionInfo(out OletxXactTransInfo xactInfo); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionCloner.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionCloner.cs new file mode 100644 index 0000000000000..7ce0b361bdf5f --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionCloner.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms684377(v=vs.85) +[ComImport, Guid("02656950-2152-11d0-944C-00A0C905416E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionCloner +{ + void CloneWithCommitDisabled([MarshalAs(UnmanagedType.Interface)] out ITransaction ppITransaction); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionDispenser.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionDispenser.cs new file mode 100644 index 0000000000000..c45ea2c5f1163 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionDispenser.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Transactions.DtcProxyShim; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms679525(v=vs.85) +[ComImport, Guid(Guids.IID_ITransactionDispenser), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionDispenser +{ + void GetOptionsObject([MarshalAs(UnmanagedType.Interface)] out ITransactionOptions ppOptions); + + void BeginTransaction( + IntPtr punkOuter, + [MarshalAs(UnmanagedType.I4)] OletxTransactionIsolationLevel isoLevel, + [MarshalAs(UnmanagedType.I4)] OletxTransactionIsoFlags isoFlags, + [MarshalAs(UnmanagedType.Interface)] ITransactionOptions pOptions, + [MarshalAs(UnmanagedType.Interface)] out ITransaction ppTransaction); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionEnlistmentAsync.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionEnlistmentAsync.cs new file mode 100644 index 0000000000000..da738568efd5e --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionEnlistmentAsync.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686429(v=vs.85) +[ComImport, Guid("0fb15081-af41-11ce-bd2b-204c4f4f5020"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionEnlistmentAsync +{ + void PrepareRequestDone(int hr, IntPtr pmk, IntPtr pboidReason); + + void CommitRequestDone(int hr); + + void AbortRequestDone(int hr); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExport.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExport.cs new file mode 100644 index 0000000000000..02f64369697ae --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExport.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms678954(v=vs.85) +[ComImport, Guid("0141fda5-8fc0-11ce-bd18-204c4f4f5020"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionExport +{ + void Export([MarshalAs(UnmanagedType.Interface)] ITransaction punkTransaction, out uint pcbTransactionCookie); + + void GetTransactionCookie( + [MarshalAs(UnmanagedType.Interface)] ITransaction pITransaction, + uint cbTransactionCookie, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] rgbTransactionCookie, + out uint pcbUsed); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExportFactory.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExportFactory.cs new file mode 100644 index 0000000000000..b82c8f5ebac37 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionExportFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686771(v=vs.85) +[ComImport, Guid("E1CF9B53-8745-11ce-A9BA-00AA006C3706"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionExportFactory +{ + void GetRemoteClassId(out Guid pclsid); + + void Create( + uint cbWhereabouts, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] byte[] rgbWhereabouts, + [MarshalAs(UnmanagedType.Interface)] out ITransactionExport ppExport); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImport.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImport.cs new file mode 100644 index 0000000000000..310f7d98a54eb --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImport.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms681296(v=vs.85) +[ComImport, Guid("E1CF9B5A-8745-11ce-A9BA-00AA006C3706"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionImport +{ + void Import( + uint cbTransactionCookie, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] byte[] rgbTransactionCookie, + Guid piid, + [MarshalAs(UnmanagedType.Interface)] out object ppvTransaction); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImportWhereabouts.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImportWhereabouts.cs new file mode 100644 index 0000000000000..28f932e72dbed --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionImportWhereabouts.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms682783(v=vs.85) +[ComImport, Guid("0141fda4-8fc0-11ce-bd18-204c4f4f5020"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionImportWhereabouts +{ + internal void GetWhereaboutsSize(out uint pcbSize); + + internal void GetWhereabouts( + uint cbWhereabouts, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0), Out] byte[] rgbWhereabouts, + out uint pcbUsed); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOptions.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOptions.cs new file mode 100644 index 0000000000000..645b4cbcf98b3 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOptions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686489(v=vs.85) +[ComImport, Guid("3A6AD9E0-23B9-11cf-AD60-00AA00A74CCD"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionOptions +{ + void SetOptions(Xactopt pOptions); + + void GetOptions(); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOutcomeEvents.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOutcomeEvents.cs new file mode 100644 index 0000000000000..d0cabc4d70831 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionOutcomeEvents.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms686465(v=vs.85) +[ComImport, Guid("3A6AD9E2-23B9-11cf-AD60-00AA00A74CCD"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface ITransactionOutcomeEvents +{ + void Committed([MarshalAs(UnmanagedType.Bool)] bool fRetaining, IntPtr pNewUOW /* always null? */, int hresult); + + void Aborted(IntPtr pboidReason, [MarshalAs(UnmanagedType.Bool)] bool fRetaining, IntPtr pNewUOW, int hresult); + + void HeuristicDecision([MarshalAs(UnmanagedType.U4)] OletxTransactionHeuristic dwDecision, IntPtr pboidReason, int hresult); + + void Indoubt(); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionPhase0EnlistmentAsync.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionPhase0EnlistmentAsync.cs new file mode 100644 index 0000000000000..ca6872fca528a --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcInterfaces/ITransactionPhase0EnlistmentAsync.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim.DtcInterfaces; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms685087(v=vs.85). _notifications = new(); + + private readonly ConcurrentQueue _cachedOptions = new(); + private readonly ConcurrentQueue _cachedTransmitters = new(); + private readonly ConcurrentQueue _cachedReceivers = new(); + + private readonly EventWaitHandle _eventHandle; + + private ITransactionDispenser _transactionDispenser = null!; // Late-initialized in ConnectToProxy + + internal DtcProxyShimFactory(EventWaitHandle notificationEventHandle) + => _eventHandle = notificationEventHandle; + + // https://docs.microsoft.com/previous-versions/windows/desktop/ms678898(v=vs.85) + [DllImport(Interop.Libraries.Xolehlp, CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] + private static extern void DtcGetTransactionManagerExW( + [MarshalAs(UnmanagedType.LPWStr)] string? pszHost, + [MarshalAs(UnmanagedType.LPWStr)] string? pszTmName, + in Guid riid, + int grfOptions, + object? pvConfigPararms, + [MarshalAs(UnmanagedType.Interface)] out ITransactionDispenser ppvObject); + + [UnconditionalSuppressMessage("Trimming", "IL2050", Justification = "Leave me alone")] + public void ConnectToProxy( + string? nodeName, + Guid resourceManagerIdentifier, + object managedIdentifier, + out bool nodeNameMatches, + out byte[] whereabouts, + out ResourceManagerShim resourceManagerShim) + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + throw new PlatformNotSupportedException(SR.DistributedNotSupportOn32Bits); + } + + lock (_proxyInitLock) + { + DtcGetTransactionManagerExW(nodeName, null, Guids.IID_ITransactionDispenser_Guid, 0, null, out ITransactionDispenser? localDispenser); + + // Check to make sure the node name matches. + if (nodeName is not null) + { + var pTmNodeName = (ITmNodeName)localDispenser; + pTmNodeName.GetNodeNameSize(out uint tmNodeNameLength); + pTmNodeName.GetNodeName(tmNodeNameLength, out string tmNodeName); + + nodeNameMatches = tmNodeName == nodeName; + } + else + { + nodeNameMatches = true; + } + + var pImportWhereabouts = (ITransactionImportWhereabouts)localDispenser; + + // Adding retry logic as a work around for MSDTC's GetWhereAbouts/GetWhereAboutsSize API + // which is single threaded and will return XACT_E_ALREADYINPROGRESS if another thread invokes the API. + uint whereaboutsSize = 0; + OletxHelper.Retry(() => pImportWhereabouts.GetWhereaboutsSize(out whereaboutsSize)); + + var tmpWhereabouts = new byte[(int)whereaboutsSize]; + + // Adding retry logic as a work around for MSDTC's GetWhereAbouts/GetWhereAboutsSize API + // which is single threaded and will return XACT_E_ALREADYINPROGRESS if another thread invokes the API. + OletxHelper.Retry(() => + { + pImportWhereabouts.GetWhereabouts(whereaboutsSize, tmpWhereabouts, out uint pcbUsed); + Debug.Assert(pcbUsed == tmpWhereabouts.Length); + }); + whereabouts = tmpWhereabouts; + + // Now we need to create the internal resource manager. + var rmFactory = (IResourceManagerFactory2)localDispenser; + + var rmNotifyShim = new ResourceManagerNotifyShim(this, managedIdentifier); + var rmShim = new ResourceManagerShim(this); + + OletxHelper.Retry(() => + { + rmFactory.CreateEx( + resourceManagerIdentifier, + "System.Transactions.InternalRM", + rmNotifyShim, + Guids.IID_IResourceManager_Guid, + out object? rm); + + rmShim.ResourceManager = (IResourceManager)rm; + }); + + resourceManagerShim = rmShim; + _transactionDispenser = localDispenser; + } + } + + internal void NewNotification(NotificationShimBase notification) + { + lock (_notificationLock) + { + _notifications.Enqueue(notification); + } + + _eventHandle.Set(); + } + + public void ReleaseNotificationLock() + => Monitor.Exit(_notificationLock); + + public void BeginTransaction( + uint timeout, + OletxTransactionIsolationLevel isolationLevel, + object? managedIdentifier, + out Guid transactionIdentifier, + out TransactionShim transactionShim) + { + ITransactionOptions options = GetCachedOptions(); + + try + { + var xactopt = new Xactopt(timeout, string.Empty); + options.SetOptions(xactopt); + + _transactionDispenser.BeginTransaction(IntPtr.Zero, isolationLevel, OletxTransactionIsoFlags.ISOFLAG_NONE, options, out ITransaction? pTx); + + SetupTransaction(pTx, managedIdentifier, out transactionIdentifier, out OletxTransactionIsolationLevel localIsoLevel, out transactionShim); + } + finally + { + ReturnCachedOptions(options); + } + } + + public void CreateResourceManager( + Guid resourceManagerIdentifier, + OletxResourceManager managedIdentifier, + out ResourceManagerShim resourceManagerShim) + { + var rmFactory = (IResourceManagerFactory2)_transactionDispenser; + + var rmNotifyShim = new ResourceManagerNotifyShim(this, managedIdentifier); + var rmShim = new ResourceManagerShim(this); + + OletxHelper.Retry(() => + { + rmFactory.CreateEx( + resourceManagerIdentifier, + "System.Transactions.ResourceManager", + rmNotifyShim, + Guids.IID_IResourceManager_Guid, + out object? rm); + + rmShim.ResourceManager = (IResourceManager)rm; + }); + + resourceManagerShim = rmShim; + } + + public void Import( + byte[] cookie, + OutcomeEnlistment managedIdentifier, + out Guid transactionIdentifier, + out OletxTransactionIsolationLevel isolationLevel, + out TransactionShim transactionShim) + { + var txImport = (ITransactionImport)_transactionDispenser; + txImport.Import(Convert.ToUInt32(cookie.Length), cookie, Guids.IID_ITransaction_Guid, out object? tx); + + SetupTransaction((ITransaction)tx, managedIdentifier, out transactionIdentifier, out isolationLevel, out transactionShim); + } + + public void ReceiveTransaction( + byte[] propagationToken, + OutcomeEnlistment managedIdentifier, + out Guid transactionIdentifier, + out OletxTransactionIsolationLevel isolationLevel, + out TransactionShim transactionShim) + { + ITransactionReceiver receiver = GetCachedReceiver(); + + try + { + receiver.UnmarshalPropagationToken( + Convert.ToUInt32(propagationToken.Length), + propagationToken, + out ITransaction? tx); + + SetupTransaction(tx, managedIdentifier, out transactionIdentifier, out isolationLevel, out transactionShim); + } + finally + { + ReturnCachedReceiver(receiver); + } + } + + public void CreateTransactionShim( + IDtcTransaction transactionNative, + OutcomeEnlistment managedIdentifier, + out Guid transactionIdentifier, + out OletxTransactionIsolationLevel isolationLevel, + out TransactionShim transactionShim) + { + var cloner = (ITransactionCloner)transactionNative; + cloner.CloneWithCommitDisabled(out ITransaction transaction); + + SetupTransaction(transaction, managedIdentifier, out transactionIdentifier, out isolationLevel, out transactionShim); + } + + internal ITransactionExportFactory ExportFactory + => (ITransactionExportFactory)_transactionDispenser; + + internal ITransactionVoterFactory2 VoterFactory + => (ITransactionVoterFactory2)_transactionDispenser; + + public void GetNotification( + out object? managedIdentifier, + out ShimNotificationType shimNotificationType, + out bool isSinglePhase, + out bool abortingHint, + out bool releaseLock, + out byte[]? prepareInfo) + { + managedIdentifier = null; + shimNotificationType = ShimNotificationType.None; + isSinglePhase = false; + abortingHint = false; + releaseLock = false; + prepareInfo = null; + + Monitor.Enter(_notificationLock); + + bool entryRemoved = _notifications.TryDequeue(out NotificationShimBase? notification); + if (entryRemoved) + { + managedIdentifier = notification!.EnlistmentIdentifier; + shimNotificationType = notification.NotificationType; + isSinglePhase = notification.IsSinglePhase; + abortingHint = notification.AbortingHint; + prepareInfo = notification.PrepareInfo; + } + + // We release the lock if we didn't find an entry or if the notification type + // is NOT ResourceManagerTMDownNotify. If it is a ResourceManagerTMDownNotify, the managed + // code will call ReleaseNotificationLock after processing the TMDown. We need to prevent + // other notifications from being processed while we are processing TMDown. But we don't want + // to force 3 roundtrips to this NotificationShimFactory for all notifications ( 1 to grab the lock, + // one to get the notification, and one to release the lock). + if (!entryRemoved || shimNotificationType != ShimNotificationType.ResourceManagerTmDownNotify) + { + Monitor.Exit(_notificationLock); + } + else + { + releaseLock = true; + } + } + + private void SetupTransaction( + ITransaction transaction, + object? managedIdentifier, + out Guid pTransactionIdentifier, + out OletxTransactionIsolationLevel pIsolationLevel, + out TransactionShim ppTransactionShim) + { + var transactionNotifyShim = new TransactionNotifyShim(this, managedIdentifier); + + // Get the transaction id. + transaction.GetTransactionInfo(out OletxXactTransInfo xactInfo); + + // Register for outcome events. + var pContainer = (IConnectionPointContainer)transaction; + var guid = Guids.IID_ITransactionOutcomeEvents_Guid; + pContainer.FindConnectionPoint(ref guid, out IConnectionPoint? pConnPoint); + pConnPoint!.Advise(transactionNotifyShim, out int connPointCookie); + + var transactionShim = new TransactionShim(this, transactionNotifyShim, transaction); + pTransactionIdentifier = xactInfo.Uow; + pIsolationLevel = xactInfo.IsoLevel; + ppTransactionShim = transactionShim; + } + + private ITransactionOptions GetCachedOptions() + { + if (_cachedOptions.TryDequeue(out ITransactionOptions? options)) + { + return options; + } + + _transactionDispenser.GetOptionsObject(out ITransactionOptions? transactionOptions); + return transactionOptions; + } + + internal void ReturnCachedOptions(ITransactionOptions options) + => _cachedOptions.Enqueue(options); + + internal ITransactionTransmitter GetCachedTransmitter(ITransaction transaction) + { + if (!_cachedTransmitters.TryDequeue(out ITransactionTransmitter? transmitter)) + { + var transmitterFactory = (ITransactionTransmitterFactory)_transactionDispenser; + transmitterFactory.Create(out transmitter); + } + + transmitter.Set(transaction); + + return transmitter; + } + + internal void ReturnCachedTransmitter(ITransactionTransmitter transmitter) + => _cachedTransmitters.Enqueue(transmitter); + + internal ITransactionReceiver GetCachedReceiver() + { + if (_cachedReceivers.TryDequeue(out ITransactionReceiver? receiver)) + { + return receiver; + } + + var receiverFactory = (ITransactionReceiverFactory)_transactionDispenser; + receiverFactory.Create(out ITransactionReceiver transactionReceiver); + + return transactionReceiver; + } + + internal void ReturnCachedReceiver(ITransactionReceiver receiver) + => _cachedReceivers.Enqueue(receiver); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentNotifyShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentNotifyShim.cs new file mode 100644 index 0000000000000..b8f46ce85bd7f --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentNotifyShim.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Transactions.DtcProxyShim.DtcInterfaces; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class EnlistmentNotifyShim : NotificationShimBase, ITransactionResourceAsync +{ + internal ITransactionEnlistmentAsync? EnlistmentAsync; + + // MSDTCPRX behaves unpredictably in that if the TM is down when we vote + // no it will send an AbortRequest. However if the TM does not go down + // the enlistment is not go down the AbortRequest is not sent. This + // makes reliable cleanup a problem. To work around this the enlisment + // shim will eat the AbortRequest if it knows that it has voted No. + + // On Win2k this same problem applies to responding Committed to a + // single phase commit request. + private bool _ignoreSpuriousProxyNotifications; + + internal EnlistmentNotifyShim(DtcProxyShimFactory shimFactory, OletxEnlistment enlistmentIdentifier) + : base(shimFactory, enlistmentIdentifier) + { + _ignoreSpuriousProxyNotifications = false; + } + + internal void SetIgnoreSpuriousProxyNotifications() + => _ignoreSpuriousProxyNotifications = true; + + public void PrepareRequest(bool fRetaining, OletxXactRm grfRM, bool fWantMoniker, bool fSinglePhase) + { + ITransactionEnlistmentAsync? pEnlistmentAsync = Interlocked.Exchange(ref EnlistmentAsync, null); + + if (pEnlistmentAsync is null) + { + throw new InvalidOperationException("Unexpected null in pEnlistmentAsync"); + } + + var pPrepareInfo = (IPrepareInfo)pEnlistmentAsync; + pPrepareInfo.GetPrepareInfoSize(out uint prepareInfoLength); + var prepareInfoBuffer = new byte[prepareInfoLength]; + pPrepareInfo.GetPrepareInfo(prepareInfoBuffer); + + PrepareInfo = prepareInfoBuffer; + IsSinglePhase = fSinglePhase; + NotificationType = ShimNotificationType.PrepareRequestNotify; + ShimFactory.NewNotification(this); + } + + public void CommitRequest(OletxXactRm grfRM, IntPtr pNewUOW) + { + NotificationType = ShimNotificationType.CommitRequestNotify; + ShimFactory.NewNotification(this); + } + + public void AbortRequest(IntPtr pboidReason, bool fRetaining, IntPtr pNewUOW) + { + if (!_ignoreSpuriousProxyNotifications) + { + // Only create the notification if we have not already voted. + NotificationType = ShimNotificationType.AbortRequestNotify; + ShimFactory.NewNotification(this); + } + } + + public void TMDown() + { + NotificationType = ShimNotificationType.ResourceManagerTmDownNotify; + ShimFactory.NewNotification(this); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentShim.cs new file mode 100644 index 0000000000000..844bc36f9d76c --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/EnlistmentShim.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class EnlistmentShim +{ + private readonly EnlistmentNotifyShim _enlistmentNotifyShim; + + internal ITransactionEnlistmentAsync? EnlistmentAsync { get; set; } + + internal EnlistmentShim(EnlistmentNotifyShim notifyShim) + => _enlistmentNotifyShim = notifyShim; + + public void PrepareRequestDone(OletxPrepareVoteType voteType) + { + var voteHr = OletxHelper.S_OK; + var releaseEnlistment = false; + + switch (voteType) + { + case OletxPrepareVoteType.ReadOnly: + { + // On W2k Proxy may send a spurious aborted notification if the TM goes down. + _enlistmentNotifyShim.SetIgnoreSpuriousProxyNotifications(); + voteHr = OletxHelper.XACT_S_READONLY; + break; + } + + case OletxPrepareVoteType.SinglePhase: + { + // On W2k Proxy may send a spurious aborted notification if the TM goes down. + _enlistmentNotifyShim.SetIgnoreSpuriousProxyNotifications(); + voteHr = OletxHelper.XACT_S_SINGLEPHASE; + break; + } + + case OletxPrepareVoteType.Prepared: + { + voteHr = OletxHelper.S_OK; + break; + } + + case OletxPrepareVoteType.Failed: + { + // Proxy may send a spurious aborted notification if the TM goes down. + _enlistmentNotifyShim.SetIgnoreSpuriousProxyNotifications(); + voteHr = OletxHelper.E_FAIL; + break; + } + + case OletxPrepareVoteType.InDoubt: + { + releaseEnlistment = true; + break; + } + + default: // unexpected, vote no. + { + voteHr = OletxHelper.E_FAIL; + break; + } + } + + if (!releaseEnlistment) + { + EnlistmentAsync!.PrepareRequestDone( + voteHr, + IntPtr.Zero, + IntPtr.Zero); + } + } + + public void CommitRequestDone() + => EnlistmentAsync!.CommitRequestDone(OletxHelper.S_OK); + + public void AbortRequestDone() + => EnlistmentAsync!.AbortRequestDone(OletxHelper.S_OK); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Guids.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Guids.cs new file mode 100644 index 0000000000000..9e395a2154037 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Guids.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace System.Transactions.DtcProxyShim; + +internal static class Guids +{ + internal const string IID_ITransactionDispenser = "3A6AD9E1-23B9-11cf-AD60-00AA00A74CCD"; + internal const string IID_IResourceManager = "13741D21-87EB-11CE-8081-0080C758527E"; + internal const string IID_ITransactionOutcomeEvents = "3A6AD9E2-23B9-11cf-AD60-00AA00A74CCD"; + internal const string IID_ITransaction = "0fb15084-af41-11ce-bd2b-204c4f4f5020"; + + internal static readonly Guid IID_ITransactionDispenser_Guid = Guid.Parse(IID_ITransactionDispenser); + internal static readonly Guid IID_IResourceManager_Guid = Guid.Parse(IID_IResourceManager); + internal static readonly Guid IID_ITransactionOutcomeEvents_Guid = Guid.Parse(IID_ITransactionOutcomeEvents); + internal static readonly Guid IID_ITransaction_Guid = Guid.Parse(IID_ITransaction); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NativeEnums.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NativeEnums.cs new file mode 100644 index 0000000000000..f852af728def2 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NativeEnums.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Transactions.DtcProxyShim; + +internal enum ShimNotificationType +{ + None = 0, + Phase0RequestNotify = 1, + VoteRequestNotify = 2, + PrepareRequestNotify = 3, + CommitRequestNotify = 4, + AbortRequestNotify = 5, + CommittedNotify = 6, + AbortedNotify = 7, + InDoubtNotify = 8, + EnlistmentTmDownNotify = 9, + ResourceManagerTmDownNotify = 10 +} + +internal enum OletxPrepareVoteType +{ + ReadOnly = 0, + SinglePhase = 1, + Prepared = 2, + Failed = 3, + InDoubt = 4 +} + +internal enum OletxTransactionOutcome +{ + NotKnownYet = 0, + Committed = 1, + Aborted = 2 +} + +internal enum OletxTransactionIsolationLevel +{ + ISOLATIONLEVEL_UNSPECIFIED = -1, + ISOLATIONLEVEL_CHAOS = 0x10, + ISOLATIONLEVEL_READUNCOMMITTED = 0x100, + ISOLATIONLEVEL_BROWSE = 0x100, + ISOLATIONLEVEL_CURSORSTABILITY = 0x1000, + ISOLATIONLEVEL_READCOMMITTED = 0x1000, + ISOLATIONLEVEL_REPEATABLEREAD = 0x10000, + ISOLATIONLEVEL_SERIALIZABLE = 0x100000, + ISOLATIONLEVEL_ISOLATED = 0x100000 +} + +[Flags] +internal enum OletxTransactionIsoFlags +{ + ISOFLAG_NONE = 0, + ISOFLAG_RETAIN_COMMIT_DC = 1, + ISOFLAG_RETAIN_COMMIT = 2, + ISOFLAG_RETAIN_COMMIT_NO = 3, + ISOFLAG_RETAIN_ABORT_DC = 4, + ISOFLAG_RETAIN_ABORT = 8, + ISOFLAG_RETAIN_ABORT_NO = 12, + ISOFLAG_RETAIN_DONTCARE = ISOFLAG_RETAIN_COMMIT_DC | ISOFLAG_RETAIN_ABORT_DC, + ISOFLAG_RETAIN_BOTH = ISOFLAG_RETAIN_COMMIT | ISOFLAG_RETAIN_ABORT, + ISOFLAG_RETAIN_NONE = ISOFLAG_RETAIN_COMMIT_NO | ISOFLAG_RETAIN_ABORT_NO, + ISOFLAG_OPTIMISTIC = 16, + ISOFLAG_READONLY = 32 +} + +internal enum OletxXacttc : uint +{ + XACTTC_NONE = 0, + XACTTC_SYNC_PHASEONE = 1, + XACTTC_SYNC_PHASETWO = 2, + XACTTC_SYNC = 2, + XACTTC_ASYNC_PHASEONE = 4, + XACTTC_ASYNC = 4 +} + +internal enum OletxXactRm : uint +{ + XACTRM_OPTIMISTICLASTWINS = 1, + XACTRM_NOREADONLYPREPARES = 2 +} + +internal enum OletxTransactionStatus +{ + OLETX_TRANSACTION_STATUS_NONE = 0, + OLETX_TRANSACTION_STATUS_OPENNORMAL = 0x1, + OLETX_TRANSACTION_STATUS_OPENREFUSED = 0x2, + OLETX_TRANSACTION_STATUS_PREPARING = 0x4, + OLETX_TRANSACTION_STATUS_PREPARED = 0x8, + OLETX_TRANSACTION_STATUS_PREPARERETAINING = 0x10, + OLETX_TRANSACTION_STATUS_PREPARERETAINED = 0x20, + OLETX_TRANSACTION_STATUS_COMMITTING = 0x40, + OLETX_TRANSACTION_STATUS_COMMITRETAINING = 0x80, + OLETX_TRANSACTION_STATUS_ABORTING = 0x100, + OLETX_TRANSACTION_STATUS_ABORTED = 0x200, + OLETX_TRANSACTION_STATUS_COMMITTED = 0x400, + OLETX_TRANSACTION_STATUS_HEURISTIC_ABORT = 0x800, + OLETX_TRANSACTION_STATUS_HEURISTIC_COMMIT = 0x1000, + OLETX_TRANSACTION_STATUS_HEURISTIC_DAMAGE = 0x2000, + OLETX_TRANSACTION_STATUS_HEURISTIC_DANGER = 0x4000, + OLETX_TRANSACTION_STATUS_FORCED_ABORT = 0x8000, + OLETX_TRANSACTION_STATUS_FORCED_COMMIT = 0x10000, + OLETX_TRANSACTION_STATUS_INDOUBT = 0x20000, + OLETX_TRANSACTION_STATUS_CLOSED = 0x40000, + OLETX_TRANSACTION_STATUS_OPEN = 0x3, + OLETX_TRANSACTION_STATUS_NOTPREPARED = 0x7ffc3, + OLETX_TRANSACTION_STATUS_ALL = 0x7ffff +} + +internal enum OletxTransactionHeuristic : uint +{ + XACTHEURISTIC_ABORT = 1, + XACTHEURISTIC_COMMIT = 2, + XACTHEURISTIC_DAMAGE = 3, + XACTHEURISTIC_DANGER = 4 +} + +internal enum OletxXactStat +{ + XACTSTAT_NONE = 0, + XACTSTAT_OPENNORMAL = 0x1, + XACTSTAT_OPENREFUSED = 0x2, + XACTSTAT_PREPARING = 0x4, + XACTSTAT_PREPARED = 0x8, + XACTSTAT_PREPARERETAINING = 0x10, + XACTSTAT_PREPARERETAINED = 0x20, + XACTSTAT_COMMITTING = 0x40, + XACTSTAT_COMMITRETAINING = 0x80, + XACTSTAT_ABORTING = 0x100, + XACTSTAT_ABORTED = 0x200, + XACTSTAT_COMMITTED = 0x400, + XACTSTAT_HEURISTIC_ABORT = 0x800, + XACTSTAT_HEURISTIC_COMMIT = 0x1000, + XACTSTAT_HEURISTIC_DAMAGE = 0x2000, + XACTSTAT_HEURISTIC_DANGER = 0x4000, + XACTSTAT_FORCED_ABORT = 0x8000, + XACTSTAT_FORCED_COMMIT = 0x10000, + XACTSTAT_INDOUBT = 0x20000, + XACTSTAT_CLOSED = 0x40000, + XACTSTAT_OPEN = 0x3, + XACTSTAT_NOTPREPARED = 0x7ffc3, + XACTSTAT_ALL = 0x7ffff +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NotificationShimBase.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NotificationShimBase.cs new file mode 100644 index 0000000000000..bed600488c502 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/NotificationShimBase.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Transactions.DtcProxyShim; + +internal class NotificationShimBase +{ + public object? EnlistmentIdentifier; + public ShimNotificationType NotificationType; + public bool AbortingHint; + public bool IsSinglePhase; + public byte[]? PrepareInfo; + + protected DtcProxyShimFactory ShimFactory; + + internal NotificationShimBase(DtcProxyShimFactory shimFactory, object? enlistmentIdentifier) + { + ShimFactory = shimFactory; + EnlistmentIdentifier = enlistmentIdentifier; + NotificationType = ShimNotificationType.None; + AbortingHint = false; + IsSinglePhase = false; + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxHelper.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxHelper.cs new file mode 100644 index 0000000000000..ce0bb80e05997 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxHelper.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Transactions.DtcProxyShim; + +internal static class OletxHelper +{ + private const int RetryInterval = 50; // in milliseconds + private const int MaxRetryCount = 100; + + internal static int S_OK = 0; + internal static int E_FAIL = -2147467259; // 0x80004005, -2147467259 + internal static int XACT_S_READONLY = 315394; // 0x0004D002, 315394 + internal static int XACT_S_SINGLEPHASE = 315401; // 0x0004D009, 315401 + internal static int XACT_E_ABORTED = -2147168231; // 0x8004D019, -2147168231 + internal static int XACT_E_NOTRANSACTION = -2147168242; // 0x8004D00E, -2147168242 + internal static int XACT_E_CONNECTION_DOWN = -2147168228; // 0x8004D01C, -2147168228 + internal static int XACT_E_REENLISTTIMEOUT = -2147168226; // 0x8004D01E, -2147168226 + internal static int XACT_E_RECOVERYALREADYDONE = -2147167996; // 0x8004D104, -2147167996 + internal static int XACT_E_TMNOTAVAILABLE = -2147168229; // 0x8004d01b, -2147168229 + internal static int XACT_E_INDOUBT = -2147168234; // 0x8004d016, + internal static int XACT_E_ALREADYINPROGRESS = -2147168232; // x08004d018, + internal static int XACT_E_TOOMANY_ENLISTMENTS = -2147167999; // 0x8004d101 + internal static int XACT_E_PROTOCOL = -2147167995; // 8004d105 + internal static int XACT_E_FIRST = -2147168256; // 0x8004D000 + internal static int XACT_E_LAST = -2147168215; // 0x8004D029 + internal static int XACT_E_NOTSUPPORTED = -2147168241; // 0x8004D00F + internal static int XACT_E_NETWORK_TX_DISABLED = -2147168220; // 0x8004D024 + + internal static void Retry(Action action) + { + int nRetries = MaxRetryCount; + + while (true) + { + try + { + action(); + return; + } + catch (COMException e) when (e.ErrorCode == XACT_E_ALREADYINPROGRESS) + { + if (--nRetries == 0) + { + throw; + } + + Thread.Sleep(RetryInterval); + } + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxXactTransInfo.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxXactTransInfo.cs new file mode 100644 index 0000000000000..1d097d97b78d5 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/OletxXactTransInfo.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim; + +[ComVisible(false)] +[StructLayout(LayoutKind.Sequential)] +internal struct OletxXactTransInfo +{ + internal Guid Uow; + internal OletxTransactionIsolationLevel IsoLevel; + internal OletxTransactionIsoFlags IsoFlags; + internal int GrfTCSupported; + internal int GrfRMSupported; + internal int GrfTCSupportedRetaining; + internal int GrfRMSupportedRetaining; +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0NotifyShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0NotifyShim.cs new file mode 100644 index 0000000000000..dc2d19226225c --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0NotifyShim.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Transactions.DtcProxyShim.DtcInterfaces; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class Phase0NotifyShim : NotificationShimBase, ITransactionPhase0NotifyAsync +{ + internal Phase0NotifyShim(DtcProxyShimFactory shimFactory, object enlistmentIdentifier) + : base(shimFactory, enlistmentIdentifier) + { + } + + public void Phase0Request(bool fAbortHint) + { + AbortingHint = fAbortHint; + NotificationType = ShimNotificationType.Phase0RequestNotify; + ShimFactory.NewNotification(this); + } + + public void EnlistCompleted(int status) + { + // We don't care about these. The managed code waited for the enlistment to be completed. + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0Shim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0Shim.cs new file mode 100644 index 0000000000000..511c52a7f7eb3 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Phase0Shim.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class Phase0EnlistmentShim +{ + private Phase0NotifyShim _phase0NotifyShim; + + internal ITransactionPhase0EnlistmentAsync? Phase0EnlistmentAsync { get; set; } + + internal Phase0EnlistmentShim(Phase0NotifyShim notifyShim) + => _phase0NotifyShim = notifyShim; + + public void Unenlist() + { + // VSWhidbey 405624 - There is a race between the enlistment and abort of a transaction + // that could cause out proxy interface to already be released when Unenlist is called. + Phase0EnlistmentAsync?.Unenlist(); + } + + public void Phase0Done(bool voteYes) + { + if (voteYes) + { + try + { + Phase0EnlistmentAsync!.Phase0Done(); + } + catch (COMException e) when (e.ErrorCode == OletxHelper.XACT_E_PROTOCOL) + { + // Deal with the proxy bug where we get a Phase0Request(false) on a + // TMDown and the proxy object state is not changed. + return; + } + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerNotifyShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerNotifyShim.cs new file mode 100644 index 0000000000000..53ac780b69659 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerNotifyShim.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Transactions.Oletx; +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class ResourceManagerNotifyShim : NotificationShimBase, IResourceManagerSink +{ + internal ResourceManagerNotifyShim( + DtcProxyShimFactory shimFactory, + object enlistmentIdentifier) + : base(shimFactory, enlistmentIdentifier) + { + } + + public void TMDown() + { + NotificationType = ShimNotificationType.ResourceManagerTmDownNotify; + ShimFactory.NewNotification(this); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerShim.cs new file mode 100644 index 0000000000000..f9dff75d4b7d7 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/ResourceManagerShim.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Transactions.Oletx; +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class ResourceManagerShim +{ + private readonly DtcProxyShimFactory _shimFactory; + + internal ResourceManagerShim(DtcProxyShimFactory shimFactory) + => _shimFactory = shimFactory; + + public IResourceManager? ResourceManager { get; set; } + + public void Enlist( + TransactionShim transactionShim, + OletxEnlistment managedIdentifier, + out EnlistmentShim enlistmentShim) + { + var pEnlistmentNotifyShim = new EnlistmentNotifyShim(_shimFactory, managedIdentifier); + var pEnlistmentShim = new EnlistmentShim(pEnlistmentNotifyShim); + + ITransaction transaction = transactionShim.Transaction; + ResourceManager!.Enlist(transaction, pEnlistmentNotifyShim, out Guid txUow, out OletxTransactionIsolationLevel isoLevel, out ITransactionEnlistmentAsync pEnlistmentAsync); + + pEnlistmentNotifyShim.EnlistmentAsync = pEnlistmentAsync; + pEnlistmentShim.EnlistmentAsync = pEnlistmentAsync; + + enlistmentShim = pEnlistmentShim; + } + + public void Reenlist(byte[] prepareInfo, out OletxTransactionOutcome outcome) + { + // Call Reenlist on the proxy, waiting for 5 milliseconds for it to get the outcome. If it doesn't know that outcome in that + // amount of time, tell the caller we don't know the outcome yet. The managed code will reschedule the check by using the + // ReenlistThread. + try + { + ResourceManager!.Reenlist(prepareInfo, (uint)prepareInfo.Length, 5, out OletxXactStat xactStatus); + outcome = xactStatus switch + { + OletxXactStat.XACTSTAT_ABORTED => OletxTransactionOutcome.Aborted, + OletxXactStat.XACTSTAT_COMMITTED => OletxTransactionOutcome.Committed, + _ => OletxTransactionOutcome.Aborted + }; + } + catch (COMException e) when (e.ErrorCode == OletxHelper.XACT_E_REENLISTTIMEOUT) + { + outcome = OletxTransactionOutcome.NotKnownYet; + return; + } + } + + public void ReenlistComplete() + => ResourceManager!.ReenlistmentComplete(); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionNotifyShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionNotifyShim.cs new file mode 100644 index 0000000000000..d605f13053a00 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionNotifyShim.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class TransactionNotifyShim : NotificationShimBase, ITransactionOutcomeEvents +{ + internal TransactionNotifyShim(DtcProxyShimFactory shimFactory, object? enlistmentIdentifier) + : base(shimFactory, enlistmentIdentifier) + { + } + + public void Committed(bool fRetaining, IntPtr pNewUOW, int hresult) + { + NotificationType = ShimNotificationType.CommittedNotify; + ShimFactory.NewNotification(this); + } + + public void Aborted(IntPtr pboidReason, bool fRetaining, IntPtr pNewUOW, int hresult) + { + NotificationType = ShimNotificationType.AbortedNotify; + ShimFactory.NewNotification(this); + } + + public void HeuristicDecision(OletxTransactionHeuristic dwDecision, IntPtr pboidReason, int hresult) + { + NotificationType = dwDecision switch + { + OletxTransactionHeuristic.XACTHEURISTIC_ABORT => ShimNotificationType.AbortedNotify, + OletxTransactionHeuristic.XACTHEURISTIC_COMMIT => ShimNotificationType.CommittedNotify, + _ => ShimNotificationType.InDoubtNotify + }; + + ShimFactory.NewNotification(this); + } + + public void Indoubt() + { + NotificationType = ShimNotificationType.InDoubtNotify; + ShimFactory.NewNotification(this); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionOutcome.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionOutcome.cs new file mode 100644 index 0000000000000..eb7ec1d460edb --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionOutcome.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Transactions.DtcProxyShim; + +internal enum TransactionOutcome +{ + NotKnownYet = 0, + Committed = 1, + Aborted = 2 +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionShim.cs new file mode 100644 index 0000000000000..349d051999f52 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/TransactionShim.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Transactions.DtcProxyShim.DtcInterfaces; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class TransactionShim +{ + private DtcProxyShimFactory _shimFactory; + private TransactionNotifyShim _transactionNotifyShim; + + internal ITransaction Transaction { get; set; } + + internal TransactionShim(DtcProxyShimFactory shimFactory, TransactionNotifyShim notifyShim, ITransaction transaction) + { + _shimFactory = shimFactory; + _transactionNotifyShim = notifyShim; + Transaction = transaction; + } + + public void Commit() + => Transaction.Commit(false, OletxXacttc.XACTTC_ASYNC, 0); + + public void Abort() + => Transaction.Abort(IntPtr.Zero, false, false); + + public void CreateVoter(OletxPhase1VolatileEnlistmentContainer managedIdentifier, out VoterBallotShim voterBallotShim) + { + var voterNotifyShim = new VoterNotifyShim(_shimFactory, managedIdentifier); + var voterShim = new VoterBallotShim(_shimFactory, voterNotifyShim); + _shimFactory.VoterFactory.Create(Transaction, voterNotifyShim, out ITransactionVoterBallotAsync2 voterBallot); + voterShim.VoterBallotAsync2 = voterBallot; + voterBallotShim = voterShim; + } + + public void Export(byte[] whereabouts, out byte[] cookieBuffer) + { + _shimFactory.ExportFactory.Create((uint)whereabouts.Length, whereabouts, out ITransactionExport export); + + uint cookieSizeULong = 0; + + OletxHelper.Retry(() => export.Export(Transaction, out cookieSizeULong)); + + var cookieSize = (uint)cookieSizeULong; + var buffer = new byte[cookieSize]; + uint bytesUsed = 0; + + OletxHelper.Retry(() => export.GetTransactionCookie(Transaction, cookieSize, buffer, out bytesUsed)); + + cookieBuffer = buffer; + } + + public void GetITransactionNative(out IDtcTransaction transactionNative) + { + var cloner = (ITransactionCloner)Transaction; + cloner.CloneWithCommitDisabled(out ITransaction returnTransaction); + + transactionNative = (IDtcTransaction)returnTransaction; + } + + public unsafe byte[] GetPropagationToken() + { + ITransactionTransmitter transmitter = _shimFactory.GetCachedTransmitter(Transaction); + + try + { + transmitter.GetPropagationTokenSize(out uint propagationTokenSizeULong); + + var propagationTokenSize = (int)propagationTokenSizeULong; + var propagationToken = new byte[propagationTokenSize]; + + transmitter.MarshalPropagationToken((uint)propagationTokenSize, propagationToken, out uint propagationTokenSizeUsed); + + return propagationToken; + } + finally + { + _shimFactory.ReturnCachedTransmitter(transmitter); + } + } + + public void Phase0Enlist(object managedIdentifier, out Phase0EnlistmentShim phase0EnlistmentShim) + { + var phase0Factory = (ITransactionPhase0Factory)Transaction; + var phase0NotifyShim = new Phase0NotifyShim(_shimFactory, managedIdentifier); + var phase0Shim = new Phase0EnlistmentShim(phase0NotifyShim); + + phase0Factory.Create(phase0NotifyShim, out ITransactionPhase0EnlistmentAsync phase0Async); + phase0Shim.Phase0EnlistmentAsync = phase0Async; + + phase0Async.Enable(); + phase0Async.WaitForEnlistment(); + + phase0EnlistmentShim = phase0Shim; + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterNotifyShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterNotifyShim.cs new file mode 100644 index 0000000000000..fad9958acbef9 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterNotifyShim.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Transactions.DtcProxyShim.DtcInterfaces; +using System.Transactions.Oletx; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class VoterNotifyShim : NotificationShimBase, ITransactionVoterNotifyAsync2 +{ + internal VoterNotifyShim(DtcProxyShimFactory shimFactory, object enlistmentIdentifier) + : base(shimFactory, enlistmentIdentifier) + { + } + + public void VoteRequest() + { + NotificationType = ShimNotificationType.VoteRequestNotify; + ShimFactory.NewNotification(this); + } + + public void Committed([MarshalAs(UnmanagedType.Bool)] bool fRetaining, IntPtr pNewUOW, uint hresult) + { + NotificationType = ShimNotificationType.CommittedNotify; + ShimFactory.NewNotification(this); + } + + public void Aborted(IntPtr pboidReason, [MarshalAs(UnmanagedType.Bool)] bool fRetaining, IntPtr pNewUOW, uint hresult) + { + NotificationType = ShimNotificationType.AbortedNotify; + ShimFactory.NewNotification(this); + } + + public void HeuristicDecision([MarshalAs(UnmanagedType.U4)] OletxTransactionHeuristic dwDecision, IntPtr pboidReason, uint hresult) + { + NotificationType = dwDecision switch { + OletxTransactionHeuristic.XACTHEURISTIC_ABORT => ShimNotificationType.AbortedNotify, + OletxTransactionHeuristic.XACTHEURISTIC_COMMIT => ShimNotificationType.CommittedNotify, + _ => ShimNotificationType.InDoubtNotify + }; + + ShimFactory.NewNotification(this); + } + + public void Indoubt() + { + NotificationType = ShimNotificationType.InDoubtNotify; + ShimFactory.NewNotification(this); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterShim.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterShim.cs new file mode 100644 index 0000000000000..db354738f7fc1 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/VoterShim.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Transactions.DtcProxyShim.DtcInterfaces; + +namespace System.Transactions.DtcProxyShim; + +internal sealed class VoterBallotShim +{ + private VoterNotifyShim _voterNotifyShim; + + internal ITransactionVoterBallotAsync2? VoterBallotAsync2 { get; set; } + + internal VoterBallotShim(DtcProxyShimFactory shimFactory, VoterNotifyShim notifyShim) + => _voterNotifyShim = notifyShim; + + public void Vote(bool voteYes) + { + int voteHr = OletxHelper.S_OK; + + if (!voteYes) + { + voteHr = OletxHelper.E_FAIL; + } + + VoterBallotAsync2!.VoteRequestDone(voteHr, IntPtr.Zero); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Xactopt.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Xactopt.cs new file mode 100644 index 0000000000000..2f6bac5637eeb --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/Xactopt.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.Transactions.DtcProxyShim; + +// https://docs.microsoft.com/previous-versions/windows/desktop/ms679195(v=vs.85) +[StructLayout(LayoutKind.Sequential)] +internal struct Xactopt +{ + internal Xactopt(uint ulTimeout, string szDescription) + => (UlTimeout, SzDescription) = (ulTimeout, szDescription); + + public uint UlTimeout; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 40)] + public string SzDescription; +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DurableEnlistmentState.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DurableEnlistmentState.cs index 8c0a9c3e8464b..f434ea6a67eab 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/DurableEnlistmentState.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DurableEnlistmentState.cs @@ -96,7 +96,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.Rollback); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.Rollback); } // Send the Rollback notification to the enlistment @@ -147,7 +147,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.SinglePhaseCommit); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.SinglePhaseCommit); } // Send the Commit notification to the enlistment diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Enlistment.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Enlistment.cs index e19dae454de15..7d23de2aad39d 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/Enlistment.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Enlistment.cs @@ -29,7 +29,7 @@ internal interface IPromotedEnlistment byte[] GetRecoveryInformation(); - InternalEnlistment InternalEnlistment + InternalEnlistment? InternalEnlistment { get; set; @@ -270,36 +270,28 @@ void ISinglePhaseNotificationInternal.SinglePhaseCommit(IPromotedEnlistment sing } } - void IEnlistmentNotificationInternal.Prepare( - IPromotedEnlistment preparingEnlistment - ) + void IEnlistmentNotificationInternal.Prepare(IPromotedEnlistment preparingEnlistment) { Debug.Assert(_twoPhaseNotifications != null); _promotedEnlistment = preparingEnlistment; _twoPhaseNotifications.Prepare(PreparingEnlistment); } - void IEnlistmentNotificationInternal.Commit( - IPromotedEnlistment enlistment - ) + void IEnlistmentNotificationInternal.Commit(IPromotedEnlistment enlistment) { Debug.Assert(_twoPhaseNotifications != null); _promotedEnlistment = enlistment; _twoPhaseNotifications.Commit(Enlistment); } - void IEnlistmentNotificationInternal.Rollback( - IPromotedEnlistment enlistment - ) + void IEnlistmentNotificationInternal.Rollback(IPromotedEnlistment enlistment) { Debug.Assert(_twoPhaseNotifications != null); _promotedEnlistment = enlistment; _twoPhaseNotifications.Rollback(Enlistment); } - void IEnlistmentNotificationInternal.InDoubt( - IPromotedEnlistment enlistment - ) + void IEnlistmentNotificationInternal.InDoubt(IPromotedEnlistment enlistment) { Debug.Assert(_twoPhaseNotifications != null); _promotedEnlistment = enlistment; diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/IDtcTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/IDtcTransaction.cs new file mode 100644 index 0000000000000..e0796c1b3c622 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/IDtcTransaction.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Transactions +{ + [ComImport] + [Guid("0fb15084-af41-11ce-bd2b-204c4f4f5020")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IDtcTransaction + { + void Commit(int retaining, [MarshalAs(UnmanagedType.I4)] int commitType, int reserved); + + void Abort(IntPtr reason, int retaining, int async); + + void GetTransactionInfo(IntPtr transactionInformation); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/InternalTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/InternalTransaction.cs index 581c9552a60a5..42e1d4c461ed5 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/InternalTransaction.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/InternalTransaction.cs @@ -5,7 +5,8 @@ using System.Diagnostics; using System.Globalization; using System.Threading; -using System.Transactions.Distributed; +using System.Transactions.Oletx; +using OletxTransaction = System.Transactions.Oletx.OletxTransaction; namespace System.Transactions { @@ -93,7 +94,7 @@ internal long CreationTime // These members are used for promoted waves of dependent blocking clones. The Ltm // does not register individually for each blocking clone created in phase 0. Instead // it multiplexes a single phase 0 blocking clone only created after phase 0 has started. - internal DistributedDependentTransaction? _phase0WaveDependentClone; + internal OletxDependentTransaction? _phase0WaveDependentClone; internal int _phase0WaveDependentCloneCount; // These members are used for keeping track of aborting dependent clones if we promote @@ -105,7 +106,7 @@ internal long CreationTime // on the distributed TM takes care of checking to make sure all the aborting dependent // clones have completed as part of its Prepare processing. These are used in conjunction with // phase1volatiles.dependentclones. - internal DistributedDependentTransaction? _abortingDependentClone; + internal OletxDependentTransaction? _abortingDependentClone; internal int _abortingDependentCloneCount; // When the size of the volatile enlistment array grows increase it by this amount. @@ -119,10 +120,10 @@ internal long CreationTime internal TransactionCompletedEventHandler? _transactionCompletedDelegate; // If this transaction get's promoted keep a reference to the promoted transaction - private DistributedTransaction? _promotedTransaction; - internal DistributedTransaction? PromotedTransaction + private OletxTransaction? _promotedTransaction; + internal OletxTransaction? PromotedTransaction { - get { return _promotedTransaction; } + get => _promotedTransaction; set { Debug.Assert(_promotedTransaction == null, "A transaction can only be promoted once!"); @@ -248,7 +249,7 @@ internal InternalTransaction(TimeSpan timeout, CommittableTransaction committabl } // Construct an internal transaction - internal InternalTransaction(Transaction outcomeSource, DistributedTransaction distributedTx) + internal InternalTransaction(Transaction outcomeSource, OletxTransaction distributedTx) { _promotedTransaction = distributedTx; diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/NonWindowsUnsupported.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/NonWindowsUnsupported.cs new file mode 100644 index 0000000000000..01d25a58cc96a --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/NonWindowsUnsupported.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Serialization; +using System.Transactions.Oletx; + +// This files contains non-Windows stubs for Windows-only functionality, so that Sys.Tx can build. The APIs below +// are only ever called when a distributed transaction is needed, and throw PlatformNotSupportedException. + +#pragma warning disable CA1822 + +namespace System.Transactions.Oletx +{ + internal sealed class OletxTransactionManager + { + internal object? NodeName { get; set; } + + internal OletxTransactionManager(string nodeName) + { + } + + internal IPromotedEnlistment ReenlistTransaction( + Guid resourceManagerIdentifier, + byte[] resourceManagerRecoveryInformation, + RecoveringInternalEnlistment internalEnlistment) + => throw NotSupported(); + + internal OletxCommittableTransaction CreateTransaction(TransactionOptions options) + => throw NotSupported(); + + internal void ResourceManagerRecoveryComplete(Guid resourceManagerIdentifier) + => throw NotSupported(); + + internal static byte[] GetWhereabouts() + => throw NotSupported(); + + internal static Transaction GetTransactionFromDtcTransaction(IDtcTransaction transactionNative) + => throw NotSupported(); + + internal static OletxTransaction GetTransactionFromExportCookie(byte[] cookie, Guid txId) + => throw NotSupported(); + + internal static OletxTransaction GetOletxTransactionFromTransmitterPropagationToken(byte[] propagationToken) + => throw NotSupported(); + + internal static Exception NotSupported() + => new PlatformNotSupportedException(SR.DistributedNotSupported); + } + + /// + /// A Transaction object represents a single transaction. It is created by TransactionManager + /// objects through CreateTransaction or through deserialization. Alternatively, the static Create + /// methods 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. + /// + internal class OletxTransaction : ISerializable, IObjectReference + { + internal OletxTransaction() + { + } + + protected OletxTransaction(SerializationInfo serializationInfo, StreamingContext context) + { + //if (serializationInfo == null) + //{ + // throw new ArgumentNullException(nameof(serializationInfo)); + //} + + //throw NotSupported(); + throw new PlatformNotSupportedException(); + } + + internal Exception? InnerException { get; set; } + internal Guid Identifier { get; set; } + internal RealOletxTransaction? RealTransaction { get; set; } + internal TransactionTraceIdentifier TransactionTraceId { get; set; } + internal IsolationLevel IsolationLevel { get; set; } + internal Transaction? SavedLtmPromotedTransaction { get; set; } + + internal IPromotedEnlistment EnlistVolatile( + InternalEnlistment internalEnlistment, + EnlistmentOptions enlistmentOptions) + => throw NotSupported(); + + internal IPromotedEnlistment EnlistDurable( + Guid resourceManagerIdentifier, + DurableInternalEnlistment internalEnlistment, + bool v, + EnlistmentOptions enlistmentOptions) + => throw NotSupported(); + + internal void Rollback() + => throw NotSupported(); + + internal OletxDependentTransaction DependentClone(bool delayCommit) + => throw NotSupported(); + + internal IPromotedEnlistment EnlistVolatile( + VolatileDemultiplexer volatileDemux, + EnlistmentOptions enlistmentOptions) + => throw NotSupported(); + + internal static byte[] GetExportCookie(byte[] whereaboutsCopy) + => throw NotSupported(); + + public object GetRealObject(StreamingContext context) + => throw NotSupported(); + + internal static byte[] GetTransmitterPropagationToken() + => throw NotSupported(); + + internal static IDtcTransaction GetDtcTransaction() + => throw NotSupported(); + + void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext context) + { + //if (serializationInfo == null) + //{ + // throw new ArgumentNullException(nameof(serializationInfo)); + //} + + //throw NotSupported(); + + throw new PlatformNotSupportedException(); + } + + internal void Dispose() + { + } + + internal static Exception NotSupported() + => new PlatformNotSupportedException(SR.DistributedNotSupported); + + internal sealed class RealOletxTransaction + { + internal InternalTransaction? InternalTransaction { get; set; } + } + } + + internal sealed class OletxDependentTransaction : OletxTransaction + { + internal void Complete() => throw NotSupported(); + } + + internal sealed class OletxCommittableTransaction : OletxTransaction + { + internal void BeginCommit(InternalTransaction tx) => throw NotSupported(); + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/DtcTransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/DtcTransactionManager.cs new file mode 100644 index 0000000000000..b844f208cccd6 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/DtcTransactionManager.cs @@ -0,0 +1,132 @@ +// 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.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Globalization; +using System.Transactions.DtcProxyShim; + +namespace System.Transactions.Oletx; + +internal sealed class DtcTransactionManager +{ + private readonly string? _nodeName; + private readonly OletxTransactionManager _oletxTm; + private readonly DtcProxyShimFactory _proxyShimFactory; + private byte[]? _whereabouts; + + internal DtcTransactionManager(string? nodeName, OletxTransactionManager oletxTm) + { + _nodeName = nodeName; + _oletxTm = oletxTm; + _proxyShimFactory = OletxTransactionManager.ProxyShimFactory; + } + + [MemberNotNull(nameof(_whereabouts))] + private void Initialize() + { + if (_whereabouts is not null) + { + return; + } + + OletxInternalResourceManager internalRM = _oletxTm.InternalResourceManager; + bool nodeNameMatches; + + try + { + _proxyShimFactory.ConnectToProxy( + _nodeName, + internalRM.Identifier, + internalRM, + out nodeNameMatches, + out _whereabouts, + out ResourceManagerShim resourceManagerShim); + + // If the node name does not match, throw. + if (!nodeNameMatches) + { + throw new NotSupportedException(SR.ProxyCannotSupportMultipleNodeNames); + } + + // Give the IResourceManagerShim to the internalRM and tell it to call ReenlistComplete. + internalRM.ResourceManagerShim = resourceManagerShim; + internalRM.CallReenlistComplete(); + } + catch (COMException ex) + { + if (ex.ErrorCode == OletxHelper.XACT_E_NOTSUPPORTED) + { + throw new NotSupportedException(SR.CannotSupportNodeNameSpecification); + } + + OletxTransactionManager.ProxyException(ex); + + // Unfortunately MSDTCPRX may return unknown error codes when attempting to connect to MSDTC + // that error should be propagated back as a TransactionManagerCommunicationException. + throw TransactionManagerCommunicationException.Create(SR.TransactionManagerCommunicationException, ex); + } + } + + internal DtcProxyShimFactory ProxyShimFactory + { + get + { + if (_whereabouts is null) + { + lock (this) + { + Initialize(); + } + } + + return _proxyShimFactory; + } + } + + internal void ReleaseProxy() + { + lock (this) + { + _whereabouts = null; + } + } + + internal byte[] Whereabouts + { + get + { + if (_whereabouts is null) + { + lock (this) + { + Initialize(); + } + } + + return _whereabouts; + } + } + + internal static uint AdjustTimeout(TimeSpan timeout) + { + uint returnTimeout = 0; + + try + { + returnTimeout = Convert.ToUInt32(timeout.TotalMilliseconds, CultureInfo.CurrentCulture); + } + catch (OverflowException caughtEx) + { + // timeout.TotalMilliseconds might be negative, so let's catch overflow exceptions, just in case. + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, caughtEx); + } + + returnTimeout = uint.MaxValue; + } + return returnTimeout; + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxCommittableTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxCommittableTransaction.cs new file mode 100644 index 0000000000000..9e61ccf368984 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxCommittableTransaction.cs @@ -0,0 +1,57 @@ +// 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; + +namespace System.Transactions.Oletx; + +/// +/// A Transaction object represents a single transaction. It is created by TransactionManager +/// objects through CreateTransaction or UnmarshalTransaction. Alternatively, the static Create +/// methodis 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. +/// +[Serializable] +internal sealed class OletxCommittableTransaction : OletxTransaction +{ + private bool _commitCalled; + + /// + /// Constructor for the Transaction object. Specifies the TransactionManager instance that is + /// creating the transaction. + /// + internal OletxCommittableTransaction(RealOletxTransaction realOletxTransaction) + : base(realOletxTransaction) + { + realOletxTransaction.CommittableTransaction = this; + } + + internal bool CommitCalled => _commitCalled; + + internal void BeginCommit(InternalTransaction internalTransaction) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this); + etwLog.TransactionCommit(TraceSourceType.TraceSourceOleTx, TransactionTraceId, "CommittableTransaction"); + } + + Debug.Assert(0 == Disposed, "OletxTransction object is disposed"); + RealOletxTransaction.InternalTransaction = internalTransaction; + + _commitCalled = true; + + RealOletxTransaction.Commit(); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxCommittableTransaction)}.{nameof(BeginCommit)}"); + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxDependentTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxDependentTransaction.cs new file mode 100644 index 0000000000000..9b2822027a7ab --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxDependentTransaction.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; + +namespace System.Transactions.Oletx; + +[Serializable] +internal sealed class OletxDependentTransaction : OletxTransaction +{ + private OletxVolatileEnlistmentContainer _volatileEnlistmentContainer; + + private int _completed; + + internal OletxDependentTransaction(RealOletxTransaction realTransaction, bool delayCommit) + : base(realTransaction) + { + if (realTransaction == null) + { + throw new ArgumentNullException(nameof(realTransaction)); + } + + _volatileEnlistmentContainer = RealOletxTransaction.AddDependentClone(delayCommit); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.TransactionDependentCloneCreate(TraceSourceType.TraceSourceOleTx, TransactionTraceId, delayCommit + ? DependentCloneOption.BlockCommitUntilComplete + : DependentCloneOption.RollbackIfNotComplete); + } + } + + public void Complete() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(DependentTransaction)}.{nameof(Complete)}"); + } + + Debug.Assert(Disposed == 0, "OletxTransction object is disposed"); + + int localCompleted = Interlocked.Exchange(ref _completed, 1); + if (localCompleted == 1) + { + throw TransactionException.CreateTransactionCompletedException(DistributedTxId); + } + + if (etwLog.IsEnabled()) + { + etwLog.TransactionDependentCloneComplete(TraceSourceType.TraceSourceOleTx, TransactionTraceId, "DependentTransaction"); + } + + _volatileEnlistmentContainer.DependentCloneCompleted(); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(DependentTransaction)}.{nameof(Complete)}"); + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxEnlistment.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxEnlistment.cs new file mode 100644 index 0000000000000..682388828f6c9 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxEnlistment.cs @@ -0,0 +1,1212 @@ +// 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 bool _canDoSinglePhase; + private IEnlistmentNotificationInternal? _iEnlistmentNotification; + // The information that comes from/goes to the proxy. + private byte[]? _proxyPrepareInfoByteArray; + + private bool _isSinglePhase; + private 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); + + byte[] txGuidByteArray = new byte[16]; + Array.Copy(_proxyPrepareInfoByteArray, txGuidByteArray, 16); + + _transactionGuid = new Guid(txGuidByteArray); + 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() + { + int hrResult = OletxHelper.S_OK; + 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 + { + if (localEnlistmentShim != null) + { + 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. + if (localEnlistmentShim != null) + { + 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 + { + if (localEnlistmentShim != null) + { + 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 + { + if (localEnlistmentShim != null) + { + 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(string.Format(null, "this.prepareInfoByteArray == null in RecoveryInformation()")); + throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId); + } + + return _prepareInfoByteArray; + } + + InternalEnlistment? IPromotedEnlistment.InternalEnlistment + { + get => base.InternalEnlistment; + set => base.InternalEnlistment = value; + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxResourceManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxResourceManager.cs new file mode 100644 index 0000000000000..be9a099184c48 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxResourceManager.cs @@ -0,0 +1,896 @@ +// 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.Threading; +using System.Transactions.DtcProxyShim; + +namespace System.Transactions.Oletx; + +internal sealed class OletxResourceManager +{ + internal Guid ResourceManagerIdentifier; + + internal ResourceManagerShim? resourceManagerShim; + internal Hashtable EnlistmentHashtable; + internal static Hashtable VolatileEnlistmentHashtable = new Hashtable(); + internal OletxTransactionManager OletxTransactionManager; + + // reenlistList is a simple ArrayList of OletxEnlistment objects that are either in the + // Preparing or Prepared state when we receive a TMDown notification or have had + // ReenlistTransaction called for them. The ReenlistThread is responsible for traversing this + // list trying to obtain the outcome for the enlistments. All access, read or write, to this + // list should get a lock on the list. + // Special Note: If you are going to lock both the OletxResourceManager object AND the + // reenlistList, lock the reenlistList FIRST. + internal ArrayList ReenlistList; + + // reenlistPendingList is also a simple ArrayList of OletxEnlistment objects. But for these + // we have received the outcome from the proxy and have called into the RM to deliver the + // notification, but the RM has not yet called EnlistmentDone to let us know that the outcome + // has been processed. This list must be empty, in addition to the reenlistList, in order for + // the ReenlistThread to call RecoveryComplete and not be rescheduled. Access to this list + // should be protected by locking the reenlistList. The lists are always accessed together, + // so there is no reason to grab two locks. + internal ArrayList ReenlistPendingList; + + // This is where we keep the reenlistThread and thread timer values. If there is a reenlist thread running, + // reenlistThread will be non-null. If reenlistThreadTimer is non-null, we have a timer scheduled which will + // fire off a reenlist thread when it expires. Only one or the other should be non-null at a time. However, they + // could both be null, which means that there is no reenlist thread running and there is no timer scheduled to + // create one. Access to these members should be done only after obtaining a lock on the OletxResourceManager object. + internal Timer? ReenlistThreadTimer; + internal Thread? reenlistThread; + + // This boolean is set to true if the resource manager application has called RecoveryComplete. + // A lock on the OletxResourceManager instance will be obtained when retrieving or modifying + // this value. Before calling ReenlistComplete on the DTC proxy, this value must be true. + internal bool RecoveryCompleteCalledByApplication { get; set; } + + internal OletxResourceManager(OletxTransactionManager transactionManager, Guid resourceManagerIdentifier) + { + Debug.Assert(transactionManager != null, "Argument is null"); + + // This will get set later, after the resource manager is created with the proxy. + resourceManagerShim = null; + OletxTransactionManager = transactionManager; + ResourceManagerIdentifier = resourceManagerIdentifier; + + EnlistmentHashtable = new Hashtable(); + ReenlistList = new ArrayList(); + ReenlistPendingList = new ArrayList(); + + ReenlistThreadTimer = null; + reenlistThread = null; + RecoveryCompleteCalledByApplication = false; + } + + internal ResourceManagerShim? ResourceManagerShim + { + get + { + ResourceManagerShim? localResourceManagerShim = null; + + if (resourceManagerShim == null) + { + lock (this) + { + if (resourceManagerShim == null) + { + OletxTransactionManager.DtcTransactionManagerLock.AcquireReaderLock( -1 ); + try + { + Guid rmGuid = ResourceManagerIdentifier; + + OletxTransactionManager.DtcTransactionManager.ProxyShimFactory.CreateResourceManager( + rmGuid, + this, + out localResourceManagerShim); + } + catch (COMException ex) + { + if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || + ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + // Just to make sure... + localResourceManagerShim = null; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + else + { + throw; + } + } + catch (TransactionException ex) + { + if (ex.InnerException is COMException comEx) + { + // Tolerate TM down. + if (comEx.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || + comEx.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + // Just to make sure... + localResourceManagerShim = null; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + else + { + throw; + } + } + else + { + throw; + } + } + finally + { + OletxTransactionManager.DtcTransactionManagerLock.ReleaseReaderLock(); + } + Thread.MemoryBarrier(); + resourceManagerShim = localResourceManagerShim; + } + } + } + return resourceManagerShim; + } + + set + { + Debug.Assert(value == null, "set_ResourceManagerShim, value not null"); + resourceManagerShim = value; + } + } + + internal bool CallProxyReenlistComplete() + { + bool success = false; + if (RecoveryCompleteCalledByApplication) + { + ResourceManagerShim? localResourceManagerShim; + try + { + localResourceManagerShim = ResourceManagerShim; + if (localResourceManagerShim != null) + { + localResourceManagerShim.ReenlistComplete(); + success = true; + } + // If we don't have an iResourceManagerOletx, just tell the caller that + // we weren't successful and it will schedule a retry. + } + catch (COMException ex) + { + // If we get a TMDown error, eat it and indicate that we were unsuccessful. + if (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || + ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + success = false; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + + // We might get an XACT_E_RECOVERYALREADYDONE if there are multiple OletxTransactionManager + // objects for the same backend TM. We can safely ignore this error. + else if (ex.ErrorCode != OletxHelper.XACT_E_RECOVERYALREADYDONE) + { + OletxTransactionManager.ProxyException(ex); + throw; + } + // Getting XACT_E_RECOVERYALREADYDONE is considered success. + else + { + success = true; + } + } + finally + { + localResourceManagerShim = null; + } + } + else // The application has not yet called RecoveryComplete, so lie just a little. + { + success = true; + } + + return success; + } + + // This is called by the internal RM when it gets a TM Down notification. This routine will + // tell the enlistments about the TMDown from the internal RM. The enlistments will then + // decide what to do, based on their state. This is mainly to work around COMPlus bug 36760/36758, + // where Phase0 enlistments get Phase0Request( abortHint = false ) when the TM goes down. We want + // to try to avoid telling the application to prepare when we know the transaction will abort. + // We can't do this out of the normal TMDown notification to the RM because it is too late. The + // Phase0Request gets sent before the TMDown notification. + internal void TMDownFromInternalRM(OletxTransactionManager oletxTM) + { + Hashtable localEnlistmentHashtable; + IDictionaryEnumerator enlistEnum; + OletxEnlistment? enlistment; + + // If the internal RM got a TMDown, we will shortly, so null out our ResourceManagerShim now. + ResourceManagerShim = null; + + // Make our own copy of the hashtable of enlistments. + lock (EnlistmentHashtable.SyncRoot) + { + localEnlistmentHashtable = (Hashtable)EnlistmentHashtable.Clone(); + } + + // Tell all of our enlistments that the TM went down. The proxy only + // tells enlistments that are in the Prepared state, but we want our Phase0 + // enlistments to know so they can avoid sending Prepare when they get a + // Phase0Request - COMPlus bug 36760/36758. + enlistEnum = localEnlistmentHashtable.GetEnumerator(); + while (enlistEnum.MoveNext()) + { + enlistment = enlistEnum.Value as OletxEnlistment; + enlistment?.TMDownFromInternalRM(oletxTM); + } + } + + public void TMDown() + { + // The ResourceManagerShim was already set to null by TMDownFromInternalRM, so we don't need to do it again here. + // Just start the ReenlistThread. + StartReenlistThread(); + } + + internal OletxEnlistment EnlistDurable( + OletxTransaction oletxTransaction, + bool canDoSinglePhase, + IEnlistmentNotificationInternal enlistmentNotification, + EnlistmentOptions enlistmentOptions) + { + ResourceManagerShim? localResourceManagerShim; + + Debug.Assert(oletxTransaction != null, "Argument is null" ); + Debug.Assert(enlistmentNotification != null, "Argument is null" ); + + EnlistmentShim enlistmentShim; + Phase0EnlistmentShim phase0Shim; + Guid txUow = Guid.Empty; + bool undecidedEnlistmentsIncremented = false; + + // Create our enlistment object. + OletxEnlistment enlistment = new( + canDoSinglePhase, + enlistmentNotification, + oletxTransaction.RealTransaction.TxGuid, + enlistmentOptions, + this, + oletxTransaction); + + bool enlistmentSucceeded = false; + + try + { + if ((enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0) + { + oletxTransaction.RealTransaction.IncrementUndecidedEnlistments(); + undecidedEnlistmentsIncremented = true; + } + + // This entire sequence needs to be executed before we can go on. + lock (enlistment) + { + try + { + // Do the enlistment on the proxy. + localResourceManagerShim = ResourceManagerShim; + if (localResourceManagerShim == null) + { + // The TM must be down. Throw the appropriate exception. + throw TransactionManagerCommunicationException.Create(SR.TraceSourceOletx, null); + } + + if ((enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0) + { + oletxTransaction.RealTransaction.TransactionShim.Phase0Enlist(enlistment, out phase0Shim); + enlistment.Phase0EnlistmentShim = phase0Shim; + } + + localResourceManagerShim.Enlist(oletxTransaction.RealTransaction.TransactionShim, enlistment, out enlistmentShim); + + enlistment.EnlistmentShim = enlistmentShim; + } + catch (COMException comException) + { + // There is no string mapping for XACT_E_TOOMANY_ENLISTMENTS, so we need to do it here. + if (comException.ErrorCode == OletxHelper.XACT_E_TOOMANY_ENLISTMENTS) + { + throw TransactionException.Create( + SR.OletxTooManyEnlistments, + comException, + enlistment == null ? Guid.Empty : enlistment.DistributedTxId); + } + + OletxTransactionManager.ProxyException(comException); + + throw; + } + } + + enlistmentSucceeded = true; + } + finally + { + if (!enlistmentSucceeded && + (enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0 && + undecidedEnlistmentsIncremented) + { + oletxTransaction.RealTransaction.DecrementUndecidedEnlistments(); + } + } + + return enlistment; + } + + internal OletxEnlistment Reenlist(byte[] prepareInfo, IEnlistmentNotificationInternal enlistmentNotification) + { + OletxTransactionOutcome outcome = OletxTransactionOutcome.NotKnownYet; + OletxTransactionStatus xactStatus = OletxTransactionStatus.OLETX_TRANSACTION_STATUS_NONE; + + if (prepareInfo == null) + { + throw new ArgumentException(SR.InvalidArgument, nameof(prepareInfo)); + } + + // Verify that the resource manager guid in the recovery info matches that of the calling resource manager. + byte[] rmGuidArray = new byte[16]; + for (int i = 0; i < 16; i++) + { + rmGuidArray[i] = prepareInfo[i + 16]; + } + Guid rmGuid = new(rmGuidArray); + if (rmGuid != ResourceManagerIdentifier) + { + throw TransactionException.Create(TraceSourceType.TraceSourceOleTx, SR.ResourceManagerIdDoesNotMatchRecoveryInformation, null); + } + + // Ask the proxy resource manager to reenlist. + ResourceManagerShim? localResourceManagerShim = null; + try + { + localResourceManagerShim = ResourceManagerShim; + if (localResourceManagerShim == null) + { + // The TM must be down. Throw the exception that will get caught below and will cause + // the enlistment to start the ReenlistThread. The TMDown thread will be trying to reestablish + // connection with the TM and will start the reenlist thread when it does. + throw new COMException(SR.DtcTransactionManagerUnavailable, OletxHelper.XACT_E_CONNECTION_DOWN); + } + + // Only wait for 5 milliseconds. If the TM doesn't have the outcome now, we will + // put the enlistment on the reenlistList for later processing. + localResourceManagerShim.Reenlist(prepareInfo, out outcome); + + if (OletxTransactionOutcome.Committed == outcome) + { + xactStatus = OletxTransactionStatus.OLETX_TRANSACTION_STATUS_COMMITTED; + } + else if (OletxTransactionOutcome.Aborted == outcome) + { + xactStatus = OletxTransactionStatus.OLETX_TRANSACTION_STATUS_ABORTED; + } + else // we must not know the outcome yet. + { + xactStatus = OletxTransactionStatus.OLETX_TRANSACTION_STATUS_PREPARED; + StartReenlistThread(); + } + } + catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN ) + { + xactStatus = OletxTransactionStatus.OLETX_TRANSACTION_STATUS_PREPARED; + ResourceManagerShim = null; + StartReenlistThread(); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + finally + { + localResourceManagerShim = null; + } + + // Now create our enlistment to tell the client the outcome. + return new OletxEnlistment(enlistmentNotification, xactStatus, prepareInfo, this); + } + + internal void RecoveryComplete() + { + Timer? localTimer = null; + + // Remember that the application has called RecoveryComplete. + RecoveryCompleteCalledByApplication = true; + + try + { + // Remove the OletxEnlistment objects from the reenlist list because the RM says it doesn't + // have any unresolved transactions, so we don't need to keep asking and the reenlist thread can exit. + // Leave the reenlistPendingList alone. If we have notifications outstanding, we still can't remove those. + lock (ReenlistList) + { + // If the ReenlistThread is not running and there are no reenlistPendingList entries, we need to call ReenlistComplete ourself. + lock (this) + { + if (ReenlistList.Count == 0 && ReenlistPendingList.Count == 0) + { + if (ReenlistThreadTimer != null) + { + // If we have a pending reenlistThreadTimer, cancel it. We do the cancel + // in the finally block to satisfy FXCop. + localTimer = ReenlistThreadTimer; + ReenlistThreadTimer = null; + } + + // Try to tell the proxy ReenlistmentComplete. + bool success = CallProxyReenlistComplete(); + if (!success) + { + // We are now responsible for calling RecoveryComplete. Fire up the ReenlistThread + // to do it for us. + StartReenlistThread(); + } + } + else + { + StartReenlistThread(); + } + } + } + } + finally + { + if (localTimer != null) + { + localTimer.Dispose(); + } + } + } + + internal void StartReenlistThread() + { + // We are not going to check the reenlistList.Count. Just always start the thread. We do this because + // if we get a COMException from calling ReenlistComplete, we start the reenlistThreadTimer to retry it for us + // in the background. + lock (this) + { + // We don't need a MemoryBarrier here because all access to the reenlistThreadTimer member is done while + // holding a lock on the OletxResourceManager object. + if (ReenlistThreadTimer == null && reenlistThread == null) + { + ReenlistThreadTimer = new Timer(ReenlistThread, this, 10, Timeout.Infinite); + } + } + } + + // This routine searches the reenlistPendingList for the specified enlistment and if it finds + // it, removes it from the list. An enlistment calls this routine when it is "finishing" because + // the RM has called EnlistmentDone or it was InDoubt. But it only calls it if the enlistment does NOT + // have a WrappedTransactionEnlistmentAsync value, indicating that it is a recovery enlistment. + internal void RemoveFromReenlistPending(OletxEnlistment enlistment) + { + // We lock the reenlistList because we have decided to lock that list when accessing either + // the reenlistList or the reenlistPendingList. + lock (ReenlistList) + { + // This will do a linear search of the list, but that is what we need to do because + // the enlistments may change indicies while notifications are outstanding. Also, + // this does not throw if the enlistment isn't on the list. + ReenlistPendingList.Remove(enlistment); + + lock (this) + { + // If we have a ReenlistThread timer and both the reenlistList and the reenlistPendingList + // are empty, kick the ReenlistThread now. + if (ReenlistThreadTimer != null && ReenlistList.Count == 0 && ReenlistPendingList.Count == 0) + { + if (!ReenlistThreadTimer.Change( 0, Timeout.Infinite)) + { + throw TransactionException.CreateInvalidOperationException( + TraceSourceType.TraceSourceOleTx, + SR.UnexpectedTimerFailure, + null); + } + } + } + } + } + + internal void ReenlistThread(object? state) + { + int localLoopCount; + bool done; + OletxEnlistment? localEnlistment; + ResourceManagerShim? localResourceManagerShim; + bool success; + Timer? localTimer = null; + bool disposeLocalTimer = false; + + OletxResourceManager resourceManager = (OletxResourceManager)state!; + + try + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxResourceManager)}.{nameof(ReenlistThread)}"); + } + + lock (resourceManager) + { + localResourceManagerShim = resourceManager.ResourceManagerShim; + localTimer = resourceManager.ReenlistThreadTimer; + resourceManager.ReenlistThreadTimer = null; + resourceManager.reenlistThread = Thread.CurrentThread; + } + + // We only want to do work if we have a resourceManagerShim. + if (localResourceManagerShim != null) + { + lock (resourceManager.ReenlistList) + { + // Get the current count on the list. + localLoopCount = resourceManager.ReenlistList.Count; + } + + done = false; + while (!done && localLoopCount > 0 && localResourceManagerShim != null) + { + lock (resourceManager.ReenlistList) + { + localEnlistment = null; + localLoopCount--; + if (resourceManager.ReenlistList.Count == 0) + { + done = true; + } + else + { + localEnlistment = resourceManager.ReenlistList[0] as OletxEnlistment; + if (localEnlistment == null) + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + throw TransactionException.Create(SR.InternalError, null); + } + + resourceManager.ReenlistList.RemoveAt(0); + object syncRoot = localEnlistment; + lock (syncRoot) + { + if (OletxEnlistment.OletxEnlistmentState.Done == localEnlistment.State) + { + // We may be racing with a RecoveryComplete here. Just forget about this + // enlistment. + localEnlistment = null; + } + + else if (OletxEnlistment.OletxEnlistmentState.Prepared != localEnlistment.State) + { + // The app hasn't yet responded to Prepare, so we don't know + // if it is indoubt or not yet. So just re-add it to the end + // of the list. + resourceManager.ReenlistList.Add(localEnlistment); + localEnlistment = null; + } + } + } + } + + if (localEnlistment != null) + { + OletxTransactionOutcome localOutcome = OletxTransactionOutcome.NotKnownYet; + try + { + Debug.Assert(localResourceManagerShim != null, "ReenlistThread - localResourceManagerShim is null" ); + + // Make sure we have a prepare info. + if (localEnlistment.ProxyPrepareInfoByteArray == null) + { + Debug.Assert(false, string.Format(null, "this.prepareInfoByteArray == null in RecoveryInformation()")); + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + throw TransactionException.Create(SR.InternalError, null); + } + + localResourceManagerShim.Reenlist(localEnlistment.ProxyPrepareInfoByteArray, out localOutcome); + + if (localOutcome == OletxTransactionOutcome.NotKnownYet) + { + object syncRoot = localEnlistment; + lock (syncRoot) + { + if (OletxEnlistment.OletxEnlistmentState.Done == localEnlistment.State) + { + // We may be racing with a RecoveryComplete here. Just forget about this + // enlistment. + localEnlistment = null; + } + else + { + // Put the enlistment back on the end of the list for retry later. + lock (resourceManager.ReenlistList) + { + resourceManager.ReenlistList.Add(localEnlistment); + localEnlistment = null; + } + } + } + } + } + catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN) + { + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + + // Release the resource manager so we can create a new one. + resourceManager.ResourceManagerShim = null; + + // Now create a new resource manager with the proxy. + localResourceManagerShim = resourceManager.ResourceManagerShim; + } + + // If we get here and we still have localEnlistment, then we got the outcome. + if (localEnlistment != null) + { + object syncRoot = localEnlistment; + lock (syncRoot) + { + if (OletxEnlistment.OletxEnlistmentState.Done == localEnlistment.State) + { + // We may be racing with a RecoveryComplete here. Just forget about this + // enlistment. + localEnlistment = null; + } + else + { + // 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 (resourceManager.ReenlistList) + { + resourceManager.ReenlistPendingList.Add(localEnlistment); + } + + if (localOutcome == OletxTransactionOutcome.Committed) + { + localEnlistment.State = OletxEnlistment.OletxEnlistmentState.Committing; + + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, localEnlistment.EnlistmentTraceId, NotificationCall.Commit); + } + + localEnlistment.EnlistmentNotification!.Commit(localEnlistment); + } + else if (localOutcome == OletxTransactionOutcome.Aborted) + { + localEnlistment.State = OletxEnlistment.OletxEnlistmentState.Aborting; + + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, localEnlistment.EnlistmentTraceId, NotificationCall.Rollback); + } + + localEnlistment.EnlistmentNotification!.Rollback(localEnlistment); + } + else + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + throw TransactionException.Create(SR.InternalError, null); + } + } + } + } // end of if null != localEnlistment + } // end of if null != localEnlistment + } + } + + localResourceManagerShim = null; + + // Check to see if there is more work to do. + lock (resourceManager.ReenlistList) + { + lock (resourceManager) + { + // Get the current count on the list. + localLoopCount = resourceManager.ReenlistList.Count; + if (localLoopCount <= 0 && resourceManager.ReenlistPendingList.Count <= 0) + { + // No more entries on the list. Try calling ReenlistComplete on the proxy, if + // appropriate. + // If the application has called RecoveryComplete, + // we are responsible for calling ReenlistComplete on the + // proxy. + success = resourceManager.CallProxyReenlistComplete(); + if (success) + { + // Okay, the reenlist thread is done and we don't need to schedule another one. + disposeLocalTimer = true; + } + else + { + // We couldn't talk to the proxy to do ReenlistComplete, so schedule + // the thread again for 10 seconds from now. + resourceManager.ReenlistThreadTimer = localTimer; + if (!localTimer!.Change(10000, Timeout.Infinite)) + { + throw TransactionException.CreateInvalidOperationException( + TraceSourceType.TraceSourceLtm, + SR.UnexpectedTimerFailure, + null); + } + } + } + else + { + // There are still entries on the list, so they must not be + // resovled, yet. Schedule the thread again in 10 seconds. + resourceManager.ReenlistThreadTimer = localTimer; + if (!localTimer!.Change(10000, Timeout.Infinite)) + { + throw TransactionException.CreateInvalidOperationException( + TraceSourceType.TraceSourceLtm, + SR.UnexpectedTimerFailure, + null); + } + } + + resourceManager.reenlistThread = null; + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxResourceManager)}.{nameof(ReenlistThread)}"); + } + } + } // end of outer-most try + finally + { + localResourceManagerShim = null; + if (disposeLocalTimer && localTimer != null) + { + localTimer.Dispose(); + } + } + } // end of ReenlistThread method; +} + +// This is the base class for all enlistment objects. The enlistment objects provide the callback +// that is made from the application and pass it through to the proxy. +internal abstract class OletxBaseEnlistment +{ + protected Guid EnlistmentGuid; + protected OletxResourceManager OletxResourceManager; + protected OletxTransaction? oletxTransaction; + internal OletxTransaction? OletxTransaction => oletxTransaction; + + internal Guid DistributedTxId + { + get + { + Guid returnValue = Guid.Empty; + + if (OletxTransaction != null) + { + returnValue = OletxTransaction.DistributedTxId; + } + return returnValue; + } + } + + protected string TransactionGuidString; + protected int EnlistmentId; + // this needs to be internal so it can be set from the recovery information during Reenlist. + internal EnlistmentTraceIdentifier TraceIdentifier; + + // Owning public Enlistment object + protected InternalEnlistment? InternalEnlistment; + + public OletxBaseEnlistment(OletxResourceManager oletxResourceManager, OletxTransaction? oletxTransaction) + { + Guid resourceManagerId = Guid.Empty; + + EnlistmentGuid = Guid.NewGuid(); + OletxResourceManager = oletxResourceManager; + this.oletxTransaction = oletxTransaction; + if (oletxTransaction != null) + { + EnlistmentId = oletxTransaction.RealOletxTransaction._enlistmentCount++; + TransactionGuidString = oletxTransaction.RealOletxTransaction.TxGuid.ToString(); + } + else + { + TransactionGuidString = Guid.Empty.ToString(); + } + TraceIdentifier = EnlistmentTraceIdentifier.Empty; + } + + protected EnlistmentTraceIdentifier InternalTraceIdentifier + { + get + { + if (EnlistmentTraceIdentifier.Empty == TraceIdentifier ) + { + lock (this) + { + if (EnlistmentTraceIdentifier.Empty == TraceIdentifier ) + { + Guid rmId = Guid.Empty; + if (OletxResourceManager != null) + { + rmId = OletxResourceManager.ResourceManagerIdentifier; + } + EnlistmentTraceIdentifier temp; + if (oletxTransaction != null) + { + temp = new EnlistmentTraceIdentifier(rmId, oletxTransaction.TransactionTraceId, EnlistmentId); + } + else + { + TransactionTraceIdentifier txTraceId = new(TransactionGuidString, 0); + temp = new EnlistmentTraceIdentifier( rmId, txTraceId, EnlistmentId); + } + Thread.MemoryBarrier(); + TraceIdentifier = temp; + } + } + } + + return TraceIdentifier; + } + } + + protected void AddToEnlistmentTable() + { + lock (OletxResourceManager.EnlistmentHashtable.SyncRoot) + { + OletxResourceManager.EnlistmentHashtable.Add(EnlistmentGuid, this); + } + } + + protected void RemoveFromEnlistmentTable() + { + lock (OletxResourceManager.EnlistmentHashtable.SyncRoot) + { + OletxResourceManager.EnlistmentHashtable.Remove(EnlistmentGuid); + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransaction.cs new file mode 100644 index 0000000000000..6f18c18cc8143 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransaction.cs @@ -0,0 +1,1373 @@ +// 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 +{ + /// + /// 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. + /// + [Serializable] + internal class OletxTransaction : ISerializable, IObjectReference + { + // 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 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) + { + if (serializationInfo == null) + { + throw new ArgumentNullException(nameof(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!; + } + + 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; + } + + /// + /// Implementation of IDisposable.Dispose. Releases managed, and unmanaged resources + /// associated with the Transaction object. + /// + 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 + + /// + /// 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. + /// + /// + /// 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. + /// + 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) + { + if (serializationInfo == null) + { + throw new ArgumentNullException(nameof(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 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 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 DateTime _creationTime; + private 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.GetResourceString(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 isRoot) + { + 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) + { + if (Phase0EnlistVolatilementContainerList == null) + { + // 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(); + } + } + if (Phase1EnlistVolatilementContainer != null) + { + 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; + } + } + + // Let the InternalTransaciton know about the outcome. + if (InternalTransaction != null) + { + InternalTransaction.DistributedTransactionOutcome(InternalTransaction, Status); + } + + } + + 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 + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransactionManager.cs new file mode 100644 index 0000000000000..86329f235b424 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxTransactionManager.cs @@ -0,0 +1,804 @@ +// 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.Runtime.InteropServices; +using System.Threading; +using System.Transactions.DtcProxyShim; + +namespace System.Transactions.Oletx; + +internal sealed class OletxTransactionManager +{ + private IsolationLevel _isolationLevelProperty; + + private TimeSpan _timeoutProperty; + + private TransactionOptions _configuredTransactionOptions = default; + + // Object for synchronizing access to the entire class( avoiding lock( typeof( ... )) ) + private static object? _classSyncObject; + + // These have to be static because we can only add an RM with the proxy once, even if we + // have multiple OletxTransactionManager instances. + internal static Hashtable? _resourceManagerHashTable; + public static ReaderWriterLock ResourceManagerHashTableLock = null!; + + internal static volatile bool ProcessingTmDown; + + internal ReaderWriterLock DtcTransactionManagerLock; + private DtcTransactionManager _dtcTransactionManager; + internal OletxInternalResourceManager InternalResourceManager; + + internal static DtcProxyShimFactory ProxyShimFactory = null!; // Late initialization + + // Double-checked locking pattern requires volatile for read/write synchronization + internal static volatile EventWaitHandle? _shimWaitHandle; + internal static EventWaitHandle ShimWaitHandle + { + get + { + if (_shimWaitHandle == null) + { + lock (ClassSyncObject) + { + _shimWaitHandle ??= new EventWaitHandle(false, EventResetMode.AutoReset); + } + } + + return _shimWaitHandle; + } + } + + private string? _nodeNameField; + + internal static void ShimNotificationCallback(object? state, bool timeout) + { + // First we need to get the notification from the shim factory. + object? enlistment2 = null; + ShimNotificationType shimNotificationType = ShimNotificationType.None; + bool isSinglePhase; + bool abortingHint; + + byte[]? prepareInfoBuffer = null; + + bool holdingNotificationLock = false; + + DtcProxyShimFactory localProxyShimFactory; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(OletxTransactionManager)}.{nameof(ShimNotificationCallback)}"); + } + + // This lock doesn't really protect any of our data. It is here so that if an exception occurs + // while calling out to the app, we get an escalation to AppDomainUnload. + Thread.BeginCriticalRegion(); + try + { + do + { + // Take a local copy of the proxyShimFactory because if we get an RM TMDown notification, + // we will still hold the critical section in that factory, but processing of the TMDown will + // cause replacement of the OletxTransactionManager.proxyShimFactory. + localProxyShimFactory = ProxyShimFactory; + try + { + Thread.BeginThreadAffinity(); + try + { + localProxyShimFactory.GetNotification( + out enlistment2, + out shimNotificationType, + out isSinglePhase, + out abortingHint, + out holdingNotificationLock, + out prepareInfoBuffer); + } + finally + { + if (holdingNotificationLock) + { + if (enlistment2 is OletxInternalResourceManager) + { + // In this case we know that the TM has gone down and we need to exchange + // the native lock for a managed lock. + ProcessingTmDown = true; + Monitor.Enter(ProxyShimFactory); + } + else + { + holdingNotificationLock = false; + } + localProxyShimFactory.ReleaseNotificationLock(); + } + Thread.EndThreadAffinity(); + } + + // If a TM down is being processed it is possible that the native lock + // has been exchanged for a managed lock. In that case we need to attempt + // to take a lock to hold up processing more events until the TM down + // processing is complete. + if (ProcessingTmDown) + { + lock (ProxyShimFactory) + { + // We don't do any work under this lock just make sure that we + // can take it. + } + } + + if (shimNotificationType != ShimNotificationType.None) + { + // Next, based on the notification type, cast the Handle accordingly and make + // the appropriate call on the enlistment. + switch (shimNotificationType) + { + case ShimNotificationType.Phase0RequestNotify: + { + if (enlistment2 is OletxPhase0VolatileEnlistmentContainer ph0VolEnlistContainer) + { + ph0VolEnlistContainer.Phase0Request(abortingHint); + } + else + { + if (enlistment2 is OletxEnlistment oletxEnlistment) + { + oletxEnlistment.Phase0Request(abortingHint); + } + else + { + Environment.FailFast(SR.InternalError); + } + } + + break; + } + + case ShimNotificationType.VoteRequestNotify: + { + if (enlistment2 is OletxPhase1VolatileEnlistmentContainer ph1VolEnlistContainer) + { + ph1VolEnlistContainer.VoteRequest(); + } + else + { + Environment.FailFast(SR.InternalError); + } + + break; + } + + case ShimNotificationType.CommittedNotify: + { + if (enlistment2 is OutcomeEnlistment outcomeEnlistment) + { + outcomeEnlistment.Committed(); + } + else + { + if (enlistment2 is OletxPhase1VolatileEnlistmentContainer ph1VolEnlistContainer) + { + ph1VolEnlistContainer.Committed(); + } + else + { + Environment.FailFast(SR.InternalError); + } + } + + break; + } + + case ShimNotificationType.AbortedNotify: + { + if (enlistment2 is OutcomeEnlistment outcomeEnlistment) + { + outcomeEnlistment.Aborted(); + } + else + { + if (enlistment2 is OletxPhase1VolatileEnlistmentContainer ph1VolEnlistContainer) + { + ph1VolEnlistContainer.Aborted(); + } + // else + // Voters may receive notifications even + // in cases where they therwise respond + // negatively to the vote request. It is + // also not guaranteed that we will get a + // notification if we do respond negatively. + // The only safe thing to do is to free the + // Handle when we abort the transaction + // with a voter. These two things together + // mean that we cannot guarantee that this + // Handle will be alive when we get this + // notification. + } + + break; + } + + case ShimNotificationType.InDoubtNotify: + { + if (enlistment2 is OutcomeEnlistment outcomeEnlistment) + { + outcomeEnlistment.InDoubt(); + } + else + { + if (enlistment2 is OletxPhase1VolatileEnlistmentContainer ph1VolEnlistContainer) + { + ph1VolEnlistContainer.InDoubt(); + } + else + { + Environment.FailFast(SR.InternalError); + } + } + + break; + } + + case ShimNotificationType.PrepareRequestNotify: + { + bool enlistmentDone = true; + + if (enlistment2 is OletxEnlistment enlistment) + { + enlistmentDone = enlistment.PrepareRequest(isSinglePhase, prepareInfoBuffer!); + } + else + { + Environment.FailFast(SR.InternalError); + } + + break; + } + + case ShimNotificationType.CommitRequestNotify: + { + if (enlistment2 is OletxEnlistment enlistment) + { + enlistment.CommitRequest(); + } + else + { + Environment.FailFast(SR.InternalError); + } + + break; + } + + case ShimNotificationType.AbortRequestNotify: + { + if (enlistment2 is OletxEnlistment enlistment) + { + enlistment.AbortRequest(); + } + else + { + Environment.FailFast(SR.InternalError); + } + + break; + } + + case ShimNotificationType.EnlistmentTmDownNotify: + { + if (enlistment2 is OletxEnlistment enlistment) + { + enlistment.TMDown(); + } + else + { + Environment.FailFast(SR.InternalError); + } + + break; + } + + case ShimNotificationType.ResourceManagerTmDownNotify: + { + switch (enlistment2) + { + case OletxResourceManager resourceManager: + resourceManager.TMDown(); + break; + + case OletxInternalResourceManager internalResourceManager: + internalResourceManager.TMDown(); + break; + + default: + Environment.FailFast(SR.InternalError); + break; + } + + break; + } + + default: + { + Environment.FailFast(SR.InternalError); + break; + } + } + } + } + finally + { + if (holdingNotificationLock) + { + holdingNotificationLock = false; + ProcessingTmDown = false; + Monitor.Exit(ProxyShimFactory); + } + } + } + while (shimNotificationType != ShimNotificationType.None); + } + finally + { + if (holdingNotificationLock) + { + holdingNotificationLock = false; + ProcessingTmDown = false; + Monitor.Exit(ProxyShimFactory); + } + + Thread.EndCriticalRegion(); + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(OletxTransactionManager)}.{nameof(ShimNotificationCallback)}"); + } + } + + internal OletxTransactionManager(string nodeName) + { + lock (ClassSyncObject) + { + // If we have not already initialized the shim factory and started the notification + // thread, do so now. + if (ProxyShimFactory == null) + { + ProxyShimFactory = new DtcProxyShimFactory(ShimWaitHandle); + + ThreadPool.UnsafeRegisterWaitForSingleObject( + ShimWaitHandle, + ShimNotificationCallback, + null, + -1, + false); + } + } + + DtcTransactionManagerLock = new ReaderWriterLock(); + + _nodeNameField = nodeName; + + // The DTC proxy doesn't like an empty string for node name on 64-bit platforms when + // running as WOW64. It treats any non-null node name as a "remote" node and turns off + // the WOW64 bit, causing problems when reading the registry. So if we got on empty + // string for the node name, just treat it as null. + if (_nodeNameField is { Length: 0 }) + { + _nodeNameField = null; + } + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.OleTxTransactionManagerCreate(GetType(), _nodeNameField); + } + + // Initialize the properties from config. + _configuredTransactionOptions.IsolationLevel = _isolationLevelProperty = TransactionManager.DefaultIsolationLevel; + _configuredTransactionOptions.Timeout = _timeoutProperty = TransactionManager.DefaultTimeout; + + InternalResourceManager = new OletxInternalResourceManager( this ); + + DtcTransactionManagerLock.AcquireWriterLock(-1); + try + { + _dtcTransactionManager = new DtcTransactionManager(_nodeNameField, this); + } + finally + { + DtcTransactionManagerLock.ReleaseWriterLock(); + } + + if (_resourceManagerHashTable == null) + { + _resourceManagerHashTable = new Hashtable(2); + ResourceManagerHashTableLock = new ReaderWriterLock(); + } + } + + internal OletxCommittableTransaction CreateTransaction(TransactionOptions properties) + { + OletxCommittableTransaction tx; + RealOletxTransaction realTransaction; + TransactionShim? transactionShim = null; + Guid txIdentifier = Guid.Empty; + OutcomeEnlistment outcomeEnlistment; + + TransactionManager.ValidateIsolationLevel(properties.IsolationLevel); + + // Never create a transaction with an IsolationLevel of Unspecified. + if (IsolationLevel.Unspecified == properties.IsolationLevel) + { + properties.IsolationLevel = _configuredTransactionOptions.IsolationLevel; + } + + properties.Timeout = TransactionManager.ValidateTimeout(properties.Timeout); + + DtcTransactionManagerLock.AcquireReaderLock(-1); + try + { + OletxTransactionIsolationLevel oletxIsoLevel = ConvertIsolationLevel(properties.IsolationLevel); + uint oletxTimeout = DtcTransactionManager.AdjustTimeout(properties.Timeout); + + outcomeEnlistment = new OutcomeEnlistment(); + try + { + _dtcTransactionManager.ProxyShimFactory.BeginTransaction( + oletxTimeout, + oletxIsoLevel, + outcomeEnlistment, + out txIdentifier, + out transactionShim); + } + catch (COMException ex) + { + ProxyException(ex); + throw; + } + + realTransaction = new RealOletxTransaction( + this, + transactionShim, + outcomeEnlistment, + txIdentifier, + oletxIsoLevel, + true); + tx = new OletxCommittableTransaction(realTransaction); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.TransactionCreated(TraceSourceType.TraceSourceOleTx, tx.TransactionTraceId, "OletxTransaction"); + } + } + finally + { + DtcTransactionManagerLock.ReleaseReaderLock(); + } + + return tx; + } + + internal OletxEnlistment ReenlistTransaction( + Guid resourceManagerIdentifier, + byte[] recoveryInformation, + IEnlistmentNotificationInternal enlistmentNotification) + { + if (recoveryInformation == null) + { + throw new ArgumentNullException(nameof(recoveryInformation)); + } + + if (enlistmentNotification == null) + { + throw new ArgumentNullException(nameof(enlistmentNotification)); + } + + // Now go find the resource manager in the collection. + OletxResourceManager oletxResourceManager = RegisterResourceManager(resourceManagerIdentifier); + if (oletxResourceManager == null) + { + throw new ArgumentException(SR.InvalidArgument, nameof(resourceManagerIdentifier)); + } + + if (oletxResourceManager.RecoveryCompleteCalledByApplication) + { + throw new InvalidOperationException(SR.ReenlistAfterRecoveryComplete); + } + + // Now ask the resource manager to reenlist. + OletxEnlistment returnValue = oletxResourceManager.Reenlist(recoveryInformation, enlistmentNotification); + + return returnValue; + } + + internal void ResourceManagerRecoveryComplete(Guid resourceManagerIdentifier) + { + OletxResourceManager oletxRm = RegisterResourceManager(resourceManagerIdentifier); + + if (oletxRm.RecoveryCompleteCalledByApplication) + { + throw new InvalidOperationException(SR.DuplicateRecoveryComplete); + } + + oletxRm.RecoveryComplete(); + } + + internal OletxResourceManager RegisterResourceManager(Guid resourceManagerIdentifier) + { + OletxResourceManager? oletxResourceManager; + + ResourceManagerHashTableLock.AcquireWriterLock(-1); + + try + { + // If this resource manager has already been registered, don't register it again. + oletxResourceManager = _resourceManagerHashTable![resourceManagerIdentifier] as OletxResourceManager; + if (oletxResourceManager != null) + { + return oletxResourceManager; + } + + oletxResourceManager = new OletxResourceManager(this, resourceManagerIdentifier); + + _resourceManagerHashTable.Add(resourceManagerIdentifier, oletxResourceManager); + } + finally + { + ResourceManagerHashTableLock.ReleaseWriterLock(); + } + + return oletxResourceManager; + } + + internal string? CreationNodeName + => _nodeNameField; + + internal OletxResourceManager FindOrRegisterResourceManager(Guid resourceManagerIdentifier) + { + if (resourceManagerIdentifier == Guid.Empty) + { + throw new ArgumentException(SR.BadResourceManagerId, nameof(resourceManagerIdentifier)); + } + + OletxResourceManager? oletxResourceManager; + + ResourceManagerHashTableLock.AcquireReaderLock(-1); + try + { + oletxResourceManager = _resourceManagerHashTable![resourceManagerIdentifier] as OletxResourceManager; + } + finally + { + ResourceManagerHashTableLock.ReleaseReaderLock(); + } + + if (oletxResourceManager == null) + { + return RegisterResourceManager(resourceManagerIdentifier); + } + + return oletxResourceManager; + } + + internal DtcTransactionManager DtcTransactionManager + { + get + { + if (DtcTransactionManagerLock.IsReaderLockHeld ||DtcTransactionManagerLock.IsWriterLockHeld) + { + if (_dtcTransactionManager == null) + { + throw TransactionException.Create(SR.DtcTransactionManagerUnavailable, null); + } + + return _dtcTransactionManager; + } + + // Internal programming error. A reader or writer lock should be held when this property is invoked. + throw TransactionException.Create(SR.InternalError, null); + } + } + + internal string? NodeName + => _nodeNameField; + + internal static void ProxyException(COMException comException) + { + if (comException.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || + comException.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + throw TransactionManagerCommunicationException.Create( + SR.TransactionManagerCommunicationException, + comException); + } + if (comException.ErrorCode == OletxHelper.XACT_E_NETWORK_TX_DISABLED) + { + throw TransactionManagerCommunicationException.Create( + SR.NetworkTransactionsDisabled, + comException); + } + // Else if the error is a transaction oriented error, throw a TransactionException + if (comException.ErrorCode >= OletxHelper.XACT_E_FIRST && + comException.ErrorCode <= OletxHelper.XACT_E_LAST) + { + // Special casing XACT_E_NOTRANSACTION + throw TransactionException.Create( + OletxHelper.XACT_E_NOTRANSACTION == comException.ErrorCode + ? SR.TransactionAlreadyOver + : comException.Message, + comException); + } + } + + internal void ReinitializeProxy() + { + // This is created by the static constructor. + DtcTransactionManagerLock.AcquireWriterLock(-1); + + try + { + _dtcTransactionManager?.ReleaseProxy(); + } + finally + { + DtcTransactionManagerLock.ReleaseWriterLock(); + } + } + + internal static OletxTransactionIsolationLevel ConvertIsolationLevel(IsolationLevel isolationLevel) + => isolationLevel switch + { + IsolationLevel.Serializable => OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE, + IsolationLevel.RepeatableRead => OletxTransactionIsolationLevel.ISOLATIONLEVEL_REPEATABLEREAD, + IsolationLevel.ReadCommitted => OletxTransactionIsolationLevel.ISOLATIONLEVEL_READCOMMITTED, + IsolationLevel.ReadUncommitted => OletxTransactionIsolationLevel.ISOLATIONLEVEL_READUNCOMMITTED, + IsolationLevel.Chaos => OletxTransactionIsolationLevel.ISOLATIONLEVEL_CHAOS, + IsolationLevel.Unspecified => OletxTransactionIsolationLevel.ISOLATIONLEVEL_UNSPECIFIED, + _ => OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE + }; + + internal static IsolationLevel ConvertIsolationLevelFromProxyValue(OletxTransactionIsolationLevel proxyIsolationLevel) + => proxyIsolationLevel switch + { + OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE => IsolationLevel.Serializable, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_REPEATABLEREAD => IsolationLevel.RepeatableRead, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_READCOMMITTED => IsolationLevel.ReadCommitted, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_READUNCOMMITTED => IsolationLevel.ReadUncommitted, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_UNSPECIFIED => IsolationLevel.Unspecified, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_CHAOS => IsolationLevel.Chaos, + _ => IsolationLevel.Serializable + }; + + // Helper object for static synchronization + internal static object ClassSyncObject + { + get + { + if (_classSyncObject == null) + { + object o = new(); + Interlocked.CompareExchange(ref _classSyncObject, o, null); + } + + return _classSyncObject; + } + } +} + +internal sealed class OletxInternalResourceManager +{ + private OletxTransactionManager _oletxTm; + + internal Guid Identifier { get; } + + internal ResourceManagerShim? ResourceManagerShim; + + internal OletxInternalResourceManager(OletxTransactionManager oletxTm) + { + _oletxTm = oletxTm; + Identifier = Guid.NewGuid(); + } + + public void TMDown() + { + // Let's set ourselves up for reinitialization with the proxy by releasing our + // reference to the resource manager shim, which will release its reference + // to the proxy when it destructs. + ResourceManagerShim = null; + + // We need to look through all the transactions and tell them about + // the TMDown so they can tell their Phase0VolatileEnlistmentContainers. + Transaction? tx; + RealOletxTransaction realTx; + IDictionaryEnumerator tableEnum; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxInternalResourceManager)}.{nameof(TMDown)}"); + } + + // make a local copy of the hash table to avoid possible deadlocks when we lock both the global hash table + // and the transaction object. + Hashtable txHashTable; + lock (TransactionManager.PromotedTransactionTable.SyncRoot) + { + txHashTable = (Hashtable)TransactionManager.PromotedTransactionTable.Clone(); + } + + // No need to lock my hashtable, nobody is going to change it. + tableEnum = txHashTable.GetEnumerator(); + while (tableEnum.MoveNext()) + { + WeakReference? txWeakRef = (WeakReference?)tableEnum.Value; + if (txWeakRef != null) + { + tx = (Transaction?)txWeakRef.Target; + if (tx != null) + { + realTx = tx._internalTransaction.PromotedTransaction!.RealOletxTransaction; + // Only deal with transactions owned by my OletxTm. + if (realTx.OletxTransactionManagerInstance == _oletxTm) + { + realTx.TMDown(); + } + } + } + } + + // Now make a local copy of the hash table of resource managers and tell each of them. This is to + // deal with Durable EDPR=true (phase0) enlistments. Each RM will also get a TMDown, but it will + // come AFTER the "buggy" Phase0Request with abortHint=true - COMPlus bug 36760/36758. + Hashtable? rmHashTable = null; + if (OletxTransactionManager._resourceManagerHashTable != null) + { + OletxTransactionManager.ResourceManagerHashTableLock.AcquireReaderLock(Timeout.Infinite); + try + { + rmHashTable = (Hashtable)OletxTransactionManager._resourceManagerHashTable.Clone(); + } + finally + { + OletxTransactionManager.ResourceManagerHashTableLock.ReleaseReaderLock(); + } + } + + if (rmHashTable != null) + { + // No need to lock my hashtable, nobody is going to change it. + tableEnum = rmHashTable.GetEnumerator(); + while (tableEnum.MoveNext()) + { + OletxResourceManager? oletxRM = (OletxResourceManager?)tableEnum.Value; + if (oletxRM != null) + { + // When the RM spins through its enlistments, it will need to make sure that + // the enlistment is for this particular TM. + oletxRM.TMDownFromInternalRM(_oletxTm); + } + } + } + + // Now let's reinitialize the shim. + _oletxTm.DtcTransactionManagerLock.AcquireWriterLock(-1); + try + { + _oletxTm.ReinitializeProxy(); + } + finally + { + _oletxTm.DtcTransactionManagerLock.ReleaseWriterLock(); + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxInternalResourceManager)}.{nameof(TMDown)}"); + } + } + + internal void CallReenlistComplete() + => ResourceManagerShim!.ReenlistComplete(); +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxVolatileEnlistment.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxVolatileEnlistment.cs new file mode 100644 index 0000000000000..82240b5f5ddf3 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Oletx/OletxVolatileEnlistment.cs @@ -0,0 +1,1508 @@ +// 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.Globalization; +using System.Runtime.InteropServices; +using System.Threading; +using System.Transactions.DtcProxyShim; + +namespace System.Transactions.Oletx; + +internal abstract class OletxVolatileEnlistmentContainer +{ + protected OletxVolatileEnlistmentContainer(RealOletxTransaction realOletxTransaction) + { + Debug.Assert(realOletxTransaction != null, "Argument is null"); + + RealOletxTransaction = realOletxTransaction; + } + + protected RealOletxTransaction RealOletxTransaction; + protected ArrayList EnlistmentList = new(); + protected int Phase; + protected int OutstandingNotifications; + protected bool CollectedVoteYes; + protected int IncompleteDependentClones; + protected bool AlreadyVoted; + + internal abstract void DecrementOutstandingNotifications(bool voteYes); + + internal abstract void AddDependentClone(); + + internal abstract void DependentCloneCompleted(); + + internal abstract void RollbackFromTransaction(); + + internal abstract void OutcomeFromTransaction(TransactionStatus outcome); + + internal abstract void Committed(); + + internal abstract void Aborted(); + + internal abstract void InDoubt(); + + internal Guid TransactionIdentifier + => RealOletxTransaction.Identifier; +} + +internal sealed class OletxPhase0VolatileEnlistmentContainer : OletxVolatileEnlistmentContainer +{ + private Phase0EnlistmentShim? _phase0EnlistmentShim; + private bool _aborting; + private bool _tmWentDown; + + internal OletxPhase0VolatileEnlistmentContainer(RealOletxTransaction realOletxTransaction) + : base(realOletxTransaction) + { + // This will be set later, after the caller creates the enlistment with the proxy. + _phase0EnlistmentShim = null; + + Phase = -1; + _aborting = false; + _tmWentDown = false; + OutstandingNotifications = 0; + IncompleteDependentClones = 0; + AlreadyVoted = false; + // If anybody votes false, this will get set to false. + CollectedVoteYes = true; + + // This is a new undecided enlistment on the transaction. Do this last since it has side affects. + realOletxTransaction.IncrementUndecidedEnlistments(); + } + + internal void TMDown() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxPhase0VolatileEnlistmentContainer)}.{nameof(TMDown)}"); + } + + _tmWentDown = true; + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxPhase0VolatileEnlistmentContainer)}.{nameof(TMDown)}"); + } + } + + // Be sure to lock this object before calling this. + internal bool NewEnlistmentsAllowed + => Phase == -1; + + internal void AddEnlistment(OletxVolatileEnlistment enlistment) + { + Debug.Assert(enlistment != null, "Argument is null"); + + lock (this) + { + if (Phase != -1) + { + throw TransactionException.Create(SR.TooLate, null); + } + + EnlistmentList.Add(enlistment); + } + } + + internal override void AddDependentClone() + { + lock (this) + { + if (Phase != -1) + { + throw TransactionException.CreateTransactionStateException(null); + } + + IncompleteDependentClones++; + } + } + + internal override void DependentCloneCompleted() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + + bool doDecrement = false; + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.DependentCloneCompleted, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture) + + ", phase = " + Phase.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + IncompleteDependentClones--; + Debug.Assert(IncompleteDependentClones >= 0, "OletxPhase0VolatileEnlistmentContainer.DependentCloneCompleted - incompleteDependentClones < 0"); + + // If we have not more incomplete dependent clones and we are in Phase 0, we need to "fake out" a notification completion. + if (IncompleteDependentClones == 0 && Phase == 0) + { + OutstandingNotifications++; + doDecrement = true; + } + } + if (doDecrement) + { + DecrementOutstandingNotifications(true); + } + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.DependentCloneCompleted"; + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } + + internal override void RollbackFromTransaction() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.RollbackFromTransaction, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + if (Phase == 0 && (OutstandingNotifications > 0 || IncompleteDependentClones > 0)) + { + AlreadyVoted = true; + // All we are going to do is release the Phase0Enlistment interface because there + // is no negative vote to Phase0Request. + if (Phase0EnlistmentShim != null) + { + Phase0EnlistmentShim.Phase0Done(false); + } + } + } + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.RollbackFromTransaction"; + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } + + + internal Phase0EnlistmentShim? Phase0EnlistmentShim + { + get + { + lock (this) + { + return _phase0EnlistmentShim; + } + } + 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 (_aborting || _tmWentDown) + { + value!.Phase0Done(false); + } + _phase0EnlistmentShim = value; + } + } + } + + internal override void DecrementOutstandingNotifications(bool voteYes) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + bool respondToProxy = false; + Phase0EnlistmentShim? localPhase0Shim = null; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.DecrementOutstandingNotifications, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + OutstandingNotifications--; + Debug.Assert(OutstandingNotifications >= 0, "OletxPhase0VolatileEnlistmentContainer.DecrementOutstandingNotifications - outstandingNotifications < 0"); + + CollectedVoteYes = CollectedVoteYes && voteYes; + if (OutstandingNotifications == 0 && IncompleteDependentClones == 0) + { + if (Phase == 0 && !AlreadyVoted) + { + respondToProxy = true; + AlreadyVoted = true; + localPhase0Shim = _phase0EnlistmentShim; + } + RealOletxTransaction.DecrementUndecidedEnlistments(); + } + } + + try + { + if (respondToProxy) + { + if (localPhase0Shim != null) + { + localPhase0Shim.Phase0Done(CollectedVoteYes && !RealOletxTransaction.Doomed); + } + } + } + catch (COMException ex) + { + 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 (OletxHelper.XACT_E_PROTOCOL == ex.ErrorCode) + { + _phase0EnlistmentShim = null; + + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + else + { + throw; + } + } + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.DecrementOutstandingNotifications"; + + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } + + internal override void OutcomeFromTransaction(TransactionStatus outcome) + { + switch (outcome) + { + case TransactionStatus.Committed: + Committed(); + break; + case TransactionStatus.Aborted: + Aborted(); + break; + case TransactionStatus.InDoubt: + InDoubt(); + break; + default: + Debug.Assert(false, "OletxPhase0VolatileEnlistmentContainer.OutcomeFromTransaction, outcome is not Commited or Aborted or InDoubt"); + break; + } + } + + internal override void Committed() + { + OletxVolatileEnlistment? enlistment; + int localCount; + + lock (this) + { + Debug.Assert(Phase == 0 && OutstandingNotifications == 0); + Phase = 2; + localCount = EnlistmentList.Count; + } + + for (int i = 0; i < localCount; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Assert(false, "OletxPhase1VolatileEnlistmentContainer.Committed, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException(SR.InternalError); + } + + enlistment.Commit(); + } + } + + internal override void Aborted() + { + OletxVolatileEnlistment? enlistment; + int localCount; + + lock (this) + { + // Tell all the enlistments that the transaction aborted and let the enlistment + // state determine if the notification should be delivered. + Phase = 2; + localCount = EnlistmentList.Count; + } + + for (int i = 0; i < localCount; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Assert(false, "OletxPhase1VolatileEnlistmentContainer.Aborted, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException(SR.InternalError); + } + + enlistment.Rollback(); + } + } + + internal override void InDoubt() + { + OletxVolatileEnlistment? enlistment; + int localCount; + + lock (this) + { + // Tell all the enlistments that the transaction is InDoubt and let the enlistment + // state determine if the notification should be delivered. + Phase = 2; + localCount = EnlistmentList.Count; + } + + for (int i = 0; i < localCount; i++ ) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.InDoubt, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException( SR.InternalError); + } + + enlistment.InDoubt(); + } + } + + internal void Phase0Request(bool abortHint) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + OletxVolatileEnlistment? enlistment; + int localCount; + OletxCommittableTransaction? committableTx; + bool commitNotYetCalled = false; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.Phase0Request, abortHint = " + + abortHint.ToString(CultureInfo.CurrentCulture) + + ", phase = " + Phase.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + _aborting = abortHint; + committableTx = 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; + _aborting = true; + } + } + // It's possible that we are in phase 2 if we got an Aborted outcome from the transaction before we got the + // Phase0Request. In both cases, we just respond to the proxy and don't bother telling the enlistments. + // They have either already heard about the abort or will soon. + if (Phase == 2 || Phase == -1) + { + if (Phase == -1) + { + Phase = 0; + } + + // 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 || _tmWentDown || commitNotYetCalled || Phase == 2) + { + // 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 (_phase0EnlistmentShim != null) + { + try + { + _phase0EnlistmentShim.Phase0Done(false); + // We need to set the alreadyVoted flag to true once we successfully voted, so later we don't vote again when OletxDependentTransaction::Complete is called + // Otherwise, in OletxPhase0VolatileEnlistmentContainer::DecrementOutstandingNotifications code path, we are going to call Phase0Done( true ) again + // and result in an access violation while accessing the pPhase0EnlistmentAsync member variable of the Phase0Shim object. + AlreadyVoted = true; + } + // 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); + } + } + } + return; + } + OutstandingNotifications = EnlistmentList.Count; + localCount = EnlistmentList.Count; + // If we don't have any enlistments, then we must have created this container for + // delay commit dependent clones only. So we need to fake a notification. + if (localCount == 0) + { + OutstandingNotifications = 1; + } + } + else // any other phase is bad news. + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError("OletxPhase0VolatileEnlistmentContainer.Phase0Request, phase != -1"); + } + + Debug.Fail("OletxPhase0VolatileEnlistmentContainer.Phase0Request, phase != -1"); + throw new InvalidOperationException( SR.InternalError); + } + } + + // We may not have any Phase0 volatile enlistments, which means that this container + // got created solely for delay commit dependent transactions. We need to fake out a + // notification completion. + if (localCount == 0) + { + DecrementOutstandingNotifications(true); + } + else + { + for (int i = 0; i < localCount; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase0VolatileEnlistmentContainer.Phase0Request, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException( SR.InternalError); + } + + // Do the notification outside any locks. + Debug.Assert(enlistment.EnlistDuringPrepareRequired, "OletxPhase0VolatileEnlistmentContainer.Phase0Request, enlistmentList element not marked as EnlistmentDuringPrepareRequired."); + Debug.Assert(!abortHint, "OletxPhase0VolatileEnlistmentContainer.Phase0Request, abortingHint is true just before sending Prepares."); + + enlistment.Prepare(this); + } + } + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase0VolatileEnlistmentContainer.Phase0Request, abortHint = " + abortHint.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } +} + +internal sealed class OletxPhase1VolatileEnlistmentContainer : OletxVolatileEnlistmentContainer +{ + private VoterBallotShim? _voterBallotShim; + + internal OletxPhase1VolatileEnlistmentContainer(RealOletxTransaction realOletxTransaction) + : base(realOletxTransaction) + { + // This will be set later, after the caller creates the enlistment with the proxy. + _voterBallotShim = null; + + Phase = -1; + OutstandingNotifications = 0; + IncompleteDependentClones = 0; + AlreadyVoted = false; + + // If anybody votes false, this will get set to false. + CollectedVoteYes = true; + + // This is a new undecided enlistment on the transaction. Do this last since it has side affects. + realOletxTransaction.IncrementUndecidedEnlistments(); + } + + // Returns true if this container is enlisted for Phase 0. + internal void AddEnlistment(OletxVolatileEnlistment enlistment) + { + Debug.Assert(enlistment != null, "Argument is null"); + + lock (this) + { + if (Phase != -1) + { + throw TransactionException.Create(SR.TooLate, null); + } + + EnlistmentList.Add( enlistment ); + } + } + + internal override void AddDependentClone() + { + lock (this) + { + if (Phase != -1) + { + throw TransactionException.CreateTransactionStateException(null, Guid.Empty); + } + + // We simply need to block the response to the proxy until all clone is completed. + IncompleteDependentClones++; + } + } + + internal override void DependentCloneCompleted() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.DependentCloneCompleted, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture) + + ", phase = " + Phase.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + // This is to synchronize with the corresponding AddDependentClone which takes the container lock while incrementing the incompleteDependentClone count + lock (this) + { + IncompleteDependentClones--; + } + + Debug.Assert(OutstandingNotifications >= 0, "OletxPhase1VolatileEnlistmentContainer.DependentCloneCompleted - DependentCloneCompleted < 0"); + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.DependentCloneCompleted"; + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } + + internal override void RollbackFromTransaction() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + bool voteNo = false; + VoterBallotShim? localVoterShim = null; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.RollbackFromTransaction, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + if (Phase == 1 && OutstandingNotifications > 0) + { + AlreadyVoted = true; + voteNo = true; + localVoterShim = _voterBallotShim; + } + } + + if (voteNo) + { + try + { + localVoterShim?.Vote(false); + + // We are not going to hear anymore from the proxy if we voted no, so we need to tell the + // enlistments to rollback. The state of the OletxVolatileEnlistment will determine whether or + // not the notification actually goes out to the app. + Aborted(); + } + catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + lock (this) + { + // If we are in phase 1, we need to tell the enlistments that the transaction is InDoubt. + if (Phase == 1) + { + InDoubt(); + } + } + + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, "OletxPhase1VolatileEnlistmentContainer.RollbackFromTransaction"); + } + } + + internal VoterBallotShim? VoterBallotShim + { + get + { + lock (this) + { + return _voterBallotShim; + } + } + set + { + lock (this) + { + _voterBallotShim = value; + } + } + } + + internal override void DecrementOutstandingNotifications(bool voteYes) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + bool respondToProxy = false; + VoterBallotShim? localVoterShim = null; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.DecrementOutstandingNotifications, outstandingNotifications = " + + OutstandingNotifications.ToString(CultureInfo.CurrentCulture) + + ", incompleteDependentClones = " + + IncompleteDependentClones.ToString(CultureInfo.CurrentCulture); + + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + OutstandingNotifications--; + Debug.Assert(OutstandingNotifications >= 0, "OletxPhase1VolatileEnlistmentContainer.DecrementOutstandingNotifications - outstandingNotifications < 0"); + CollectedVoteYes = CollectedVoteYes && voteYes; + if (OutstandingNotifications == 0) + { + if (Phase == 1 && !AlreadyVoted) + { + respondToProxy = true; + AlreadyVoted = true; + localVoterShim = VoterBallotShim; + } + RealOletxTransaction.DecrementUndecidedEnlistments(); + } + } + + try + { + if (respondToProxy) + { + if (CollectedVoteYes && !RealOletxTransaction.Doomed) + { + localVoterShim?.Vote(true); + } + else // we need to vote no. + { + localVoterShim?.Vote(false); + + // We are not going to hear anymore from the proxy if we voted no, so we need to tell the + // enlistments to rollback. The state of the OletxVolatileEnlistment will determine whether or + // not the notification actually goes out to the app. + Aborted(); + } + } + } + catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_CONNECTION_DOWN || ex.ErrorCode == OletxHelper.XACT_E_TMNOTAVAILABLE) + { + lock (this) + { + // If we are in phase 1, we need to tell the enlistments that the transaction is InDoubt. + if (Phase == 1) + { + InDoubt(); + } + + // There is nothing special to do for phase 2. + } + + if (etwLog.IsEnabled()) + { + etwLog.ExceptionConsumed(TraceSourceType.TraceSourceOleTx, ex); + } + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, "OletxPhase1VolatileEnlistmentContainer.DecrementOutstandingNotifications"); + } + } + + internal override void OutcomeFromTransaction(TransactionStatus outcome) + { + bool driveAbort = false; + bool driveInDoubt = false; + + lock (this) + { + // If we are in Phase 1 and still have outstanding notifications, we need + // to drive sending of the outcome to the enlistments. If we are in any + // other phase, or we don't have outstanding notifications, we will eventually + // get the outcome notification on our OWN voter enlistment, so we will just + // wait for that. + if (Phase == 1 && OutstandingNotifications > 0) + { + switch (outcome) + { + case TransactionStatus.Aborted: + driveAbort = true; + break; + case TransactionStatus.InDoubt: + driveInDoubt = true; + break; + default: + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.OutcomeFromTransaction, outcome is not Aborted or InDoubt"); + break; + } + } + } + + if (driveAbort) + { + Aborted(); + } + + if (driveInDoubt) + { + InDoubt(); + } + } + + internal override void Committed() + { + OletxVolatileEnlistment? enlistment; + int localPhase1Count; + + lock (this) + { + Phase = 2; + localPhase1Count = EnlistmentList.Count; + } + + for ( int i = 0; i < localPhase1Count; i++ ) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.Committed, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException(SR.InternalError); + } + + enlistment.Commit(); + } + } + + internal override void Aborted() + { + OletxVolatileEnlistment? enlistment; + int localPhase1Count; + + lock (this) + { + Phase = 2; + localPhase1Count = EnlistmentList.Count; + } + + for (int i = 0; i < localPhase1Count; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.Aborted, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException( SR.InternalError); + } + + enlistment.Rollback(); + } + } + + internal override void InDoubt() + { + OletxVolatileEnlistment? enlistment; + int localPhase1Count; + + lock (this) + { + Phase = 2; + localPhase1Count = EnlistmentList.Count; + } + + for (int i = 0; i < localPhase1Count; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.InDoubt, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException( SR.InternalError); + } + + enlistment.InDoubt(); + } + } + + internal void VoteRequest() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + OletxVolatileEnlistment? enlistment; + int localPhase1Count = 0; + bool voteNo = false; + + lock (this) + { + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.VoteRequest"; + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, description); + } + + Phase = 1; + + // If we still have incomplete dependent clones, vote no now. + if (IncompleteDependentClones > 0) + { + voteNo = true; + OutstandingNotifications = 1; + } + else + { + OutstandingNotifications = EnlistmentList.Count; + localPhase1Count = EnlistmentList.Count; + // We may not have an volatile phase 1 enlistments, which means that this + // container was created only for non-delay commit dependent clones. If that + // is the case, fake out a notification and response. + if (localPhase1Count == 0) + { + OutstandingNotifications = 1; + } + } + + RealOletxTransaction.TooLateForEnlistments = true; + } + + if (voteNo) + { + DecrementOutstandingNotifications( false ); + } + else if (localPhase1Count == 0) + { + DecrementOutstandingNotifications( true ); + } + else + { + for (int i = 0; i < localPhase1Count; i++) + { + enlistment = EnlistmentList[i] as OletxVolatileEnlistment; + if (enlistment == null) + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxPhase1VolatileEnlistmentContainer.VoteRequest, enlistmentList element is not an OletxVolatileEnlistment."); + throw new InvalidOperationException( SR.InternalError); + } + + enlistment.Prepare(this); + } + } + + if (etwLog.IsEnabled()) + { + string description = "OletxPhase1VolatileEnlistmentContainer.VoteRequest"; + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, description); + } + } +} + +internal sealed class OletxVolatileEnlistment : OletxBaseEnlistment, IPromotedEnlistment +{ + private enum OletxVolatileEnlistmentState + { + Active, + Preparing, + Committing, + Aborting, + Prepared, + Aborted, + InDoubt, + Done + } + + private IEnlistmentNotificationInternal _iEnlistmentNotification; + private OletxVolatileEnlistmentState _state = OletxVolatileEnlistmentState.Active; + private OletxVolatileEnlistmentContainer? _container; + internal bool EnlistDuringPrepareRequired; + + // This is used if the transaction outcome is received while a prepare request + // is still outstanding to an app. Active means no outcome, yet. Aborted means + // we should tell the app Aborted. InDoubt means tell the app InDoubt. This + // should never be Committed because we shouldn't receive a Committed notification + // from the proxy while we have a Prepare outstanding. + private TransactionStatus _pendingOutcome; + + internal OletxVolatileEnlistment( + IEnlistmentNotificationInternal enlistmentNotification, + EnlistmentOptions enlistmentOptions, + OletxTransaction oletxTransaction) + : base(null!, oletxTransaction) + { + _iEnlistmentNotification = enlistmentNotification; + EnlistDuringPrepareRequired = (enlistmentOptions & EnlistmentOptions.EnlistDuringPrepareRequired) != 0; + + // We get a container when we are asked to vote. + _container = null; + + _pendingOutcome = TransactionStatus.Active; + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentCreated(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, EnlistmentType.Volatile, enlistmentOptions); + } + } + + internal void Prepare(OletxVolatileEnlistmentContainer container) + { + OletxVolatileEnlistmentState localState = OletxVolatileEnlistmentState.Active; + IEnlistmentNotificationInternal localEnlistmentNotification; + + lock (this) + { + localEnlistmentNotification = _iEnlistmentNotification; + + // The app may have already called EnlistmentDone. If this occurs, don't bother sending + // the notification to the app. + if (OletxVolatileEnlistmentState.Active == _state) + { + localState = _state = OletxVolatileEnlistmentState.Preparing; + } + else + { + localState = _state; + } + _container = container; + } + + // Tell the application to do the work. + if (localState == OletxVolatileEnlistmentState.Preparing) + { + if (localEnlistmentNotification != null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Prepare); + } + + localEnlistmentNotification.Prepare(this); + } + else + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Prepare, no enlistmentNotification member."); + throw new InvalidOperationException(SR.InternalError); + } + } + else if (localState == OletxVolatileEnlistmentState.Done) + { + // Voting yes because it was an early read-only vote. + container.DecrementOutstandingNotifications( true ); + + // We must have had a race between EnlistmentDone and the proxy telling + // us Phase0Request. Just return. + return; + } + // It is okay to be in Prepared state if we are edpr=true because we already + // did our prepare in Phase0. + else if (localState == OletxVolatileEnlistmentState.Prepared && EnlistDuringPrepareRequired) + { + container.DecrementOutstandingNotifications(true); + return; + } + else if (localState is OletxVolatileEnlistmentState.Aborting or OletxVolatileEnlistmentState.Aborted) + { + // An abort has raced with this volatile Prepare + // decrement the outstanding notifications making sure to vote no. + container.DecrementOutstandingNotifications(false); + return; + } + else + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Prepare, invalid state."); + throw new InvalidOperationException( SR.InternalError); + } + } + + internal void Commit() + { + OletxVolatileEnlistmentState localState = OletxVolatileEnlistmentState.Active; + IEnlistmentNotificationInternal? localEnlistmentNotification = null; + + lock (this) + { + // 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 (_state == OletxVolatileEnlistmentState.Prepared) + { + localState = _state = OletxVolatileEnlistmentState.Committing; + localEnlistmentNotification = _iEnlistmentNotification; + } + else + { + localState = _state; + } + } + + // Tell the application to do the work. + if (OletxVolatileEnlistmentState.Committing == localState) + { + if (localEnlistmentNotification != null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Commit); + } + + localEnlistmentNotification.Commit(this); + } + else + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Commit, no enlistmentNotification member."); + throw new InvalidOperationException(SR.InternalError); + } + } + else if (localState == OletxVolatileEnlistmentState.Done) + { + // Early Exit - state was Done + } + else + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Commit, invalid state."); + throw new InvalidOperationException(SR.InternalError); + } + } + + internal void Rollback() + { + OletxVolatileEnlistmentState localState = OletxVolatileEnlistmentState.Active; + IEnlistmentNotificationInternal? localEnlistmentNotification = null; + + lock (this) + { + // 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 ( _state is OletxVolatileEnlistmentState.Prepared or OletxVolatileEnlistmentState.Active) + { + localState = _state = OletxVolatileEnlistmentState.Aborting; + localEnlistmentNotification = _iEnlistmentNotification; + } + else + { + if (_state == OletxVolatileEnlistmentState.Preparing) + { + _pendingOutcome = TransactionStatus.Aborted; + } + + localState = _state; + } + } + + switch (localState) + { + // Tell the application to do the work. + case OletxVolatileEnlistmentState.Aborting: + { + if (localEnlistmentNotification != null) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.Rollback); + } + + localEnlistmentNotification.Rollback(this); + } + + // There is a small race where Rollback could be called when the enlistment is already + // aborting the transaciton, so just ignore that call. When the app enlistment + // finishes responding to its Rollback notification with EnlistmentDone, things will get + // cleaned up. + break; + } + case OletxVolatileEnlistmentState.Preparing: + // We need to tolerate this state, but we have already marked the + // enlistment as pendingRollback, so there is nothing else to do here. + break; + case OletxVolatileEnlistmentState.Done: + // Early Exit - state was Done + break; + default: + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Rollback, invalid state."); + throw new InvalidOperationException(SR.InternalError); + } + } + } + + internal void InDoubt() + { + OletxVolatileEnlistmentState localState = OletxVolatileEnlistmentState.Active; + IEnlistmentNotificationInternal? localEnlistmentNotification = null; + + lock (this) + { + // 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 (_state == OletxVolatileEnlistmentState.Prepared) + { + localState = _state = OletxVolatileEnlistmentState.InDoubt; + localEnlistmentNotification = _iEnlistmentNotification; + } + else + { + if (_state == OletxVolatileEnlistmentState.Preparing) + { + _pendingOutcome = TransactionStatus.InDoubt; + } + localState = _state; + } + } + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + + switch (localState) + { + // Tell the application to do the work. + case OletxVolatileEnlistmentState.InDoubt when localEnlistmentNotification != null: + { + if (etwLog.IsEnabled()) + { + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceOleTx, InternalTraceIdentifier, NotificationCall.InDoubt); + } + + localEnlistmentNotification.InDoubt(this); + break; + } + case OletxVolatileEnlistmentState.InDoubt: + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.InDoubt, no enlistmentNotification member."); + throw new InvalidOperationException(SR.InternalError); + } + case OletxVolatileEnlistmentState.Preparing: + // We have already set pendingOutcome, so there is nothing else to do. + break; + case OletxVolatileEnlistmentState.Done: + // Early Exit - state was Done + break; + default: + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.InDoubt, invalid state."); + throw new InvalidOperationException( SR.InternalError); + } + } + } + + void IPromotedEnlistment.EnlistmentDone() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(IPromotedEnlistment.EnlistmentDone)}"); + etwLog.EnlistmentCallbackPositive(InternalTraceIdentifier, EnlistmentCallback.Done); + } + + OletxVolatileEnlistmentState localState = OletxVolatileEnlistmentState.Active; + OletxVolatileEnlistmentContainer? localContainer; + + lock (this) + { + localState = _state; + localContainer = _container; + + if (_state != OletxVolatileEnlistmentState.Active && + _state != OletxVolatileEnlistmentState.Preparing && + _state != OletxVolatileEnlistmentState.Aborting && + _state != OletxVolatileEnlistmentState.Committing && + _state != OletxVolatileEnlistmentState.InDoubt) + { + throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId); + } + + _state = OletxVolatileEnlistmentState.Done; + } + + // For the Preparing state, we need to decrement the outstanding + // count with the container. If the state is Active, it is an early vote so we + // just stay in the Done state and when we get the Prepare, we will vote appropriately. + if (localState == OletxVolatileEnlistmentState.Preparing) + { + if (localContainer != null) + { + // Specify true. If aborting, it is okay because the transaction is already + // aborting. + localContainer.DecrementOutstandingNotifications(true); + } + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"{nameof(OletxEnlistment)}.{nameof(IPromotedEnlistment.EnlistmentDone)}"); + } + } + + void IPromotedEnlistment.Prepared() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(IPromotedEnlistment.Prepared)}"); + etwLog.EnlistmentCallbackPositive(InternalTraceIdentifier, EnlistmentCallback.Prepared); + } + + OletxVolatileEnlistmentContainer localContainer; + TransactionStatus localPendingOutcome = TransactionStatus.Active; + + lock (this) + { + if (_state != OletxVolatileEnlistmentState.Preparing) + { + throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId); + } + + _state = OletxVolatileEnlistmentState.Prepared; + localPendingOutcome = _pendingOutcome; + + if (_container == null) + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Prepared, no container member."); + throw new InvalidOperationException(SR.InternalError); + } + + localContainer = _container; + } + + // Vote yes. + localContainer.DecrementOutstandingNotifications(true); + + switch (localPendingOutcome) + { + case TransactionStatus.Active: + // nothing to do. Everything is proceeding as normal. + break; + + case TransactionStatus.Aborted: + // The transaction aborted while the Prepare was outstanding. + // We need to tell the app to rollback. + Rollback(); + break; + + case TransactionStatus.InDoubt: + // The transaction went InDoubt while the Prepare was outstanding. + // We need to tell the app. + InDoubt(); + break; + + default: + // This shouldn't happen. + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.Prepared, invalid pending outcome value."); + throw new InvalidOperationException(SR.InternalError); + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(IPromotedEnlistment.Prepared)}"); + } + } + + void IPromotedEnlistment.ForceRollback() + => ((IPromotedEnlistment)this).ForceRollback(null); + + void IPromotedEnlistment.ForceRollback(Exception? e) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(IPromotedEnlistment.ForceRollback)}"); + etwLog.EnlistmentCallbackNegative(InternalTraceIdentifier, EnlistmentCallback.ForceRollback); + } + + OletxVolatileEnlistmentContainer localContainer; + + lock (this) + { + if (_state != OletxVolatileEnlistmentState.Preparing) + { + throw TransactionException.CreateEnlistmentStateException(null, DistributedTxId); + } + + // There are no more notifications that need to happen on this enlistment. + _state = OletxVolatileEnlistmentState.Done; + + if (_container == null) + { + if (etwLog.IsEnabled()) + { + etwLog.InternalError(); + } + + Debug.Fail("OletxVolatileEnlistment.ForceRollback, no container member."); + throw new InvalidOperationException(SR.InternalError); + } + + localContainer = _container; + } + + Interlocked.CompareExchange(ref oletxTransaction!.RealOletxTransaction.InnerException, e, null); + + // Vote no. + localContainer.DecrementOutstandingNotifications(false); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this, $"OletxPreparingEnlistment.{nameof(IPromotedEnlistment.ForceRollback)}"); + } + } + + void IPromotedEnlistment.Committed() => throw new InvalidOperationException(); + void IPromotedEnlistment.Aborted() => throw new InvalidOperationException(); + void IPromotedEnlistment.Aborted(Exception? e) => throw new InvalidOperationException(); + void IPromotedEnlistment.InDoubt() => throw new InvalidOperationException(); + void IPromotedEnlistment.InDoubt(Exception? e) => throw new InvalidOperationException(); + + byte[] IPromotedEnlistment.GetRecoveryInformation() + => throw TransactionException.CreateInvalidOperationException( + TraceSourceType.TraceSourceOleTx, + SR.VolEnlistNoRecoveryInfo, + null, + DistributedTxId); + + InternalEnlistment? IPromotedEnlistment.InternalEnlistment + { + get => InternalEnlistment; + set => InternalEnlistment = value; + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Transaction.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Transaction.cs index ba6d5af2f3d66..10eac90c862d3 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/Transaction.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/Transaction.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading; -using System.Transactions.Distributed; +using System.Transactions.Oletx; namespace System.Transactions { @@ -276,7 +276,7 @@ internal Transaction(IsolationLevel isoLevel, InternalTransaction? internalTrans } } - internal Transaction(DistributedTransaction distributedTransaction) + internal Transaction(OletxTransaction distributedTransaction) { _isoLevel = distributedTransaction.IsolationLevel; _internalTransaction = new InternalTransaction(this, distributedTransaction); @@ -566,7 +566,7 @@ public void Rollback() if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceLtm, this); - etwLog.TransactionRollback(this, "Transaction"); + etwLog.TransactionRollback(TraceSourceType.TraceSourceLtm, TransactionTraceId, "Transaction"); } ObjectDisposedException.ThrowIf(Disposed, this); @@ -590,7 +590,7 @@ public void Rollback(Exception? e) if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceLtm, this); - etwLog.TransactionRollback(this, "Transaction"); + etwLog.TransactionRollback(TraceSourceType.TraceSourceLtm, TransactionTraceId, "Transaction"); } ObjectDisposedException.ThrowIf(Disposed, this); @@ -965,7 +965,7 @@ public Enlistment PromoteAndEnlistDurable(Guid resourceManagerIdentifier, TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, this); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, this); } ObjectDisposedException.ThrowIf(Disposed, this); @@ -996,7 +996,7 @@ public Enlistment PromoteAndEnlistDurable(Guid resourceManagerIdentifier, if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, this); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, this); } return enlistment; @@ -1041,7 +1041,7 @@ public void SetDistributedTransactionIdentifier(IPromotableSinglePhaseNotificati } } - internal DistributedTransaction? Promote() + internal OletxTransaction? Promote() { lock (_internalTransaction) { diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInterop.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInterop.cs index a65f8c24b9a90..a8983b5afad2b 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInterop.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInterop.cs @@ -1,26 +1,17 @@ // 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.Transactions.Distributed; +using System.Transactions.DtcProxyShim; +using System.Transactions.DtcProxyShim.DtcInterfaces; +using System.Transactions.Oletx; namespace System.Transactions { - [ComImport] - [Guid("0fb15084-af41-11ce-bd2b-204c4f4f5020")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IDtcTransaction - { - void Commit(int retaining, [MarshalAs(UnmanagedType.I4)] int commitType, int reserved); - - void Abort(IntPtr reason, int retaining, int async); - - void GetTransactionInfo(IntPtr transactionInformation); - } - public static class TransactionInterop { - internal static DistributedTransaction ConvertToDistributedTransaction(Transaction transaction) + internal static OletxTransaction ConvertToOletxTransaction(Transaction transaction) { ArgumentNullException.ThrowIfNull(transaction); @@ -31,12 +22,10 @@ internal static DistributedTransaction ConvertToDistributedTransaction(Transacti throw TransactionException.CreateTransactionCompletedException(transaction.DistributedTxId); } - DistributedTransaction? distributedTx = transaction.Promote(); - if (distributedTx == null) - { - throw DistributedTransaction.NotSupported(); - } - return distributedTx; + OletxTransaction? oletxTx = transaction.Promote(); + Debug.Assert(oletxTx != null, "transaction.Promote returned null instead of throwing."); + + return oletxTx; } /// @@ -62,19 +51,39 @@ public static byte[] GetExportCookie(Transaction transaction, byte[] whereabouts TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetExportCookie"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetExportCookie)}"); } + byte[] cookie; + // Copy the whereabouts so that it cannot be modified later. var whereaboutsCopy = new byte[whereabouts.Length]; Buffer.BlockCopy(whereabouts, 0, whereaboutsCopy, 0, whereabouts.Length); - ConvertToDistributedTransaction(transaction); - byte[] cookie = DistributedTransaction.GetExportCookie(whereaboutsCopy); + // First, make sure we are working with an OletxTransaction. + OletxTransaction oletxTx = ConvertToOletxTransaction(transaction); + + try + { + oletxTx.RealOletxTransaction.TransactionShim.Export(whereabouts, out cookie); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + + // We are unsure of what the exception may mean. It is possible that + // we could get E_FAIL when trying to contact a transaction manager that is + // being blocked by a fire wall. On the other hand we may get a COMException + // based on bad data. The more common situation is that the data is fine + // (since it is generated by Microsoft code) and the problem is with + // communication. So in this case we default for unknown exceptions to + // assume that the problem is with communication. + throw TransactionManagerCommunicationException.Create(null, comException); + } if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetExportCookie"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetExportCookie)}"); } return cookie; @@ -92,13 +101,20 @@ public static Transaction GetTransactionFromExportCookie(byte[] cookie) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromExportCookie"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromExportCookie)}"); } var cookieCopy = new byte[cookie.Length]; Buffer.BlockCopy(cookie, 0, cookieCopy, 0, cookie.Length); cookie = cookieCopy; + Transaction? transaction; + TransactionShim? transactionShim = null; + Guid txIdentifier = Guid.Empty; + OletxTransactionIsolationLevel oletxIsoLevel = OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE; + OutcomeEnlistment? outcomeEnlistment; + OletxTransaction? oleTx; + // Extract the transaction guid from the propagation token to see if we already have a // transaction object for the transaction. // In a cookie, the transaction guid is preceded by a signature guid. @@ -106,24 +122,64 @@ public static Transaction GetTransactionFromExportCookie(byte[] cookie) // First check to see if there is a promoted LTM transaction with the same ID. If there // is, just return that. - Transaction? transaction = TransactionManager.FindPromotedTransaction(txId); + transaction = TransactionManager.FindPromotedTransaction(txId); if (transaction != null) { if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromExportCookie"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromExportCookie"); } return transaction; } - // Find or create the promoted transaction. - DistributedTransaction dTx = DistributedTransactionManager.GetTransactionFromExportCookie(cookieCopy, txId); - transaction = TransactionManager.FindOrCreatePromotedTransaction(txId, dTx); + // We need to create a new transaction + RealOletxTransaction? realTx = null; + OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager; + + oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1); + try + { + outcomeEnlistment = new OutcomeEnlistment(); + oletxTm.DtcTransactionManager.ProxyShimFactory.Import(cookie, outcomeEnlistment, out txIdentifier, out oletxIsoLevel, out transactionShim); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + + // We are unsure of what the exception may mean. It is possible that + // we could get E_FAIL when trying to contact a transaction manager that is + // being blocked by a fire wall. On the other hand we may get a COMException + // based on bad data. The more common situation is that the data is fine + // (since it is generated by Microsoft code) and the problem is with + // communication. So in this case we default for unknown exceptions to + // assume that the problem is with communication. + throw TransactionManagerCommunicationException.Create(SR.TraceSourceOletx, comException); + } + finally + { + oletxTm.DtcTransactionManagerLock.ReleaseReaderLock(); + } + + // We need to create a new RealOletxTransaction. + realTx = new RealOletxTransaction( + oletxTm, + transactionShim, + outcomeEnlistment, + txIdentifier, + oletxIsoLevel, + false); + + // Now create the associated OletxTransaction. + oleTx = new OletxTransaction(realTx); + + // If a transaction is found then FindOrCreate will Dispose the oletx + // created. + transaction = TransactionManager.FindOrCreatePromotedTransaction(txId, oleTx); if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromExportCookie"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromExportCookie)}"); } return transaction; @@ -136,20 +192,39 @@ public static byte[] GetTransmitterPropagationToken(Transaction transaction) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransmitterPropagationToken"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransmitterPropagationToken)}"); } - ConvertToDistributedTransaction(transaction); - byte[] token = DistributedTransaction.GetTransmitterPropagationToken(); + // First, make sure we are working with an OletxTransaction. + OletxTransaction oletxTx = ConvertToOletxTransaction(transaction); + + byte[] token = GetTransmitterPropagationToken(oletxTx); if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransmitterPropagationToken"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransmitterPropagationToken)}"); } return token; } + internal static byte[] GetTransmitterPropagationToken(OletxTransaction oletxTx) + { + byte[]? propagationToken = null; + + try + { + propagationToken = oletxTx.RealOletxTransaction.TransactionShim.GetPropagationToken(); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + throw; + } + + return propagationToken; + } + public static Transaction GetTransactionFromTransmitterPropagationToken(byte[] propagationToken) { ArgumentNullException.ThrowIfNull(propagationToken); @@ -162,7 +237,7 @@ public static Transaction GetTransactionFromTransmitterPropagationToken(byte[] p TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); } // Extract the transaction guid from the propagation token to see if we already have a @@ -176,20 +251,20 @@ public static Transaction GetTransactionFromTransmitterPropagationToken(byte[] p { if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); } return tx; } - DistributedTransaction dTx = GetDistributedTransactionFromTransmitterPropagationToken(propagationToken); + OletxTransaction dTx = GetOletxTransactionFromTransmitterPropagationToken(propagationToken); // If a transaction is found then FindOrCreate will Dispose the distributed transaction created. Transaction returnValue = TransactionManager.FindOrCreatePromotedTransaction(txId, dTx); if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); } return returnValue; } @@ -201,15 +276,27 @@ public static IDtcTransaction GetDtcTransaction(Transaction transaction) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetDtcTransaction"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetDtcTransaction)}"); } - ConvertToDistributedTransaction(transaction); - IDtcTransaction transactionNative = DistributedTransaction.GetDtcTransaction(); + IDtcTransaction? transactionNative; + + // First, make sure we are working with an OletxTransaction. + OletxTransaction oletxTx = ConvertToOletxTransaction(transaction); + + try + { + oletxTx.RealOletxTransaction.TransactionShim.GetITransactionNative(out transactionNative); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + throw; + } if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetDtcTransaction"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetDtcTransaction)}"); } return transactionNative; @@ -217,20 +304,121 @@ public static IDtcTransaction GetDtcTransaction(Transaction transaction) public static Transaction GetTransactionFromDtcTransaction(IDtcTransaction transactionNative) { - ArgumentNullException.ThrowIfNull(transactionNative); + ArgumentNullException.ThrowIfNull(transactionNative, nameof(transactionNative)); TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromDtcTransaction"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}"); + } + + Transaction? transaction = null; + bool tooLate = false; + TransactionShim? transactionShim = null; + Guid txIdentifier = Guid.Empty; + OletxTransactionIsolationLevel oletxIsoLevel = OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE; + OutcomeEnlistment? outcomeEnlistment = null; + RealOletxTransaction? realTx = null; + OletxTransaction? oleTx = null; + + // Let's get the guid of the transaction from the proxy to see if we already have an object. + if (transactionNative is not ITransaction myTransactionNative) + { + throw new ArgumentException(SR.InvalidArgument, nameof(transactionNative)); + } + + OletxXactTransInfo xactInfo; + try + { + myTransactionNative.GetTransactionInfo(out xactInfo); + } + catch (COMException ex) when (ex.ErrorCode == OletxHelper.XACT_E_NOTRANSACTION) + { + // If we get here, the transaction has appraently already been committed or aborted. Allow creation of the + // OletxTransaction, but it will be marked with a status of InDoubt and attempts to get its Identifier + // property will result in a TransactionException. + tooLate = true; + xactInfo.Uow = Guid.Empty; } - Transaction transaction = DistributedTransactionManager.GetTransactionFromDtcTransaction(transactionNative); + OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager; + if (!tooLate) + { + // First check to see if there is a promoted LTM transaction with the same ID. If there + // is, just return that. + transaction = TransactionManager.FindPromotedTransaction(xactInfo.Uow); + if (transaction != null) + { + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}"); + } + + return transaction; + } + + // We need to create a new RealOletxTransaction... + oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1); + try + { + outcomeEnlistment = new OutcomeEnlistment(); + oletxTm.DtcTransactionManager.ProxyShimFactory.CreateTransactionShim( + transactionNative, + outcomeEnlistment, + out txIdentifier, + out oletxIsoLevel, + out transactionShim); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + throw; + } + finally + { + oletxTm.DtcTransactionManagerLock.ReleaseReaderLock(); + } + + // We need to create a new RealOletxTransaction. + realTx = new RealOletxTransaction( + oletxTm, + transactionShim, + outcomeEnlistment, + txIdentifier, + oletxIsoLevel, + false); + + oleTx = new OletxTransaction(realTx); + + // If a transaction is found then FindOrCreate will Dispose the oletx + // created. + transaction = TransactionManager.FindOrCreatePromotedTransaction(xactInfo.Uow, oleTx); + } + else + { + // It was too late to do a clone of the provided ITransactionNative, so we are just going to + // create a RealOletxTransaction without a transaction shim or outcome enlistment. + realTx = new RealOletxTransaction( + oletxTm, + null, + null, + txIdentifier, + OletxTransactionIsolationLevel.ISOLATIONLEVEL_SERIALIZABLE, + false); + + oleTx = new OletxTransaction(realTx); + transaction = new Transaction(oleTx); + TransactionManager.FireDistributedTransactionStarted(transaction); + oleTx.SavedLtmPromotedTransaction = transaction; + + InternalTransaction.DistributedTransactionOutcome(transaction._internalTransaction, TransactionStatus.InDoubt); + } if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetTransactionFromDtcTransaction"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.{nameof(GetTransactionFromDtcTransaction)}"); } + return transaction; } @@ -239,19 +427,36 @@ public static byte[] GetWhereabouts() TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.MethodEnter(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetWhereabouts"); + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.${nameof(GetWhereabouts)}"); + } + + OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager; + if (oletxTm == null) + { + throw new ArgumentException(SR.InvalidArgument, "transactionManager"); } - byte[] returnValue = DistributedTransactionManager.GetWhereabouts(); + byte[]? returnValue; + + oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1); + try + { + returnValue = oletxTm.DtcTransactionManager.Whereabouts; + } + finally + { + oletxTm.DtcTransactionManagerLock.ReleaseReaderLock(); + } if (etwLog.IsEnabled()) { - etwLog.MethodExit(TraceSourceType.TraceSourceDistributed, "TransactionInterop.GetWhereabouts"); + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionInterop)}.${nameof(GetWhereabouts)}"); } + return returnValue; } - internal static DistributedTransaction GetDistributedTransactionFromTransmitterPropagationToken(byte[] propagationToken) + internal static OletxTransaction GetOletxTransactionFromTransmitterPropagationToken(byte[] propagationToken) { ArgumentNullException.ThrowIfNull(propagationToken); @@ -260,10 +465,56 @@ internal static DistributedTransaction GetDistributedTransactionFromTransmitterP throw new ArgumentException(SR.InvalidArgument, nameof(propagationToken)); } + Guid identifier; + OletxTransactionIsolationLevel oletxIsoLevel; + OutcomeEnlistment outcomeEnlistment; + TransactionShim? transactionShim = null; + byte[] propagationTokenCopy = new byte[propagationToken.Length]; Array.Copy(propagationToken, propagationTokenCopy, propagationToken.Length); + propagationToken = propagationTokenCopy; + + // First we need to create an OletxTransactionManager from Config. + OletxTransactionManager oletxTm = TransactionManager.DistributedTransactionManager; + + oletxTm.DtcTransactionManagerLock.AcquireReaderLock(-1); + try + { + outcomeEnlistment = new OutcomeEnlistment(); + oletxTm.DtcTransactionManager.ProxyShimFactory.ReceiveTransaction( + propagationToken, + outcomeEnlistment, + out identifier, + out oletxIsoLevel, + out transactionShim); + } + catch (COMException comException) + { + OletxTransactionManager.ProxyException(comException); + + // We are unsure of what the exception may mean. It is possible that + // we could get E_FAIL when trying to contact a transaction manager that is + // being blocked by a fire wall. On the other hand we may get a COMException + // based on bad data. The more common situation is that the data is fine + // (since it is generated by Microsoft code) and the problem is with + // communication. So in this case we default for unknown exceptions to + // assume that the problem is with communication. + throw TransactionManagerCommunicationException.Create(SR.TraceSourceOletx, comException); + } + finally + { + oletxTm.DtcTransactionManagerLock.ReleaseReaderLock(); + } + + var realTx = new RealOletxTransaction( + oletxTm, + transactionShim, + outcomeEnlistment, + identifier, + oletxIsoLevel, + false); - return DistributedTransactionManager.GetDistributedTransactionFromTransmitterPropagationToken(propagationTokenCopy); + return new OletxTransaction(realTx); } } } diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInteropNonWindows.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInteropNonWindows.cs new file mode 100644 index 0000000000000..d656c1a6458d5 --- /dev/null +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionInteropNonWindows.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Transactions.Oletx; + +namespace System.Transactions +{ + public static class TransactionInterop + { + internal static OletxTransaction ConvertToOletxTransaction(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction); + + ObjectDisposedException.ThrowIf(transaction.Disposed, transaction); + + if (transaction._complete) + { + throw TransactionException.CreateTransactionCompletedException(transaction.DistributedTxId); + } + + OletxTransaction? distributedTx = transaction.Promote(); + if (distributedTx == null) + { + throw OletxTransaction.NotSupported(); + } + return distributedTx; + } + + /// + /// This is the PromoterType value that indicates that the transaction is promoting to MSDTC. + /// + /// If using the variation of Transaction.EnlistPromotableSinglePhase that takes a PromoterType and the + /// ITransactionPromoter being used promotes to MSDTC, then this is the value that should be + /// specified for the PromoterType parameter to EnlistPromotableSinglePhase. + /// + /// If using the variation of Transaction.EnlistPromotableSinglePhase that assumes promotion to MSDTC and + /// it that returns false, the caller can compare this value with Transaction.PromoterType to + /// verify that the transaction promoted, or will promote, to MSDTC. If the Transaction.PromoterType + /// matches this value, then the caller can continue with its enlistment with MSDTC. But if it + /// does not match, the caller will not be able to enlist with MSDTC. + /// + public static readonly Guid PromoterTypeDtc = new Guid("14229753-FFE1-428D-82B7-DF73045CB8DA"); + + public static byte[] GetExportCookie(Transaction transaction, byte[] whereabouts) + { + ArgumentNullException.ThrowIfNull(transaction); + ArgumentNullException.ThrowIfNull(whereabouts); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetExportCookie"); + } + + // Copy the whereabouts so that it cannot be modified later. + var whereaboutsCopy = new byte[whereabouts.Length]; + Buffer.BlockCopy(whereabouts, 0, whereaboutsCopy, 0, whereabouts.Length); + + ConvertToOletxTransaction(transaction); + byte[] cookie = OletxTransaction.GetExportCookie(whereaboutsCopy); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetExportCookie"); + } + + return cookie; + } + + public static Transaction GetTransactionFromExportCookie(byte[] cookie) + { + ArgumentNullException.ThrowIfNull(cookie); + + if (cookie.Length < 32) + { + throw new ArgumentException(SR.InvalidArgument, nameof(cookie)); + } + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromExportCookie"); + } + + var cookieCopy = new byte[cookie.Length]; + Buffer.BlockCopy(cookie, 0, cookieCopy, 0, cookie.Length); + cookie = cookieCopy; + + // Extract the transaction guid from the propagation token to see if we already have a + // transaction object for the transaction. + // In a cookie, the transaction guid is preceded by a signature guid. + var txId = new Guid(cookie.AsSpan(16, 16)); + + // First check to see if there is a promoted LTM transaction with the same ID. If there + // is, just return that. + Transaction? transaction = TransactionManager.FindPromotedTransaction(txId); + if (transaction != null) + { + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromExportCookie"); + } + + return transaction; + } + + // Find or create the promoted transaction. + OletxTransaction dTx = OletxTransactionManager.GetTransactionFromExportCookie(cookieCopy, txId); + transaction = TransactionManager.FindOrCreatePromotedTransaction(txId, dTx); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromExportCookie"); + } + + return transaction; + } + + public static byte[] GetTransmitterPropagationToken(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransmitterPropagationToken"); + } + + ConvertToOletxTransaction(transaction); + byte[] token = OletxTransaction.GetTransmitterPropagationToken(); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransmitterPropagationToken"); + } + + return token; + } + + public static Transaction GetTransactionFromTransmitterPropagationToken(byte[] propagationToken) + { + ArgumentNullException.ThrowIfNull(propagationToken); + + if (propagationToken.Length < 24) + { + throw new ArgumentException(SR.InvalidArgument, nameof(propagationToken)); + } + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + } + + // Extract the transaction guid from the propagation token to see if we already have a + // transaction object for the transaction. + // In a propagation token, the transaction guid is preceded by two version DWORDs. + var txId = new Guid(propagationToken.AsSpan(8, 16)); + + // First check to see if there is a promoted LTM transaction with the same ID. If there is, just return that. + Transaction? tx = TransactionManager.FindPromotedTransaction(txId); + if (null != tx) + { + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + } + + return tx; + } + + OletxTransaction dTx = GetOletxTransactionFromTransmitterPropagationToken(propagationToken); + + // If a transaction is found then FindOrCreate will Dispose the distributed transaction created. + Transaction returnValue = TransactionManager.FindOrCreatePromotedTransaction(txId, dTx); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromTransmitterPropagationToken"); + } + return returnValue; + } + + public static IDtcTransaction GetDtcTransaction(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetDtcTransaction"); + } + + ConvertToOletxTransaction(transaction); + IDtcTransaction transactionNative = OletxTransaction.GetDtcTransaction(); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetDtcTransaction"); + } + + return transactionNative; + } + + public static Transaction GetTransactionFromDtcTransaction(IDtcTransaction transactionNative) + { + ArgumentNullException.ThrowIfNull(transactionNative); + + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromDtcTransaction"); + } + + Transaction transaction = OletxTransactionManager.GetTransactionFromDtcTransaction(transactionNative); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetTransactionFromDtcTransaction"); + } + return transaction; + } + + public static byte[] GetWhereabouts() + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetWhereabouts"); + } + + byte[] returnValue = OletxTransactionManager.GetWhereabouts(); + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, "TransactionInterop.GetWhereabouts"); + } + return returnValue; + } + + internal static OletxTransaction GetOletxTransactionFromTransmitterPropagationToken(byte[] propagationToken) + { + ArgumentNullException.ThrowIfNull(propagationToken); + + if (propagationToken.Length < 24) + { + throw new ArgumentException(SR.InvalidArgument, nameof(propagationToken)); + } + + byte[] propagationTokenCopy = new byte[propagationToken.Length]; + Array.Copy(propagationToken, propagationTokenCopy, propagationToken.Length); + + return OletxTransactionManager.GetOletxTransactionFromTransmitterPropagationToken(propagationTokenCopy); + } + } +} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs index ff811ad32a6de..7ce21c51bad23 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs @@ -6,7 +6,7 @@ using System.IO; using System.Threading; using System.Transactions.Configuration; -using System.Transactions.Distributed; +using System.Transactions.Oletx; namespace System.Transactions { @@ -18,6 +18,7 @@ public static class TransactionManager { // Revovery Information Version private const int RecoveryInformationVersion1 = 1; + private const int CurrentRecoveryVersion = RecoveryInformationVersion1; // Hashtable of promoted transactions, keyed by identifier guid. This is used by // FindPromotedTransaction to support transaction equivalence when a transaction is @@ -215,9 +216,9 @@ public static Enlistment Reenlist( } - private static DistributedTransactionManager CheckTransactionManager(string? nodeName) + private static OletxTransactionManager CheckTransactionManager(string? nodeName) { - DistributedTransactionManager tm = DistributedTransactionManager; + OletxTransactionManager tm = DistributedTransactionManager; if (!((tm.NodeName == null && (nodeName == null || nodeName.Length == 0)) || (tm.NodeName != null && tm.NodeName.Equals(nodeName)))) { @@ -390,6 +391,55 @@ public static TimeSpan MaximumTimeout } } + // This routine writes the "header" for the recovery information, based on the + // type of the calling object and its provided parameter collection. This information + // we be read back by the static Reenlist method to create the necessary transaction + // manager object with the right parameters in order to do a ReenlistTransaction call. + internal static byte[] GetRecoveryInformation( + string? startupInfo, + byte[] resourceManagerRecoveryInformation + ) + { + TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; + if (etwLog.IsEnabled()) + { + etwLog.MethodEnter(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionManager)}.{nameof(GetRecoveryInformation)}"); + } + + MemoryStream stream = new MemoryStream(); + byte[]? returnValue = null; + + try + { + // Manually write the recovery information + BinaryWriter writer = new BinaryWriter(stream); + + writer.Write(CurrentRecoveryVersion); + if (startupInfo != null) + { + writer.Write(startupInfo); + } + else + { + writer.Write(""); + } + writer.Write(resourceManagerRecoveryInformation); + writer.Flush(); + returnValue = stream.ToArray(); + } + finally + { + stream.Close(); + } + + if (etwLog.IsEnabled()) + { + etwLog.MethodExit(TraceSourceType.TraceSourceOleTx, $"{nameof(TransactionManager)}.{nameof(GetRecoveryInformation)}"); + } + + return returnValue; + } + /// /// This static function throws an ArgumentOutOfRange if the specified IsolationLevel is not within /// the range of valid values. @@ -414,7 +464,6 @@ internal static void ValidateIsolationLevel(IsolationLevel transactionIsolationL } } - /// /// This static function throws an ArgumentOutOfRange if the specified TimeSpan does not meet /// requirements of a valid transaction timeout. Timeout values must be positive. @@ -462,7 +511,7 @@ internal static TimeSpan ValidateTimeout(TimeSpan transactionTimeout) return null; } - internal static Transaction FindOrCreatePromotedTransaction(Guid transactionIdentifier, DistributedTransaction dtx) + internal static Transaction FindOrCreatePromotedTransaction(Guid transactionIdentifier, OletxTransaction dtx) { Transaction? tx = null; Hashtable promotedTransactionTable = PromotedTransactionTable; @@ -511,9 +560,10 @@ internal static Transaction FindOrCreatePromotedTransaction(Guid transactionIden LazyInitializer.EnsureInitialized(ref s_transactionTable, ref s_classSyncObject, () => new TransactionTable()); // Fault in a DistributedTransactionManager if one has not already been created. - internal static DistributedTransactionManager? distributedTransactionManager; - internal static DistributedTransactionManager DistributedTransactionManager => + internal static OletxTransactionManager? distributedTransactionManager; + internal static OletxTransactionManager DistributedTransactionManager => // If the distributed transaction manager is not configured, throw an exception - LazyInitializer.EnsureInitialized(ref distributedTransactionManager, ref s_classSyncObject, () => new DistributedTransactionManager()); + LazyInitializer.EnsureInitialized(ref distributedTransactionManager, ref s_classSyncObject, + () => new OletxTransactionManager(DefaultSettingsSection.DistributedTransactionManagerName)); } } diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionScope.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionScope.cs index 938a4c0735beb..0ca05d76d4bb7 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionScope.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionScope.cs @@ -854,7 +854,7 @@ private static void TimerCallback(object? state) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionScopeInternalError("TransactionScopeTimerObjectInvalid"); + etwLog.InternalError("TransactionScopeTimerObjectInvalid"); } throw TransactionException.Create(TraceSourceType.TraceSourceBase, SR.InternalError + SR.TransactionScopeTimerObjectInvalid, null); diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionState.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionState.cs index f6a99cb47d940..331e679db04ea 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionState.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionState.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.Serialization; using System.Threading; -using System.Transactions.Distributed; +using System.Transactions.Oletx; namespace System.Transactions { @@ -1532,7 +1532,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionCommitted(tx.TransactionTraceId); + etwLog.TransactionCommitted(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } // Fire Completion for anyone listening @@ -1595,7 +1595,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionInDoubt(tx.TransactionTraceId); + etwLog.TransactionInDoubt(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } // Fire Completion for anyone listening @@ -1676,7 +1676,7 @@ Transaction atomicTransaction EnlistmentState.EnlistmentStatePromoted.EnterState(en.InternalEnlistment); en.InternalEnlistment.PromotedEnlistment = - DistributedTransaction.EnlistVolatile( + tx.PromotedTransaction.EnlistVolatile( en.InternalEnlistment, enlistmentOptions); return en; } @@ -1705,7 +1705,7 @@ Transaction atomicTransaction EnlistmentState.EnlistmentStatePromoted.EnterState(en.InternalEnlistment); en.InternalEnlistment.PromotedEnlistment = - DistributedTransaction.EnlistVolatile( + tx.PromotedTransaction.EnlistVolatile( en.InternalEnlistment, enlistmentOptions); return en; } @@ -1742,7 +1742,7 @@ Transaction atomicTransaction EnlistmentState.EnlistmentStatePromoted.EnterState(en.InternalEnlistment); en.InternalEnlistment.PromotedEnlistment = - DistributedTransaction.EnlistDurable( + tx.PromotedTransaction.EnlistDurable( resourceManagerIdentifier, (DurableInternalEnlistment)en.InternalEnlistment, false, @@ -1783,7 +1783,7 @@ Transaction atomicTransaction EnlistmentState.EnlistmentStatePromoted.EnterState(en.InternalEnlistment); en.InternalEnlistment.PromotedEnlistment = - DistributedTransaction.EnlistDurable( + tx.PromotedTransaction.EnlistDurable( resourceManagerIdentifier, (DurableInternalEnlistment)en.InternalEnlistment, true, @@ -1809,7 +1809,7 @@ internal override void Rollback(InternalTransaction tx, Exception? e) Monitor.Exit(tx); try { - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); } finally { @@ -1894,16 +1894,17 @@ internal override void CompleteBlockingClone(InternalTransaction tx) Debug.Assert(tx._phase0WaveDependentCloneCount >= 0); if (tx._phase0WaveDependentCloneCount == 0) { + OletxDependentTransaction dtx = tx._phase0WaveDependentClone!; tx._phase0WaveDependentClone = null; Monitor.Exit(tx); try { - DistributedDependentTransaction.Complete(); + dtx.Complete(); } finally { - Monitor.Enter(tx); + dtx.Dispose(); } } } @@ -1930,22 +1931,22 @@ internal override void CompleteAbortingClone(InternalTransaction tx) { // We need to complete our dependent clone on the promoted transaction and null it out // so if we get a new one, a new one will be created on the promoted transaction. + OletxDependentTransaction dtx = tx._abortingDependentClone!; tx._abortingDependentClone = null; Monitor.Exit(tx); try { - DistributedDependentTransaction.Complete(); + dtx.Complete(); } finally { - Monitor.Enter(tx); + dtx.Dispose(); } } } } - internal override void CreateBlockingClone(InternalTransaction tx) { // Once the transaction is promoted leverage the distributed @@ -1954,7 +1955,7 @@ internal override void CreateBlockingClone(InternalTransaction tx) if (tx._phase0WaveDependentClone == null) { Debug.Assert(tx.PromotedTransaction != null); - tx._phase0WaveDependentClone = DistributedTransaction.DependentClone(true); + tx._phase0WaveDependentClone = tx.PromotedTransaction.DependentClone(true); } tx._phase0WaveDependentCloneCount++; @@ -1976,7 +1977,7 @@ internal override void CreateAbortingClone(InternalTransaction tx) if (null == tx._abortingDependentClone) { Debug.Assert(tx.PromotedTransaction != null); - tx._abortingDependentClone = DistributedTransaction.DependentClone(false); + tx._abortingDependentClone = tx.PromotedTransaction.DependentClone(false); } tx._abortingDependentCloneCount++; } @@ -2050,7 +2051,7 @@ internal override void Timeout(InternalTransaction tx) { tx._innerException ??= new TimeoutException(SR.TraceTransactionTimeout); Debug.Assert(tx.PromotedTransaction != null); - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) @@ -2143,7 +2144,7 @@ internal override void EnterState(InternalTransaction tx) CommonEnterState(tx); // Create a transaction with the distributed transaction manager - DistributedCommittableTransaction? distributedTx = null; + OletxCommittableTransaction? distributedTx = null; try { TimeSpan newTimeout; @@ -2169,7 +2170,7 @@ internal override void EnterState(InternalTransaction tx) // Create a new distributed transaction. distributedTx = - DistributedTransactionManager.CreateTransaction(options); + TransactionManager.DistributedTransactionManager.CreateTransaction(options); distributedTx.SavedLtmPromotedTransaction = tx._outcomeSource; TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; @@ -2240,7 +2241,7 @@ protected static bool PromotePhaseVolatiles( } Debug.Assert(tx.PromotedTransaction != null); - volatiles.VolatileDemux._promotedEnlistment = DistributedTransaction.EnlistVolatile(volatiles.VolatileDemux, + volatiles.VolatileDemux._promotedEnlistment = tx.PromotedTransaction.EnlistVolatile(volatiles.VolatileDemux, phase0 ? EnlistmentOptions.EnlistDuringPrepareRequired : EnlistmentOptions.None); } @@ -2256,7 +2257,7 @@ internal virtual bool PromoteDurable(InternalTransaction tx) // Directly enlist the durable enlistment with the resource manager. InternalEnlistment enlistment = tx._durableEnlistment; Debug.Assert(tx.PromotedTransaction != null); - IPromotedEnlistment promotedEnlistment = DistributedTransaction.EnlistDurable( + IPromotedEnlistment promotedEnlistment = tx.PromotedTransaction.EnlistDurable( enlistment.ResourceManagerIdentifier, (DurableInternalEnlistment)enlistment, enlistment.SinglePhaseNotification != null, @@ -2306,7 +2307,7 @@ internal virtual void PromoteEnlistmentsAndOutcome(InternalTransaction tx) { if (!enlistmentsPromoted) { - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); // Now abort this transaction. tx.State.ChangeStateAbortedDuringPromotion(tx); @@ -2335,7 +2336,7 @@ internal virtual void PromoteEnlistmentsAndOutcome(InternalTransaction tx) { if (!enlistmentsPromoted) { - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); // Now abort this transaction. tx.State.ChangeStateAbortedDuringPromotion(tx); @@ -2365,7 +2366,7 @@ internal virtual void PromoteEnlistmentsAndOutcome(InternalTransaction tx) { if (!enlistmentsPromoted) { - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); // Now abort this transaction. tx.State.ChangeStateAbortedDuringPromotion(tx); @@ -2450,7 +2451,8 @@ internal override void EnterState(InternalTransaction tx) CommonEnterState(tx); // Use the asynchronous commit provided by the promoted transaction - DistributedCommittableTransaction.BeginCommit(tx); + OletxCommittableTransaction ctx = (OletxCommittableTransaction)tx.PromotedTransaction!; + ctx.BeginCommit(tx); } @@ -2762,7 +2764,7 @@ internal override void EnterState(InternalTransaction tx) { Debug.Assert(tx.PromotedTransaction != null); // Otherwise make sure that the transaction rolls back. - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); } } @@ -2932,7 +2934,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionAborted(tx.TransactionTraceId); + etwLog.TransactionAborted(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -3076,7 +3078,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionCommitted(tx.TransactionTraceId); + etwLog.TransactionCommitted(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -3146,7 +3148,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionInDoubt(tx.TransactionTraceId); + etwLog.TransactionInDoubt(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -3247,7 +3249,7 @@ internal override void EnterState(InternalTransaction tx) CommonEnterState(tx); // Create a transaction with the distributed transaction manager - DistributedTransaction? distributedTx = null; + Oletx.OletxTransaction? distributedTx = null; try { // Ask the delegation interface to promote the transaction. @@ -3256,7 +3258,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(tx._durableEnlistment, NotificationCall.Promote); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, tx._durableEnlistment.EnlistmentTraceId, NotificationCall.Promote); } } @@ -3834,7 +3836,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(tx._durableEnlistment, NotificationCall.SinglePhaseCommit); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, tx._durableEnlistment.EnlistmentTraceId, NotificationCall.SinglePhaseCommit); } // We are about to tell the PSPE to do the SinglePhaseCommit. It is too late for us to timeout the transaction. @@ -4036,7 +4038,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionAborted(tx.TransactionTraceId); + etwLog.TransactionAborted(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -4129,7 +4131,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionCommitted(tx.TransactionTraceId); + etwLog.TransactionCommitted(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -4173,7 +4175,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.TransactionInDoubt(tx.TransactionTraceId); + etwLog.TransactionInDoubt(TraceSourceType.TraceSourceLtm, tx.TransactionTraceId); } } @@ -4233,7 +4235,7 @@ internal override void EnterState(InternalTransaction tx) CommonEnterState(tx); // We are never going to have an DistributedTransaction for this one. - DistributedTransaction? distributedTx; + OletxTransaction? distributedTx; try { // Ask the delegation interface to promote the transaction. @@ -4242,7 +4244,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(tx._durableEnlistment, NotificationCall.Promote); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, tx._durableEnlistment.EnlistmentTraceId, NotificationCall.Promote); } } @@ -4291,7 +4293,7 @@ internal override void Rollback(InternalTransaction tx, Exception? e) tx._innerException ??= e; Debug.Assert(tx.PromotedTransaction != null); - DistributedTransaction.Rollback(); + tx.PromotedTransaction.Rollback(); TransactionStatePromotedAborted.EnterState(tx); } @@ -4379,7 +4381,7 @@ internal void Phase0PSPEInitialize( } } - internal DistributedTransaction? PSPEPromote(InternalTransaction tx) + internal Oletx.OletxTransaction? PSPEPromote(InternalTransaction tx) { bool changeToReturnState = true; @@ -4390,7 +4392,7 @@ internal void Phase0PSPEInitialize( "PSPEPromote called from state other than TransactionStateDelegated[NonMSDTC]"); CommonEnterState(tx); - DistributedTransaction? distributedTx = null; + Oletx.OletxTransaction? distributedTx = null; try { if (tx._attemptingPSPEPromote) @@ -4457,7 +4459,7 @@ internal void Phase0PSPEInitialize( { try { - distributedTx = TransactionInterop.GetDistributedTransactionFromTransmitterPropagationToken( + distributedTx = TransactionInterop.GetOletxTransactionFromTransmitterPropagationToken( propagationToken! ); } @@ -4603,13 +4605,12 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(tx._durableEnlistment, NotificationCall.SinglePhaseCommit); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, tx._durableEnlistment.EnlistmentTraceId, NotificationCall.SinglePhaseCommit); } try { - tx._durableEnlistment.PromotableSinglePhaseNotification.SinglePhaseCommit( - tx._durableEnlistment.SinglePhaseEnlistment); + tx._durableEnlistment.PromotableSinglePhaseNotification.SinglePhaseCommit(tx._durableEnlistment.SinglePhaseEnlistment); } finally { @@ -4640,7 +4641,7 @@ internal override void EnterState(InternalTransaction tx) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(tx._durableEnlistment, NotificationCall.Rollback); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, tx._durableEnlistment.EnlistmentTraceId, NotificationCall.Rollback); } tx._durableEnlistment.PromotableSinglePhaseNotification.Rollback( diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionsEtwProvider.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionsEtwProvider.cs index f6fd5f6b87e2d..ec230efce8972 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionsEtwProvider.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionsEtwProvider.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Collections; +using System.Transactions.Oletx; namespace System.Transactions { @@ -34,6 +35,16 @@ internal enum NotificationCall Promote = 5 } + internal enum EnlistmentCallback + { + Done = 0, + Prepared = 1, + ForceRollback = 2, + Committed = 3, + Aborted = 4, + InDoubt = 5 + } + internal enum TransactionScopeResult { CreatedTransaction = 0, @@ -57,7 +68,7 @@ internal enum TraceSourceType { TraceSourceBase = 0, TraceSourceLtm = 1, - TraceSourceDistributed = 2 + TraceSourceOleTx = 2 } /// Provides an event source for tracing Transactions information. [EventSource( @@ -94,7 +105,7 @@ private TransactionsEtwProvider() { } /// The event ID for the enlistment done event. private const int ENLISTMENT_DONE_EVENTID = 4; /// The event ID for the enlistment status. - private const int ENLISTMENT_EVENTID = 5; + private const int ENLISTMENT_LTM_EVENTID = 5; /// The event ID for the enlistment forcerollback event. private const int ENLISTMENT_FORCEROLLBACK_EVENTID = 6; /// The event ID for the enlistment indoubt event. @@ -114,33 +125,33 @@ private TransactionsEtwProvider() { } /// The event ID for method exit event. private const int METHOD_EXIT_BASE_EVENTID = 14; /// The event ID for method enter event. - private const int METHOD_ENTER_DISTRIBUTED_EVENTID = 15; + private const int METHOD_ENTER_OLETX_EVENTID = 15; /// The event ID for method exit event. - private const int METHOD_EXIT_DISTRIBUTED_EVENTID = 16; + private const int METHOD_EXIT_OLETX_EVENTID = 16; /// The event ID for transaction aborted event. - private const int TRANSACTION_ABORTED_EVENTID = 17; + private const int TRANSACTION_ABORTED_LTM_EVENTID = 17; /// The event ID for the transaction clone create event. private const int TRANSACTION_CLONECREATE_EVENTID = 18; /// The event ID for the transaction commit event. - private const int TRANSACTION_COMMIT_EVENTID = 19; + private const int TRANSACTION_COMMIT_LTM_EVENTID = 19; /// The event ID for transaction committed event. - private const int TRANSACTION_COMMITTED_EVENTID = 20; + private const int TRANSACTION_COMMITTED_LTM_EVENTID = 20; /// The event ID for when we encounter a new Transactions object that hasn't had its name traced to the trace file. - private const int TRANSACTION_CREATED_EVENTID = 21; + private const int TRANSACTION_CREATED_LTM_EVENTID = 21; /// The event ID for the transaction dependent clone complete event. - private const int TRANSACTION_DEPENDENT_CLONE_COMPLETE_EVENTID = 22; + private const int TRANSACTION_DEPENDENT_CLONE_COMPLETE_LTM_EVENTID = 22; /// The event ID for the transaction exception event. private const int TRANSACTION_EXCEPTION_LTM_EVENTID = 23; /// The event ID for the transaction exception event. private const int TRANSACTION_EXCEPTION_BASE_EVENTID = 24; /// The event ID for transaction indoubt event. - private const int TRANSACTION_INDOUBT_EVENTID = 25; + private const int TRANSACTION_INDOUBT_LTM_EVENTID = 25; /// The event ID for the transaction invalid operation event. private const int TRANSACTION_INVALID_OPERATION_EVENTID = 26; /// The event ID for transaction promoted event. private const int TRANSACTION_PROMOTED_EVENTID = 27; /// The event ID for the transaction rollback event. - private const int TRANSACTION_ROLLBACK_EVENTID = 28; + private const int TRANSACTION_ROLLBACK_LTM_EVENTID = 28; /// The event ID for the transaction serialized event. private const int TRANSACTION_SERIALIZED_EVENTID = 29; /// The event ID for transaction timeout event. @@ -158,7 +169,7 @@ private TransactionsEtwProvider() { } /// The event ID for transactionscope incomplete event. private const int TRANSACTIONSCOPE_INCOMPLETE_EVENTID = 36; /// The event ID for transactionscope internal error event. - private const int TRANSACTIONSCOPE_INTERNAL_ERROR_EVENTID = 37; + private const int INTERNAL_ERROR_EVENTID = 37; /// The event ID for transactionscope nested incorrectly event. private const int TRANSACTIONSCOPE_NESTED_INCORRECTLY_EVENTID = 38; /// The event ID for transactionscope timeout event. @@ -166,6 +177,43 @@ private TransactionsEtwProvider() { } /// The event ID for enlistment event. private const int TRANSACTIONSTATE_ENLIST_EVENTID = 40; + /// The event ID for the transaction commit event. + private const int TRANSACTION_COMMIT_OLETX_EVENTID = 41; + /// The event ID for the transaction rollback event. + private const int TRANSACTION_ROLLBACK_OLETX_EVENTID = 42; + /// The event ID for exception consumed event. + private const int EXCEPTION_CONSUMED_OLETX_EVENTID = 43; + /// The event ID for transaction committed event. + private const int TRANSACTION_COMMITTED_OLETX_EVENTID = 44; + /// The event ID for transaction aborted event. + private const int TRANSACTION_ABORTED_OLETX_EVENTID = 45; + /// The event ID for transaction indoubt event. + private const int TRANSACTION_INDOUBT_OLETX_EVENTID = 46; + /// The event ID for the transaction dependent clone complete event. + private const int TRANSACTION_DEPENDENT_CLONE_COMPLETE_OLETX_EVENTID = 47; + /// The event ID for the transaction dependent clone complete event. + private const int TRANSACTION_DEPENDENT_CLONE_CREATE_LTM_EVENTID = 48; + /// The event ID for the transaction dependent clone complete event. + private const int TRANSACTION_DEPENDENT_CLONE_CREATE_OLETX_EVENTID = 49; + /// The event ID for the transaction deserialized event. + private const int TRANSACTION_DESERIALIZED_EVENTID = 50; + /// The event ID for when we encounter a new Transactions object that hasn't had its name traced to the trace file. + private const int TRANSACTION_CREATED_OLETX_EVENTID = 11; + + /// The event ID for the enlistment status. + private const int ENLISTMENT_OLETX_EVENTID = 52; + /// The event ID for the enlistment callback positive event. + private const int ENLISTMENT_CALLBACK_POSITIVE_EVENTID = 53; + /// The event ID for the enlistment callback positive event. + private const int ENLISTMENT_CALLBACK_NEGATIVE_EVENTID = 54; + /// The event ID for when we create an enlistment. + private const int ENLISTMENT_CREATED_LTM_EVENTID = 55; + /// The event ID for when we create an enlistment. + private const int ENLISTMENT_CREATED_OLETX_EVENTID = 56; + + /// The event ID for transactionmanager reenlist event. + private const int TRANSACTIONMANAGER_CREATE_OLETX_EVENTID = 57; + //----------------------------------------------------------------------------------- // // Transactions Events @@ -183,28 +231,34 @@ private TransactionsEtwProvider() { } public static int GetHashCode(object? value) => value?.GetHashCode() ?? 0; #region Transaction Creation - /// Trace an event when a new transaction is created. - /// The transaction that was created. - /// The type of transaction.Method [NonEvent] - internal void TransactionCreated(Transaction transaction, string? type) + internal void TransactionCreated(TraceSourceType traceSource, TransactionTraceIdentifier txTraceId, string? type) { - Debug.Assert(transaction != null, "Transaction needed for the ETW event."); - if (IsEnabled(EventLevel.Informational, ALL_KEYWORDS)) { - if (transaction != null && transaction.TransactionTraceId.TransactionIdentifier != null) - TransactionCreated(transaction.TransactionTraceId.TransactionIdentifier, type); - else - TransactionCreated(string.Empty, type); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionCreatedLtm(txTraceId.TransactionIdentifier, type); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionCreatedOleTx(txTraceId.TransactionIdentifier, type); + } } } - [Event(TRANSACTION_CREATED_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.Create, Message = "Transaction Created. ID is {0}, type is {1}")] - private void TransactionCreated(string transactionIdentifier, string? type) + [Event(TRANSACTION_CREATED_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.Create, Message = "Transaction Created (LTM). ID is {0}, type is {1}")] + private void TransactionCreatedLtm(string transactionIdentifier, string? type) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_CREATED_LTM_EVENTID, transactionIdentifier, type); + } + + [Event(TRANSACTION_CREATED_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.Create, Message = "Transaction Created (OLETX). ID is {0}, type is {1}")] + private void TransactionCreatedOleTx(string transactionIdentifier, string? type) { SetActivityId(transactionIdentifier); - WriteEvent(TRANSACTION_CREATED_EVENTID, transactionIdentifier, type); + WriteEvent(TRANSACTION_CREATED_OLETX_EVENTID, transactionIdentifier, type); } #endregion @@ -235,28 +289,38 @@ private void TransactionCloneCreate(string transactionIdentifier, string type) #endregion #region Transaction Serialized - /// Trace an event when a transaction is serialized. - /// The transaction that was serialized. - /// The type of transaction. [NonEvent] - internal void TransactionSerialized(Transaction transaction, string type) + internal void TransactionSerialized(TransactionTraceIdentifier transactionTraceId) { - Debug.Assert(transaction != null, "Transaction needed for the ETW event."); - if (IsEnabled(EventLevel.Informational, ALL_KEYWORDS)) { - if (transaction != null && transaction.TransactionTraceId.TransactionIdentifier != null) - TransactionSerialized(transaction.TransactionTraceId.TransactionIdentifier, type); - else - TransactionSerialized(string.Empty, type); + TransactionSerialized(transactionTraceId.TransactionIdentifier); + } + } + + [Event(TRANSACTION_SERIALIZED_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.Serialized, Message = "Transaction Serialized. ID is {0}")] + private void TransactionSerialized(string transactionIdentifier) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_SERIALIZED_EVENTID, transactionIdentifier); + } + #endregion + + #region Transaction Deserialized + [NonEvent] + internal void TransactionDeserialized(TransactionTraceIdentifier transactionTraceId) + { + if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) + { + TransactionDeserialized(transactionTraceId.TransactionIdentifier); } } - [Event(TRANSACTION_SERIALIZED_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.Serialized, Message = "Transaction Serialized. ID is {0}, type is {1}")] - private void TransactionSerialized(string transactionIdentifier, string type) + [Event(TRANSACTION_DESERIALIZED_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Serialized, Message = "Transaction Deserialized. ID is {0}")] + private void TransactionDeserialized(string transactionIdentifier) { SetActivityId(transactionIdentifier); - WriteEvent(TRANSACTION_SERIALIZED_EVENTID, transactionIdentifier, type); + WriteEvent(TRANSACTION_DESERIALIZED_EVENTID, transactionIdentifier); } #endregion @@ -332,106 +396,196 @@ private void TransactionInvalidOperation(string? transactionIdentifier, string? #endregion #region Transaction Rollback - /// Trace an event when rollback on a transaction. - /// The transaction to rollback. - /// The type of transaction. [NonEvent] - internal void TransactionRollback(Transaction transaction, string? type) + internal void TransactionRollback(TraceSourceType traceSource, TransactionTraceIdentifier txTraceId, string? type) { - Debug.Assert(transaction != null, "Transaction needed for the ETW event."); - if (IsEnabled(EventLevel.Warning, ALL_KEYWORDS)) { - if (transaction != null && transaction.TransactionTraceId.TransactionIdentifier != null) - TransactionRollback(transaction.TransactionTraceId.TransactionIdentifier, type); - else - TransactionRollback(string.Empty, type); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionRollbackLtm(txTraceId.TransactionIdentifier, type); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionRollbackOleTx(txTraceId.TransactionIdentifier, type); + } } } - [Event(TRANSACTION_ROLLBACK_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Rollback, Message = "Transaction Rollback. ID is {0}, type is {1}")] - private void TransactionRollback(string transactionIdentifier, string? type) + [Event(TRANSACTION_ROLLBACK_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Rollback, Message = "Transaction LTM Rollback. ID is {0}, type is {1}")] + private void TransactionRollbackLtm(string transactionIdentifier, string? type) { SetActivityId(transactionIdentifier); - WriteEvent(TRANSACTION_ROLLBACK_EVENTID, transactionIdentifier, type); + WriteEvent(TRANSACTION_ROLLBACK_LTM_EVENTID, transactionIdentifier, type); + } + + [Event(TRANSACTION_ROLLBACK_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Rollback, Message = "Transaction OleTx Rollback. ID is {0}, type is {1}")] + private void TransactionRollbackOleTx(string transactionIdentifier, string? type) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_ROLLBACK_OLETX_EVENTID, transactionIdentifier, type); } #endregion - #region Transaction Dependent Clone Complete - /// Trace an event when transaction dependent clone complete. - /// The transaction that do dependent clone. - /// The type of transaction. + #region Transaction Dependent Clone Create [NonEvent] - internal void TransactionDependentCloneComplete(Transaction transaction, string? type) + internal void TransactionDependentCloneCreate(TraceSourceType traceSource, TransactionTraceIdentifier txTraceId, DependentCloneOption option) { - Debug.Assert(transaction != null, "Transaction needed for the ETW event."); + if (IsEnabled(EventLevel.Informational, ALL_KEYWORDS)) + { + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionDependentCloneCreateLtm(txTraceId.TransactionIdentifier, option.ToString()); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionDependentCloneCreateOleTx(txTraceId.TransactionIdentifier, option.ToString()); + } + } + } + + [Event(TRANSACTION_DEPENDENT_CLONE_CREATE_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.DependentCloneComplete, Message = "Transaction Dependent Clone Created (LTM). ID is {0}, option is {1}")] + private void TransactionDependentCloneCreateLtm(string transactionIdentifier, string? option) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_DEPENDENT_CLONE_CREATE_LTM_EVENTID, transactionIdentifier, option); + } + + [Event(TRANSACTION_DEPENDENT_CLONE_CREATE_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.DependentCloneComplete, Message = "Transaction Dependent Clone Created (OLETX). ID is {0}, option is {1}")] + private void TransactionDependentCloneCreateOleTx(string transactionIdentifier, string? option) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_DEPENDENT_CLONE_CREATE_OLETX_EVENTID, transactionIdentifier, option); + } + #endregion + #region Transaction Dependent Clone Complete + [NonEvent] + internal void TransactionDependentCloneComplete(TraceSourceType traceSource, TransactionTraceIdentifier txTraceId, string? type) + { if (IsEnabled(EventLevel.Informational, ALL_KEYWORDS)) { - if (transaction != null && transaction.TransactionTraceId.TransactionIdentifier != null) - TransactionDependentCloneComplete(transaction.TransactionTraceId.TransactionIdentifier, type); - else - TransactionDependentCloneComplete(string.Empty, type); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionDependentCloneCompleteLtm(txTraceId.TransactionIdentifier, type); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionDependentCloneCompleteOleTx(txTraceId.TransactionIdentifier, type); + } } } - [Event(TRANSACTION_DEPENDENT_CLONE_COMPLETE_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.DependentCloneComplete, Message = "Transaction Dependent Clone Completed. ID is {0}, type is {1}")] - private void TransactionDependentCloneComplete(string transactionIdentifier, string? type) + [Event(TRANSACTION_DEPENDENT_CLONE_COMPLETE_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.DependentCloneComplete, Message = "Transaction Dependent Clone Completed (LTM). ID is {0}, type is {1}")] + private void TransactionDependentCloneCompleteLtm(string transactionIdentifier, string? type) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_DEPENDENT_CLONE_COMPLETE_LTM_EVENTID, transactionIdentifier, type); + } + + [Event(TRANSACTION_DEPENDENT_CLONE_COMPLETE_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Informational, Task = Tasks.Transaction, Opcode = Opcodes.DependentCloneComplete, Message = "Transaction Dependent Clone Completed (OLETX). ID is {0}, type is {1}")] + private void TransactionDependentCloneCompleteOleTx(string transactionIdentifier, string? type) { SetActivityId(transactionIdentifier); - WriteEvent(TRANSACTION_DEPENDENT_CLONE_COMPLETE_EVENTID, transactionIdentifier, type); + WriteEvent(TRANSACTION_DEPENDENT_CLONE_COMPLETE_OLETX_EVENTID, transactionIdentifier, type); } #endregion #region Transaction Commit - /// Trace an event when there is commit on that transaction. - /// The transaction to commit. - /// The type of transaction. [NonEvent] - internal void TransactionCommit(Transaction transaction, string? type) + internal void TransactionCommit(TraceSourceType traceSource, TransactionTraceIdentifier txTraceId, string? type) { - Debug.Assert(transaction != null, "Transaction needed for the ETW event."); - if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) { - if (transaction != null && transaction.TransactionTraceId.TransactionIdentifier != null) - TransactionCommit(transaction.TransactionTraceId.TransactionIdentifier, type); - else - TransactionCommit(string.Empty, type); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionCommitLtm(txTraceId.TransactionIdentifier, type); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionCommitOleTx(txTraceId.TransactionIdentifier, type); + } } } - [Event(TRANSACTION_COMMIT_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Commit, Message = "Transaction Commit: ID is {0}, type is {1}")] - private void TransactionCommit(string transactionIdentifier, string? type) + [Event(TRANSACTION_COMMIT_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Commit, Message = "Transaction LTM Commit: ID is {0}, type is {1}")] + private void TransactionCommitLtm(string transactionIdentifier, string? type) + { + SetActivityId(transactionIdentifier); + WriteEvent(TRANSACTION_COMMIT_LTM_EVENTID, transactionIdentifier, type); + } + + [Event(TRANSACTION_COMMIT_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Commit, Message = "Transaction OleTx Commit: ID is {0}, type is {1}")] + private void TransactionCommitOleTx(string transactionIdentifier, string? type) { SetActivityId(transactionIdentifier); - WriteEvent(TRANSACTION_COMMIT_EVENTID, transactionIdentifier, type); + WriteEvent(TRANSACTION_COMMIT_OLETX_EVENTID, transactionIdentifier, type); } #endregion #region Enlistment - /// Trace an event for enlistment status. - /// The enlistment to report status. - /// The notification call on the enlistment. [NonEvent] - internal void EnlistmentStatus(InternalEnlistment enlistment, NotificationCall notificationCall) + internal void EnlistmentStatus(TraceSourceType traceSource, EnlistmentTraceIdentifier enlistmentTraceId, NotificationCall notificationCall) { - Debug.Assert(enlistment != null, "Enlistment needed for the ETW event."); - if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) { - if (enlistment != null && enlistment.EnlistmentTraceId.EnlistmentIdentifier != 0) - EnlistmentStatus(enlistment.EnlistmentTraceId.EnlistmentIdentifier, notificationCall.ToString()); - else - EnlistmentStatus(0, notificationCall.ToString()); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + EnlistmentStatusLtm(enlistmentTraceId.EnlistmentIdentifier, notificationCall.ToString()); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + EnlistmentStatusOleTx(enlistmentTraceId.EnlistmentIdentifier, notificationCall.ToString()); + } + } + } + + [Event(ENLISTMENT_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Enlistment, Message = "Enlistment status (LTM): ID is {0}, notificationcall is {1}")] + private void EnlistmentStatusLtm(int enlistmentIdentifier, string notificationCall) + { + SetActivityId(string.Empty); + WriteEvent(ENLISTMENT_LTM_EVENTID, enlistmentIdentifier, notificationCall); + } + + [Event(ENLISTMENT_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Enlistment, Message = "Enlistment status (OLETX): ID is {0}, notificationcall is {1}")] + private void EnlistmentStatusOleTx(int enlistmentIdentifier, string notificationCall) + { + SetActivityId(string.Empty); + WriteEvent(ENLISTMENT_OLETX_EVENTID, enlistmentIdentifier, notificationCall); + } + #endregion + + #region Enlistment Creation + [NonEvent] + internal void EnlistmentCreated(TraceSourceType traceSource, EnlistmentTraceIdentifier enlistmentTraceId, EnlistmentType enlistmentType, EnlistmentOptions enlistmentOptions) + { + if (IsEnabled(EventLevel.Informational, ALL_KEYWORDS)) + { + if (traceSource == TraceSourceType.TraceSourceLtm) + { + EnlistmentCreatedLtm(enlistmentTraceId.EnlistmentIdentifier, enlistmentType.ToString(), enlistmentOptions.ToString()); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + EnlistmentCreatedOleTx(enlistmentTraceId.EnlistmentIdentifier, enlistmentType.ToString(), enlistmentOptions.ToString()); + } } } - [Event(ENLISTMENT_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Enlistment, Message = "Enlistment status: ID is {0}, notificationcall is {1}")] - private void EnlistmentStatus(int enlistmentIdentifier, string notificationCall) + [Event(ENLISTMENT_CREATED_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Informational, Task = Tasks.Enlistment, Opcode = Opcodes.Create, Message = "Enlistment Created (LTM). ID is {0}, type is {1}, options is {2}")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026", Justification = "Only string/int are passed")] + private void EnlistmentCreatedLtm(int enlistmentIdentifier, string enlistmentType, string enlistmentOptions) + { + SetActivityId(string.Empty); + WriteEvent(ENLISTMENT_CREATED_LTM_EVENTID, enlistmentIdentifier, enlistmentType, enlistmentOptions); + } + + [Event(ENLISTMENT_CREATED_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Informational, Task = Tasks.Enlistment, Opcode = Opcodes.Create, Message = "Enlistment Created (OLETX). ID is {0}, type is {1}, options is {2}")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026", Justification = "Only string/int are passed")] + private void EnlistmentCreatedOleTx(int enlistmentIdentifier, string enlistmentType, string enlistmentOptions) { SetActivityId(string.Empty); - WriteEvent(ENLISTMENT_EVENTID, enlistmentIdentifier, notificationCall); + WriteEvent(ENLISTMENT_CREATED_OLETX_EVENTID, enlistmentIdentifier, enlistmentType, enlistmentOptions); } #endregion @@ -586,6 +740,42 @@ private void EnlistmentInDoubt(int enlistmentIdentifier) } #endregion + #region Enlistment Callback Positive + [NonEvent] + internal void EnlistmentCallbackPositive(EnlistmentTraceIdentifier enlistmentTraceIdentifier, EnlistmentCallback callback) + { + if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) + { + EnlistmentCallbackPositive(enlistmentTraceIdentifier.EnlistmentIdentifier, callback.ToString()); + } + } + + [Event(ENLISTMENT_CALLBACK_POSITIVE_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Enlistment, Opcode = Opcodes.CallbackPositive, Message = "Enlistment callback positive: ID is {0}, callback is {1}")] + private void EnlistmentCallbackPositive(int enlistmentIdentifier, string? callback) + { + SetActivityId(string.Empty); + WriteEvent(ENLISTMENT_CALLBACK_POSITIVE_EVENTID, enlistmentIdentifier, callback); + } + #endregion + + #region Enlistment Callback Negative + [NonEvent] + internal void EnlistmentCallbackNegative(EnlistmentTraceIdentifier enlistmentTraceIdentifier, EnlistmentCallback callback) + { + if (IsEnabled(EventLevel.Warning, ALL_KEYWORDS)) + { + EnlistmentCallbackNegative(enlistmentTraceIdentifier.EnlistmentIdentifier, callback.ToString()); + } + } + + [Event(ENLISTMENT_CALLBACK_NEGATIVE_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Warning, Task = Tasks.Enlistment, Opcode = Opcodes.CallbackNegative, Message = "Enlistment callback negative: ID is {0}, callback is {1}")] + private void EnlistmentCallbackNegative(int enlistmentIdentifier, string? callback) + { + SetActivityId(string.Empty); + WriteEvent(ENLISTMENT_CALLBACK_NEGATIVE_EVENTID, enlistmentIdentifier, callback); + } + #endregion + #region Method Enter /// Trace an event when enter a method. /// trace source @@ -604,7 +794,7 @@ internal void MethodEnter(TraceSourceType traceSource, object? thisOrContextObje { MethodEnterTraceBase(IdOf(thisOrContextObject), methodname); } - else if (traceSource == TraceSourceType.TraceSourceDistributed) + else if (traceSource == TraceSourceType.TraceSourceOleTx) { MethodEnterTraceDistributed(IdOf(thisOrContextObject), methodname); } @@ -627,7 +817,7 @@ internal void MethodEnter(TraceSourceType traceSource, [CallerMemberName] string { MethodEnterTraceBase(string.Empty, methodname); } - else if (traceSource == TraceSourceType.TraceSourceDistributed) + else if (traceSource == TraceSourceType.TraceSourceOleTx) { MethodEnterTraceDistributed(string.Empty, methodname); } @@ -646,11 +836,11 @@ private void MethodEnterTraceBase(string thisOrContextObject, string? methodname SetActivityId(string.Empty); WriteEvent(METHOD_ENTER_BASE_EVENTID, thisOrContextObject, methodname); } - [Event(METHOD_ENTER_DISTRIBUTED_EVENTID, Keywords = Keywords.TraceDistributed, Level = EventLevel.Verbose, Task = Tasks.Method, Opcode = Opcodes.Enter, Message = "Enter method : {0}.{1}")] + [Event(METHOD_ENTER_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Method, Opcode = Opcodes.Enter, Message = "Enter method : {0}.{1}")] private void MethodEnterTraceDistributed(string thisOrContextObject, string? methodname) { SetActivityId(string.Empty); - WriteEvent(METHOD_ENTER_DISTRIBUTED_EVENTID, thisOrContextObject, methodname); + WriteEvent(METHOD_ENTER_OLETX_EVENTID, thisOrContextObject, methodname); } #endregion @@ -672,7 +862,7 @@ internal void MethodExit(TraceSourceType traceSource, object? thisOrContextObjec { MethodExitTraceBase(IdOf(thisOrContextObject), methodname); } - else if (traceSource == TraceSourceType.TraceSourceDistributed) + else if (traceSource == TraceSourceType.TraceSourceOleTx) { MethodExitTraceDistributed(IdOf(thisOrContextObject), methodname); } @@ -695,7 +885,7 @@ internal void MethodExit(TraceSourceType traceSource, [CallerMemberName] string? { MethodExitTraceBase(string.Empty, methodname); } - else if (traceSource == TraceSourceType.TraceSourceDistributed) + else if (traceSource == TraceSourceType.TraceSourceOleTx) { MethodExitTraceDistributed(string.Empty, methodname); } @@ -714,11 +904,11 @@ private void MethodExitTraceBase(string thisOrContextObject, string? methodname) SetActivityId(string.Empty); WriteEvent(METHOD_EXIT_BASE_EVENTID, thisOrContextObject, methodname); } - [Event(METHOD_EXIT_DISTRIBUTED_EVENTID, Keywords = Keywords.TraceDistributed, Level = EventLevel.Verbose, Task = Tasks.Method, Opcode = Opcodes.Exit, Message = "Exit method: {0}.{1}")] + [Event(METHOD_EXIT_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Method, Opcode = Opcodes.Exit, Message = "Exit method: {0}.{1}")] private void MethodExitTraceDistributed(string thisOrContextObject, string? methodname) { SetActivityId(string.Empty); - WriteEvent(METHOD_EXIT_DISTRIBUTED_EVENTID, thisOrContextObject, methodname); + WriteEvent(METHOD_EXIT_OLETX_EVENTID, thisOrContextObject, methodname); } #endregion @@ -732,13 +922,17 @@ internal void ExceptionConsumed(TraceSourceType traceSource, Exception exception { if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) { - if (traceSource == TraceSourceType.TraceSourceBase) - { - ExceptionConsumedBase(exception.ToString()); - } - else + switch (traceSource) { - ExceptionConsumedLtm(exception.ToString()); + case TraceSourceType.TraceSourceBase: + ExceptionConsumedBase(exception.ToString()); + return; + case TraceSourceType.TraceSourceLtm: + ExceptionConsumedLtm(exception.ToString()); + return; + case TraceSourceType.TraceSourceOleTx: + ExceptionConsumedOleTx(exception.ToString()); + return; } } } @@ -765,6 +959,30 @@ private void ExceptionConsumedLtm(string exceptionStr) SetActivityId(string.Empty); WriteEvent(EXCEPTION_CONSUMED_LTM_EVENTID, exceptionStr); } + [Event(EXCEPTION_CONSUMED_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Opcode = Opcodes.ExceptionConsumed, Message = "Exception consumed: {0}")] + private void ExceptionConsumedOleTx(string exceptionStr) + { + SetActivityId(string.Empty); + WriteEvent(EXCEPTION_CONSUMED_OLETX_EVENTID, exceptionStr); + } + #endregion + + #region OleTx TransactionManager Create + [NonEvent] + internal void OleTxTransactionManagerCreate(Type tmType, string? nodeName) + { + if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) + { + OleTxTransactionManagerCreate(tmType.ToString(), nodeName); + } + } + + [Event(TRANSACTIONMANAGER_CREATE_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.TransactionManager, Opcode = Opcodes.Created, Message = "Created OleTx transaction manager, type is {0}, node name is {1}")] + private void OleTxTransactionManagerCreate(string tmType, string? nodeName) + { + SetActivityId(string.Empty); + WriteEvent(TRANSACTIONMANAGER_CREATE_OLETX_EVENTID, tmType, nodeName); + } #endregion #region TransactionManager Reenlist @@ -940,26 +1158,6 @@ private void TransactionScopeIncomplete(string transactionID) } #endregion - #region Transactionscope Internal Error - /// Trace an event when there is an internal error on transactionscope. - /// The error information. - [NonEvent] - internal void TransactionScopeInternalError(string? error) - { - if (IsEnabled(EventLevel.Critical, ALL_KEYWORDS)) - { - TransactionScopeInternalErrorTrace(error); - } - } - - [Event(TRANSACTIONSCOPE_INTERNAL_ERROR_EVENTID, Keywords = Keywords.TraceBase, Level = EventLevel.Critical, Task = Tasks.TransactionScope, Opcode = Opcodes.InternalError, Message = "Transactionscope internal error: {0}")] - private void TransactionScopeInternalErrorTrace(string? error) - { - SetActivityId(string.Empty); - WriteEvent(TRANSACTIONSCOPE_INTERNAL_ERROR_EVENTID, error); - } - #endregion - #region Transactionscope Timeout /// Trace an event when there is timeout on transactionscope. /// The transaction ID. @@ -1000,7 +1198,7 @@ private void TransactionTimeout(string transactionID) } #endregion - #region Transactionstate Enlist + #region Transaction Enlist /// Trace an event when there is enlist. /// The enlistment ID. /// The enlistment type. @@ -1025,43 +1223,65 @@ private void TransactionstateEnlist(string enlistmentID, string type, string opt } #endregion - #region Transactionstate committed - /// Trace an event when transaction is committed. - /// The transaction ID. + #region Transaction committed [NonEvent] - internal void TransactionCommitted(TransactionTraceIdentifier transactionID) + internal void TransactionCommitted(TraceSourceType traceSource, TransactionTraceIdentifier transactionID) { if (IsEnabled(EventLevel.Verbose, ALL_KEYWORDS)) { - TransactionCommitted(transactionID.TransactionIdentifier ?? string.Empty); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionCommittedLtm(transactionID.TransactionIdentifier ?? string.Empty); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionCommittedOleTx(transactionID.TransactionIdentifier ?? string.Empty); + } } } - [Event(TRANSACTION_COMMITTED_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Committed, Message = "Transaction committed: transaction ID is {0}")] - private void TransactionCommitted(string transactionID) + [Event(TRANSACTION_COMMITTED_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Committed, Message = "Transaction committed LTM: transaction ID is {0}")] + private void TransactionCommittedLtm(string transactionID) { SetActivityId(transactionID); - WriteEvent(TRANSACTION_COMMITTED_EVENTID, transactionID); + WriteEvent(TRANSACTION_COMMITTED_LTM_EVENTID, transactionID); + } + [Event(TRANSACTION_COMMITTED_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Verbose, Task = Tasks.Transaction, Opcode = Opcodes.Committed, Message = "Transaction committed OleTx: transaction ID is {0}")] + private void TransactionCommittedOleTx(string transactionID) + { + SetActivityId(transactionID); + WriteEvent(TRANSACTION_COMMITTED_OLETX_EVENTID, transactionID); } #endregion - #region Transactionstate indoubt - /// Trace an event when transaction is indoubt. - /// The transaction ID. + #region Transaction indoubt [NonEvent] - internal void TransactionInDoubt(TransactionTraceIdentifier transactionID) + internal void TransactionInDoubt(TraceSourceType traceSource, TransactionTraceIdentifier transactionID) { if (IsEnabled(EventLevel.Warning, ALL_KEYWORDS)) { - TransactionInDoubt(transactionID.TransactionIdentifier ?? string.Empty); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionInDoubtLtm(transactionID.TransactionIdentifier ?? string.Empty); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionInDoubtOleTx(transactionID.TransactionIdentifier ?? string.Empty); + } } } - [Event(TRANSACTION_INDOUBT_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.InDoubt, Message = "Transaction indoubt: transaction ID is {0}")] - private void TransactionInDoubt(string transactionID) + [Event(TRANSACTION_INDOUBT_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.InDoubt, Message = "Transaction indoubt LTM: transaction ID is {0}")] + private void TransactionInDoubtLtm(string transactionID) { SetActivityId(transactionID); - WriteEvent(TRANSACTION_INDOUBT_EVENTID, transactionID); + WriteEvent(TRANSACTION_INDOUBT_LTM_EVENTID, transactionID); + } + [Event(TRANSACTION_INDOUBT_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.InDoubt, Message = "Transaction indoubt OleTx: transaction ID is {0}")] + private void TransactionInDoubtOleTx(string transactionID) + { + SetActivityId(transactionID); + WriteEvent(TRANSACTION_INDOUBT_OLETX_EVENTID, transactionID); } #endregion @@ -1086,25 +1306,57 @@ private void TransactionPromoted(string transactionID, string distributedTxID) } #endregion - #region Transactionstate aborted - /// Trace an event when transaction is aborted. - /// The transaction ID. + #region Transaction aborted [NonEvent] - internal void TransactionAborted(TransactionTraceIdentifier transactionID) + internal void TransactionAborted(TraceSourceType traceSource, TransactionTraceIdentifier transactionID) { if (IsEnabled(EventLevel.Warning, ALL_KEYWORDS)) { - TransactionAborted(transactionID.TransactionIdentifier ?? string.Empty); + if (traceSource == TraceSourceType.TraceSourceLtm) + { + TransactionAbortedLtm(transactionID.TransactionIdentifier ?? string.Empty); + } + else if (traceSource == TraceSourceType.TraceSourceOleTx) + { + TransactionAbortedOleTx(transactionID.TransactionIdentifier ?? string.Empty); + } } } - [Event(TRANSACTION_ABORTED_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Aborted, Message = "Transaction aborted: transaction ID is {0}")] - private void TransactionAborted(string transactionID) + [Event(TRANSACTION_ABORTED_LTM_EVENTID, Keywords = Keywords.TraceLtm, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Aborted, Message = "Transaction aborted LTM: transaction ID is {0}")] + private void TransactionAbortedLtm(string transactionID) + { + SetActivityId(transactionID); + WriteEvent(TRANSACTION_ABORTED_LTM_EVENTID, transactionID); + } + [Event(TRANSACTION_ABORTED_OLETX_EVENTID, Keywords = Keywords.TraceOleTx, Level = EventLevel.Warning, Task = Tasks.Transaction, Opcode = Opcodes.Aborted, Message = "Transaction aborted OleTx: transaction ID is {0}")] + private void TransactionAbortedOleTx(string transactionID) { SetActivityId(transactionID); - WriteEvent(TRANSACTION_ABORTED_EVENTID, transactionID); + WriteEvent(TRANSACTION_ABORTED_OLETX_EVENTID, transactionID); } #endregion + + #region Internal Error + /// Trace an event when there is an internal error. + /// The error information. + [NonEvent] + internal void InternalError(string? error = null) + { + if (IsEnabled(EventLevel.Critical, ALL_KEYWORDS)) + { + InternalErrorTrace(error); + } + } + + [Event(INTERNAL_ERROR_EVENTID, Keywords = Keywords.TraceBase, Level = EventLevel.Critical, Task = Tasks.TransactionScope, Opcode = Opcodes.InternalError, Message = "Transactionscope internal error: {0}")] + private void InternalErrorTrace(string? error) + { + SetActivityId(string.Empty); + WriteEvent(INTERNAL_ERROR_EVENTID, error); + } + #endregion + public static class Opcodes { public const EventOpcode Aborted = (EventOpcode)100; @@ -1136,6 +1388,8 @@ public static class Opcodes public const EventOpcode Rollback = (EventOpcode)126; public const EventOpcode Serialized = (EventOpcode)127; public const EventOpcode Timeout = (EventOpcode)128; + public const EventOpcode CallbackPositive = (EventOpcode)129; + public const EventOpcode CallbackNegative = (EventOpcode)130; } public static class Tasks @@ -1155,7 +1409,7 @@ public static class Keywords { public const EventKeywords TraceBase = (EventKeywords)0x0001; public const EventKeywords TraceLtm = (EventKeywords)0x0002; - public const EventKeywords TraceDistributed = (EventKeywords)0x0004; + public const EventKeywords TraceOleTx = (EventKeywords)0x0004; } private static void SetActivityId(string str) diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/VolatileEnlistmentState.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/VolatileEnlistmentState.cs index 360dc3e3a80e7..a0bf1387b3ab5 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/VolatileEnlistmentState.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/VolatileEnlistmentState.cs @@ -141,7 +141,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.Prepare); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.Prepare); } Debug.Assert(enlistment.EnlistmentNotification != null); @@ -213,7 +213,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.SinglePhaseCommit); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.SinglePhaseCommit); } Monitor.Exit(enlistment.Transaction); @@ -366,7 +366,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.Rollback); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.Rollback); } Debug.Assert(enlistment.EnlistmentNotification != null); @@ -409,7 +409,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.Commit); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.Commit); } Debug.Assert(enlistment.EnlistmentNotification != null); @@ -443,7 +443,7 @@ internal override void EnterState(InternalEnlistment enlistment) TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log; if (etwLog.IsEnabled()) { - etwLog.EnlistmentStatus(enlistment, NotificationCall.InDoubt); + etwLog.EnlistmentStatus(TraceSourceType.TraceSourceLtm, enlistment.EnlistmentTraceId, NotificationCall.InDoubt); } Debug.Assert(enlistment.EnlistmentNotification != null); diff --git a/src/libraries/System.Transactions.Local/tests/LTMEnlistmentTests.cs b/src/libraries/System.Transactions.Local/tests/LTMEnlistmentTests.cs index a5fd3047fa64a..c686049d742b5 100644 --- a/src/libraries/System.Transactions.Local/tests/LTMEnlistmentTests.cs +++ b/src/libraries/System.Transactions.Local/tests/LTMEnlistmentTests.cs @@ -36,83 +36,28 @@ public void SinglePhaseDurable(int volatileCount, EnlistmentOptions volatileEnli Transaction tx = null; try { - using (TransactionScope ts = new TransactionScope()) - { - tx = Transaction.Current.Clone(); - - if (volatileCount > 0) - { - TestSinglePhaseEnlistment[] volatiles = new TestSinglePhaseEnlistment[volatileCount]; - for (int i = 0; i < volatileCount; i++) - { - // It doesn't matter what we specify for SinglePhaseVote. - volatiles[i] = new TestSinglePhaseEnlistment(volatilePhase1Vote, SinglePhaseVote.InDoubt, expectedVolatileOutcome); - tx.EnlistVolatile(volatiles[i], volatileEnlistmentOption); - } - } + using var ts = new TransactionScope(); - // Doesn't really matter what we specify for EnlistmentOutcome here. This is an SPC, so Phase2 won't happen for this enlistment. - TestSinglePhaseEnlistment durable = new TestSinglePhaseEnlistment(Phase1Vote.Prepared, singlePhaseVote, EnlistmentOutcome.Committed); - tx.EnlistDurable(Guid.NewGuid(), durable, EnlistmentOptions.None); + tx = Transaction.Current!.Clone(); - if (commit) + if (volatileCount > 0) + { + TestSinglePhaseEnlistment[] volatiles = new TestSinglePhaseEnlistment[volatileCount]; + for (int i = 0; i < volatileCount; i++) { - ts.Complete(); + // It doesn't matter what we specify for SinglePhaseVote. + volatiles[i] = new TestSinglePhaseEnlistment(volatilePhase1Vote, SinglePhaseVote.InDoubt, expectedVolatileOutcome); + tx.EnlistVolatile(volatiles[i], volatileEnlistmentOption); } } - } - catch (TransactionInDoubtException) - { - Assert.Equal(TransactionStatus.InDoubt, expectedTxStatus); - } - catch (TransactionAbortedException) - { - Assert.Equal(TransactionStatus.Aborted, expectedTxStatus); - } - - Assert.NotNull(tx); - Assert.Equal(expectedTxStatus, tx.TransactionInformation.Status); - } + // Doesn't really matter what we specify for EnlistmentOutcome here. This is an SPC, so Phase2 won't happen for this enlistment. + TestSinglePhaseEnlistment durable = new TestSinglePhaseEnlistment(Phase1Vote.Prepared, singlePhaseVote, EnlistmentOutcome.Committed); + tx.EnlistDurable(Guid.NewGuid(), durable, EnlistmentOptions.None); - [Theory] - // This test needs to change once we have promotion support. - // Right now any attempt to create a two phase durable enlistment will attempt to promote and will fail because promotion is not supported. This results in the transaction being - // aborted. - [InlineData(0, EnlistmentOptions.None, EnlistmentOptions.None, Phase1Vote.Prepared, true, EnlistmentOutcome.Aborted, EnlistmentOutcome.Aborted, TransactionStatus.Aborted)] - [InlineData(1, EnlistmentOptions.None, EnlistmentOptions.None, Phase1Vote.Prepared, true, EnlistmentOutcome.Aborted, EnlistmentOutcome.Aborted, TransactionStatus.Aborted)] - [InlineData(2, EnlistmentOptions.None, EnlistmentOptions.None, Phase1Vote.Prepared, true, EnlistmentOutcome.Aborted, EnlistmentOutcome.Aborted, TransactionStatus.Aborted)] - public void TwoPhaseDurable(int volatileCount, EnlistmentOptions volatileEnlistmentOption, EnlistmentOptions durableEnlistmentOption, Phase1Vote volatilePhase1Vote, bool commit, EnlistmentOutcome expectedVolatileOutcome, EnlistmentOutcome expectedDurableOutcome, TransactionStatus expectedTxStatus) - { - Transaction tx = null; - try - { - using (TransactionScope ts = new TransactionScope()) + if (commit) { - tx = Transaction.Current.Clone(); - - if (volatileCount > 0) - { - TestEnlistment[] volatiles = new TestEnlistment[volatileCount]; - for (int i = 0; i < volatileCount; i++) - { - // It doesn't matter what we specify for SinglePhaseVote. - volatiles[i] = new TestEnlistment(volatilePhase1Vote, expectedVolatileOutcome); - tx.EnlistVolatile(volatiles[i], volatileEnlistmentOption); - } - } - - TestEnlistment durable = new TestEnlistment(Phase1Vote.Prepared, expectedDurableOutcome); - // This needs to change once we have promotion support. - Assert.Throws(() => // Creation of two phase durable enlistment attempts to promote to MSDTC - { - tx.EnlistDurable(Guid.NewGuid(), durable, durableEnlistmentOption); - }); - - if (commit) - { - ts.Complete(); - } + ts.Complete(); } } catch (TransactionInDoubtException) @@ -137,17 +82,16 @@ public void EnlistDuringPhase0(EnlistmentOptions enlistmentOption, Phase1Vote ph AutoResetEvent outcomeEvent = null; try { - using (TransactionScope ts = new TransactionScope()) - { - tx = Transaction.Current.Clone(); - outcomeEvent = new AutoResetEvent(false); - TestEnlistment enlistment = new TestEnlistment(phase1Vote, expectedOutcome, true, expectPhase0EnlistSuccess, outcomeEvent); - tx.EnlistVolatile(enlistment, enlistmentOption); + using var ts = new TransactionScope(); - if (commit) - { - ts.Complete(); - } + tx = Transaction.Current!.Clone(); + outcomeEvent = new AutoResetEvent(false); + var enlistment = new TestEnlistment(phase1Vote, expectedOutcome, true, expectPhase0EnlistSuccess, outcomeEvent); + tx.EnlistVolatile(enlistment, enlistmentOption); + + if (commit) + { + ts.Complete(); } } catch (TransactionInDoubtException) @@ -173,30 +117,29 @@ public void EnlistVolatile(int volatileCount, EnlistmentOptions enlistmentOption Transaction tx = null; try { - using (TransactionScope ts = new TransactionScope()) - { - tx = Transaction.Current.Clone(); + using var ts = new TransactionScope(); - if (volatileCount > 0) - { - TestEnlistment[] volatiles = new TestEnlistment[volatileCount]; - outcomeEvents = new AutoResetEvent[volatileCount]; - for (int i = 0; i < volatileCount-1; i++) - { - outcomeEvents[i] = new AutoResetEvent(false); - volatiles[i] = new TestEnlistment(volatilePhase1Vote, expectedEnlistmentOutcome, false, true, outcomeEvents[i]); - tx.EnlistVolatile(volatiles[i], enlistmentOption); - } - - outcomeEvents[volatileCount-1] = new AutoResetEvent(false); - volatiles[volatileCount - 1] = new TestEnlistment(lastPhase1Vote, expectedEnlistmentOutcome, false, true, outcomeEvents[volatileCount-1]); - tx.EnlistVolatile(volatiles[volatileCount - 1], enlistmentOption); - } + tx = Transaction.Current!.Clone(); - if (commit) + if (volatileCount > 0) + { + TestEnlistment[] volatiles = new TestEnlistment[volatileCount]; + outcomeEvents = new AutoResetEvent[volatileCount]; + for (int i = 0; i < volatileCount-1; i++) { - ts.Complete(); + outcomeEvents[i] = new AutoResetEvent(false); + volatiles[i] = new TestEnlistment(volatilePhase1Vote, expectedEnlistmentOutcome, false, true, outcomeEvents[i]); + tx.EnlistVolatile(volatiles[i], enlistmentOption); } + + outcomeEvents[volatileCount-1] = new AutoResetEvent(false); + volatiles[volatileCount - 1] = new TestEnlistment(lastPhase1Vote, expectedEnlistmentOutcome, false, true, outcomeEvents[volatileCount-1]); + tx.EnlistVolatile(volatiles[volatileCount - 1], enlistmentOption); + } + + if (commit) + { + ts.Complete(); } } catch (TransactionInDoubtException) @@ -216,6 +159,5 @@ public void EnlistVolatile(int volatileCount, EnlistmentOptions enlistmentOption Assert.NotNull(tx); Assert.Equal(expectedTxStatus, tx.TransactionInformation.Status); } - } } diff --git a/src/libraries/System.Transactions.Local/tests/NonMsdtcPromoterTests.cs b/src/libraries/System.Transactions.Local/tests/NonMsdtcPromoterTests.cs index 747d33a280d8c..dc2ba01f63bcb 100644 --- a/src/libraries/System.Transactions.Local/tests/NonMsdtcPromoterTests.cs +++ b/src/libraries/System.Transactions.Local/tests/NonMsdtcPromoterTests.cs @@ -724,106 +724,6 @@ public void Rollback(Enlistment enlistment) } #endregion - // This class is used in conjunction with SubordinateTransaction. When asked via the Promote - // method, it needs to create a DTC transaction and return the propagation token. Since we - // can't just create another CommittableTransaction and promote it and return it's propagation - // token in the same AppDomain, we spin up another AppDomain and do it there. - private class MySimpleTransactionSuperior : ISimpleTransactionSuperior - { - private DtcTxCreator _dtcTxCreator = new DtcTxCreator() { TraceEnabled = false }; - private PromotedTx _promotedTx; - - public byte[] Promote() - { - byte[] propagationToken = null; - - Trace("MySimpleTransactionSuperior.Promote"); - propagationToken = _dtcTxCreator.CreatePromotedTx(ref _promotedTx); - - return propagationToken; - } - - public void Rollback() - { - Trace("MySimpleTransactionSuperior.Rollback"); - _promotedTx.Rollback(); - } - - public void Commit() - { - Trace("MySimpleTransactionSuperior.Commit"); - _promotedTx.Commit(); - } - } - - public class DtcTxCreator // : MarshalByRefObject - { - private static bool s_trace = false; - - public bool TraceEnabled - { - get { return s_trace; } - set { s_trace = value; } - } - public static void Trace(string stringToTrace, params object[] args) - { - if (s_trace) - { - Debug.WriteLine(stringToTrace, args); - } - } - - public byte[] CreatePromotedTx(ref PromotedTx promotedTx) - { - DtcTxCreator.Trace("DtcTxCreator.CreatePromotedTx"); - byte[] propagationToken; - CommittableTransaction commitTx = new CommittableTransaction(); - promotedTx = new PromotedTx(commitTx); - propagationToken = TransactionInterop.GetTransmitterPropagationToken(commitTx); - return propagationToken; - } - } - - // This is the class that is created in the "other" AppDomain to create a - // CommittableTransaction, promote it to DTC, and return the propagation token. - // It also commits or aborts the transaction. Used by MySimpleTransactionSuperior - // to create a DTC transaction when asked to promote. - public class PromotedTx // : MarshalByRefObject - { - private CommittableTransaction _commitTx; - - public PromotedTx(CommittableTransaction commitTx) - { - DtcTxCreator.Trace("PromotedTx constructor"); - _commitTx = commitTx; - } - - ~PromotedTx() - { - DtcTxCreator.Trace("PromotedTx destructor"); - if (_commitTx != null) - { - DtcTxCreator.Trace("PromotedTx destructor calling Rollback"); - _commitTx.Rollback(); - _commitTx = null; - } - } - - public void Commit() - { - DtcTxCreator.Trace("PromotedTx.Commit"); - _commitTx.Commit(); - _commitTx = null; - } - - public void Rollback() - { - DtcTxCreator.Trace("PromotedTx.Rollback"); - _commitTx.Rollback(); - _commitTx = null; - } - } - #region TestCase_ methods private static void TestCase_VolatileEnlistments( int count, @@ -2062,7 +1962,6 @@ private static void TestCase_SetDistributedIdWithWrongNotificationObject() #endregion - /// /// This test case is very basic Volatile Enlistment test. /// @@ -2273,29 +2172,5 @@ public void PSPENonMsdtcSetDistributedTransactionIdentifierCallWithWrongNotifica // Call SetDistributedTransactionIdentifier at the wrong time. TestCase_SetDistributedIdWithWrongNotificationObject(); } - - [Fact] - public void SimpleTransactionSuperior() - { - MySimpleTransactionSuperior superior = new MySimpleTransactionSuperior(); - SubordinateTransaction subTx = new SubordinateTransaction(IsolationLevel.Serializable, superior); - - AutoResetEvent durableCompleted = new AutoResetEvent(false); - MyEnlistment durable = null; - - durable = new MyEnlistment( - durableCompleted, - true, - false, - EnlistmentOptions.None, - /*expectSuccessfulEnlist=*/ false, - /*secondEnlistmentCompleted=*/ null); - durable.TransactionToEnlist = Transaction.Current; - - Assert.Throws(() => // SubordinateTransaction promotes to MSDTC - { - subTx.EnlistDurable(Guid.NewGuid(), durable, EnlistmentOptions.None); - }); - } } } diff --git a/src/libraries/System.Transactions.Local/tests/OleTxNonWindowsUnsupportedTests.cs b/src/libraries/System.Transactions.Local/tests/OleTxNonWindowsUnsupportedTests.cs new file mode 100644 index 0000000000000..fc753cef0e2d0 --- /dev/null +++ b/src/libraries/System.Transactions.Local/tests/OleTxNonWindowsUnsupportedTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Transactions.Tests; + +#nullable enable + +[SkipOnPlatform(TestPlatforms.Windows, "These tests assert that OleTx operations properly throw PlatformNotSupportedException on non-Windows platforms")] +public class OleTxNonWindowsUnsupportedTests +{ + [Fact] + public void Durable_enlistment() + { + var tx = new CommittableTransaction(); + + // Votes and outcomes don't matter, the 2nd enlistment fails in non-Windows + var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); + + Assert.Throws(() => tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None)); + Assert.Equal(TransactionStatus.Aborted, tx.TransactionInformation.Status); + } + + [Fact] + public void Promotable_enlistments() + { + var tx = new CommittableTransaction(); + + var promotableEnlistment1 = new TestPromotableSinglePhaseEnlistment(() => new byte[24], EnlistmentOutcome.Aborted); + var promotableEnlistment2 = new TestPromotableSinglePhaseEnlistment(null, EnlistmentOutcome.Aborted); + + // 1st promotable enlistment - no distributed transaction yet. + Assert.True(tx.EnlistPromotableSinglePhase(promotableEnlistment1)); + Assert.True(promotableEnlistment1.InitializedCalled); + + // 2nd promotable enlistment returns false. + tx.EnlistPromotableSinglePhase(promotableEnlistment2); + Assert.False(promotableEnlistment2.InitializedCalled); + + // Now enlist a durable enlistment, this will cause the escalation to a distributed transaction and fail on non-Windows. + var durableEnlistment = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); + Assert.Throws(() => tx.EnlistDurable(Guid.NewGuid(), durableEnlistment, EnlistmentOptions.None)); + + Assert.True(promotableEnlistment1.PromoteCalled); + Assert.False(promotableEnlistment2.PromoteCalled); + + Assert.Equal(TransactionStatus.Aborted, tx.TransactionInformation.Status); + } + + [Fact] + public void TransmitterPropagationToken() + => Assert.Throws(() => + TransactionInterop.GetTransmitterPropagationToken(new CommittableTransaction())); + + [Fact] + public void GetWhereabouts() + => Assert.Throws(() => TransactionInterop.GetWhereabouts()); + + [Fact] + public void GetExportCookie() + => Assert.Throws(() => TransactionInterop.GetExportCookie( + new CommittableTransaction(), new byte[200])); +} diff --git a/src/libraries/System.Transactions.Local/tests/OleTxTests.cs b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs new file mode 100644 index 0000000000000..fd89ef4071459 --- /dev/null +++ b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs @@ -0,0 +1,477 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; +using Xunit.Sdk; + +namespace System.Transactions.Tests; + +#nullable enable + +[PlatformSpecific(TestPlatforms.Windows)] +public class OleTxTests +{ + //private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(3); + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10); + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + [InlineData(Phase1Vote.Prepared, Phase1Vote.Prepared, EnlistmentOutcome.Committed, EnlistmentOutcome.Committed, TransactionStatus.Committed)] + [InlineData(Phase1Vote.Prepared, Phase1Vote.ForceRollback, EnlistmentOutcome.Aborted, EnlistmentOutcome.Aborted, TransactionStatus.Aborted)] + [InlineData(Phase1Vote.ForceRollback, Phase1Vote.Prepared, EnlistmentOutcome.Aborted, EnlistmentOutcome.Aborted, TransactionStatus.Aborted)] + public void Two_durable_enlistments_commit(Phase1Vote vote1, Phase1Vote vote2, EnlistmentOutcome expectedOutcome1, EnlistmentOutcome expectedOutcome2, TransactionStatus expectedTxStatus) + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + var tx = new CommittableTransaction(); + + try + { + var enlistment1 = new TestEnlistment(vote1, expectedOutcome1); + var enlistment2 = new TestEnlistment(vote2, expectedOutcome2); + + tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None); + tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None); + + Assert.Equal(TransactionStatus.Active, tx.TransactionInformation.Status); + tx.Commit(); + } + catch (TransactionInDoubtException) + { + Assert.Equal(TransactionStatus.InDoubt, expectedTxStatus); + } + catch (TransactionAbortedException) + { + Assert.Equal(TransactionStatus.Aborted, expectedTxStatus); + } + + Retry(() => Assert.Equal(expectedTxStatus, tx.TransactionInformation.Status)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void Two_durable_enlistments_rollback() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + var tx = new CommittableTransaction(); + + var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); + var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); + + tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None); + tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None); + + tx.Rollback(); + + Assert.False(enlistment1.WasPreparedCalled); + Assert.False(enlistment2.WasPreparedCalled); + + // This matches the .NET Framework behavior + Retry(() => Assert.Equal(TransactionStatus.Aborted, tx.TransactionInformation.Status)); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Volatile_and_durable_enlistments(int volatileCount) + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + var tx = new CommittableTransaction(); + + if (volatileCount > 0) + { + TestEnlistment[] volatiles = new TestEnlistment[volatileCount]; + for (int i = 0; i < volatileCount; i++) + { + // It doesn't matter what we specify for SinglePhaseVote. + volatiles[i] = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); + tx.EnlistVolatile(volatiles[i], EnlistmentOptions.None); + } + } + + var durable = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); + + // Creation of two phase durable enlistment attempts to promote to MSDTC + tx.EnlistDurable(Guid.NewGuid(), durable, EnlistmentOptions.None); + + tx.Commit(); + + Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status)); + } + + protected static bool IsRemoteExecutorSupportedAndNotNano => RemoteExecutor.IsSupported && PlatformDetection.IsNotWindowsNanoServer; + + [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))] + public void Promotion() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + // This simulates the full promotable flow, as implemented for SQL Server. + + // We are going to spin up two external processes. + // 1. The 1st external process will create the transaction and save its propagation token to disk. + // 2. The main process will read that, and propagate the transaction to the 2nd external process. + // 3. The main process will then notify the 1st external process to commit (as the main's transaction is delegated to it). + // 4. At that point the MSDTC Commit will be triggered; enlistments on both the 1st and 2nd processes will be notified + // to commit, and the transaction status will reflect the committed status in the main process. + var tx = new CommittableTransaction(); + + string propagationTokenFilePath = Path.GetTempFileName(); + string exportCookieFilePath = Path.GetTempFileName(); + using var waitHandle1 = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion1"); + using var waitHandle2 = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion2"); + using var waitHandle3 = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion3"); + + RemoteInvokeHandle? remote1 = null, remote2 = null; + + try + { + remote1 = RemoteExecutor.Invoke(Remote1, propagationTokenFilePath, new RemoteInvokeOptions { ExpectedExitCode = 42 }); + + // Wait for the external process to start a transaction and save its propagation token + Assert.True(waitHandle1.WaitOne(Timeout)); + + // Enlist the first PSPE. No escalation happens yet, since its the only enlistment. + var pspe1 = new TestPromotableSinglePhaseNotification(propagationTokenFilePath); + Assert.True(tx.EnlistPromotableSinglePhase(pspe1)); + Assert.True(pspe1.WasInitializedCalled); + Assert.False(pspe1.WasPromoteCalled); + Assert.False(pspe1.WasRollbackCalled); + Assert.False(pspe1.WasSinglePhaseCommitCalled); + + // Enlist the second PSPE. This returns false and does nothing, since there's already an enlistment. + var pspe2 = new TestPromotableSinglePhaseNotification(propagationTokenFilePath); + Assert.False(tx.EnlistPromotableSinglePhase(pspe2)); + Assert.False(pspe2.WasInitializedCalled); + Assert.False(pspe2.WasPromoteCalled); + Assert.False(pspe2.WasRollbackCalled); + Assert.False(pspe2.WasSinglePhaseCommitCalled); + + // Now generate an export cookie for the 2nd external process. This causes escalation and promotion. + byte[] whereabouts = TransactionInterop.GetWhereabouts(); + byte[] exportCookie = TransactionInterop.GetExportCookie(tx, whereabouts); + + Assert.True(pspe1.WasPromoteCalled); + Assert.False(pspe1.WasRollbackCalled); + Assert.False(pspe1.WasSinglePhaseCommitCalled); + + // Write the export cookie and start the 2nd external process, which will read the cookie and enlist in the transaction. + // Wait for it to complete. + File.WriteAllBytes(exportCookieFilePath, exportCookie); + remote2 = RemoteExecutor.Invoke(Remote2, exportCookieFilePath, new RemoteInvokeOptions { ExpectedExitCode = 42 }); + Assert.True(waitHandle2.WaitOne(Timeout)); + + // We now have two external processes with enlistments to our distributed transaction. Commit. + // Since our transaction is delegated to the 1st PSPE enlistment, Sys.Tx will call SinglePhaseCommit on it. + // In SQL Server this contacts the 1st DB to actually commit the transaction with MSDTC. In this simulation we'll just use a wait handle to trigger this. + tx.Commit(); + Assert.True(pspe1.WasSinglePhaseCommitCalled); + waitHandle3.Set(); + + Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status)); + } + catch + { + try + { + remote1?.Process.Kill(); + remote2?.Process.Kill(); + } + catch + { + } + + throw; + } + finally + { + File.Delete(propagationTokenFilePath); + } + + // Disposal of the RemoteExecutor handles will wait for the external processes to exit with the right exit code, + // which will happen when their enlistments receive the commit. + remote1?.Dispose(); + remote2?.Dispose(); + + static void Remote1(string propagationTokenFilePath) + { + var tx = new CommittableTransaction(); + + var outcomeEvent = new AutoResetEvent(false); + var enlistment = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed, outcomeReceived: outcomeEvent); + tx.EnlistDurable(Guid.NewGuid(), enlistment, EnlistmentOptions.None); + + // We now have an OleTx transaction. Save its propagation token to disk so that the main process can read it when promoting. + byte[] propagationToken = TransactionInterop.GetTransmitterPropagationToken(tx); + File.WriteAllBytes(propagationTokenFilePath, propagationToken); + + // Signal to the main process that the propagation token is ready to be read + using var waitHandle1 = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion1"); + waitHandle1.Set(); + + // The main process will now import our transaction via the propagation token, and propagate it to a 2nd process. + // In the main process the transaction is delegated; we're the one who started it, and so we're the one who need to Commit. + // When Commit() is called in the main process, that will trigger a SinglePhaseCommit on the PSPE which represents us. In SQL Server this + // contacts the DB to actually commit the transaction with MSDTC. In this simulation we'll just use the wait handle again to trigger this. + using var waitHandle3 = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion3"); + Assert.True(waitHandle3.WaitOne(Timeout)); + + tx.Commit(); + + // Wait for the commit to occur on our enlistment, then exit successfully. + Assert.True(outcomeEvent.WaitOne(Timeout)); + Environment.Exit(42); // 42 is error code expected by RemoteExecutor + } + + static void Remote2(string exportCookieFilePath) + { + // Load the export cookie and enlist durably + byte[] exportCookie = File.ReadAllBytes(exportCookieFilePath); + var tx = TransactionInterop.GetTransactionFromExportCookie(exportCookie); + + // Now enlist durably. This triggers promotion of the first PSPE, reading the propagation token. + var outcomeEvent = new AutoResetEvent(false); + var enlistment = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed, outcomeReceived: outcomeEvent); + tx.EnlistDurable(Guid.NewGuid(), enlistment, EnlistmentOptions.None); + + // Signal to the main process that we're enlisted and ready to commit + using var waitHandle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Promotion2"); + waitHandle.Set(); + + // Wait for the main process to commit the transaction + Assert.True(outcomeEvent.WaitOne(Timeout)); + Environment.Exit(42); // 42 is error code expected by RemoteExecutor + } + } + + public class TestPromotableSinglePhaseNotification : IPromotableSinglePhaseNotification + { + private string _propagationTokenFilePath; + + public TestPromotableSinglePhaseNotification(string propagationTokenFilePath) + => _propagationTokenFilePath = propagationTokenFilePath; + + public bool WasInitializedCalled { get; private set; } + public bool WasPromoteCalled { get; private set; } + public bool WasRollbackCalled { get; private set; } + public bool WasSinglePhaseCommitCalled { get; private set; } + + public void Initialize() + => WasInitializedCalled = true; + + public byte[] Promote() + { + WasPromoteCalled = true; + + return File.ReadAllBytes(_propagationTokenFilePath); + } + + public void Rollback(SinglePhaseEnlistment singlePhaseEnlistment) + => WasRollbackCalled = true; + + public void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment) + { + WasSinglePhaseCommitCalled = true; + + singlePhaseEnlistment.Committed(); + } + } + + [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))] + public void Recovery() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + // We are going to spin up an external process to also enlist in the transaction, and then to crash when it + // receives the commit notification. We will then initiate the recovery flow. + + var tx = new CommittableTransaction(); + + var outcomeEvent1 = new AutoResetEvent(false); + var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed, outcomeReceived: outcomeEvent1); + var guid1 = Guid.NewGuid(); + tx.EnlistDurable(guid1, enlistment1, EnlistmentOptions.None); + + // The propagation token is used to propagate the transaction to that process so it can enlist to our + // transaction. We also provide the resource manager identifier GUID, and a path where the external process will + // write the recovery information it will receive from the MSDTC when preparing. + // We'll need these two elements later in order to Reenlist and trigger recovery. + byte[] propagationToken = TransactionInterop.GetTransmitterPropagationToken(tx); + string propagationTokenText = Convert.ToBase64String(propagationToken); + var guid2 = Guid.NewGuid(); + string secondEnlistmentRecoveryFilePath = Path.GetTempFileName(); + + using var waitHandle = new EventWaitHandle( + initialState: false, + EventResetMode.ManualReset, + "System.Transactions.Tests.OleTxTests.Recovery"); + + try + { + using (RemoteExecutor.Invoke( + EnlistAndCrash, + propagationTokenText, guid2.ToString(), secondEnlistmentRecoveryFilePath, + new RemoteInvokeOptions { ExpectedExitCode = 42 })) + { + // Wait for the external process to enlist in the transaction, it will signal this EventWaitHandle. + Assert.True(waitHandle.WaitOne(Timeout)); + + tx.Commit(); + } + + // The other has crashed when the MSDTC notified it to commit. + // Load the recovery information the other process has written to disk for us and reenlist with + // the failed RM's Guid to commit. + var outcomeEvent3 = new AutoResetEvent(false); + var enlistment3 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed, outcomeReceived: outcomeEvent3); + byte[] secondRecoveryInformation = File.ReadAllBytes(secondEnlistmentRecoveryFilePath); + _ = TransactionManager.Reenlist(guid2, secondRecoveryInformation, enlistment3); + TransactionManager.RecoveryComplete(guid2); + + Assert.True(outcomeEvent1.WaitOne(Timeout)); + Assert.True(outcomeEvent3.WaitOne(Timeout)); + Assert.Equal(EnlistmentOutcome.Committed, enlistment1.Outcome); + Assert.Equal(EnlistmentOutcome.Committed, enlistment3.Outcome); + Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status); + + // Note: verify manually in the MSDTC console that the distributed transaction is gone + // (i.e. successfully committed), + // (Start -> Component Services -> Computers -> My Computer -> Distributed Transaction Coordinator -> + // Local DTC -> Transaction List) + } + finally + { + File.Delete(secondEnlistmentRecoveryFilePath); + } + + static void EnlistAndCrash(string propagationTokenText, string resourceManagerIdentifierGuid, string recoveryInformationFilePath) + { + byte[] propagationToken = Convert.FromBase64String(propagationTokenText); + var tx = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); + + var crashingEnlistment = new CrashingEnlistment(recoveryInformationFilePath); + tx.EnlistDurable(Guid.Parse(resourceManagerIdentifierGuid), crashingEnlistment, EnlistmentOptions.None); + + // Signal to the main process that we've enlisted and are ready to accept prepare/commit. + using var waitHandle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, "System.Transactions.Tests.OleTxTests.Recovery"); + waitHandle.Set(); + + // We've enlisted, and set it up so that when the MSDTC tells us to commit, the process will crash. + Thread.Sleep(Timeout); + } + } + + public class CrashingEnlistment : IEnlistmentNotification + { + private string _recoveryInformationFilePath; + + public CrashingEnlistment(string recoveryInformationFilePath) + => _recoveryInformationFilePath = recoveryInformationFilePath; + + public void Prepare(PreparingEnlistment preparingEnlistment) + { + // Received a prepare notification from MSDTC, persist the recovery information so that the main process can perform recovery for it. + File.WriteAllBytes(_recoveryInformationFilePath, preparingEnlistment.RecoveryInformation()); + + preparingEnlistment.Prepared(); + } + + public void Commit(Enlistment enlistment) + => Environment.Exit(42); // 42 is error code expected by RemoteExecutor + + public void Rollback(Enlistment enlistment) + => Environment.Exit(1); + + public void InDoubt(Enlistment enlistment) + => Environment.Exit(1); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void TransmitterPropagationToken() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + var tx = new CommittableTransaction(); + + Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + + var propagationToken = TransactionInterop.GetTransmitterPropagationToken(tx); + + Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + + var tx2 = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); + + Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void GetExportCookie() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + return; // Temporarily skip on 32-bit where we have an issue + } + + var tx = new CommittableTransaction(); + + var whereabouts = TransactionInterop.GetWhereabouts(); + + Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + + var exportCookie = TransactionInterop.GetExportCookie(tx, whereabouts); + + Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + + var tx2 = TransactionInterop.GetTransactionFromExportCookie(exportCookie); + + Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); + } + + // MSDTC is aynchronous, i.e. Commit/Rollback may return before the transaction has actually completed; + // so allow some time for assertions to succeed. + private static void Retry(Action action) + { + const int Retries = 50; + + for (var i = 0; i < Retries; i++) + { + try + { + action(); + return; + } + catch (EqualException) + { + if (i == Retries - 1) + { + throw; + } + + Thread.Sleep(100); + } + } + } +} diff --git a/src/libraries/System.Transactions.Local/tests/System.Transactions.Local.Tests.csproj b/src/libraries/System.Transactions.Local/tests/System.Transactions.Local.Tests.csproj index a5dda3d568935..2f5841b9f63d9 100644 --- a/src/libraries/System.Transactions.Local/tests/System.Transactions.Local.Tests.csproj +++ b/src/libraries/System.Transactions.Local/tests/System.Transactions.Local.Tests.csproj @@ -8,6 +8,8 @@ + + @@ -17,4 +19,7 @@ + + + \ No newline at end of file diff --git a/src/libraries/System.Transactions.Local/tests/TestEnlistments.cs b/src/libraries/System.Transactions.Local/tests/TestEnlistments.cs index 800b4aedd8fdb..039a7cc296c79 100644 --- a/src/libraries/System.Transactions.Local/tests/TestEnlistments.cs +++ b/src/libraries/System.Transactions.Local/tests/TestEnlistments.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Reflection; +using System.IO; using System.Threading; -using System.Threading.Tasks; using Xunit; +#nullable enable + namespace System.Transactions.Tests { public enum Phase1Vote { Prepared, ForceRollback, Done }; @@ -91,25 +92,35 @@ public void InDoubt(Enlistment enlistment) public class TestEnlistment : IEnlistmentNotification { - Phase1Vote _phase1Vote; - EnlistmentOutcome _expectedOutcome; - bool _volatileEnlistDuringPrepare; - bool _expectEnlistToSucceed; - AutoResetEvent _outcomeReceived; - Transaction _txToEnlist; + readonly Phase1Vote _phase1Vote; + readonly EnlistmentOutcome _expectedOutcome; + readonly bool _volatileEnlistDuringPrepare; + readonly bool _expectEnlistToSucceed; + readonly AutoResetEvent? _outcomeReceived; + readonly Transaction _txToEnlist; - public TestEnlistment(Phase1Vote phase1Vote, EnlistmentOutcome expectedOutcome, bool volatileEnlistDuringPrepare = false, bool expectEnlistToSucceed = true, AutoResetEvent outcomeReceived = null) + public TestEnlistment( + Phase1Vote phase1Vote, + EnlistmentOutcome expectedOutcome, + bool volatileEnlistDuringPrepare = false, + bool expectEnlistToSucceed = true, + AutoResetEvent? outcomeReceived = null) { _phase1Vote = phase1Vote; _expectedOutcome = expectedOutcome; _volatileEnlistDuringPrepare = volatileEnlistDuringPrepare; _expectEnlistToSucceed = expectEnlistToSucceed; _outcomeReceived = outcomeReceived; - _txToEnlist = Transaction.Current; + _txToEnlist = Transaction.Current!; } + public EnlistmentOutcome? Outcome { get; private set; } + public bool WasPreparedCalled { get; private set; } + public void Prepare(PreparingEnlistment preparingEnlistment) { + WasPreparedCalled = true; + switch (_phase1Vote) { case Phase1Vote.Prepared: @@ -132,19 +143,13 @@ public void Prepare(PreparingEnlistment preparingEnlistment) } case Phase1Vote.ForceRollback: { - if (_outcomeReceived != null) - { - _outcomeReceived.Set(); - } + _outcomeReceived?.Set(); preparingEnlistment.ForceRollback(); break; } case Phase1Vote.Done: { - if (_outcomeReceived != null) - { - _outcomeReceived.Set(); - } + _outcomeReceived?.Set(); preparingEnlistment.Done(); break; } @@ -153,32 +158,76 @@ public void Prepare(PreparingEnlistment preparingEnlistment) public void Commit(Enlistment enlistment) { + Outcome = EnlistmentOutcome.Committed; Assert.Equal(EnlistmentOutcome.Committed, _expectedOutcome); - if (_outcomeReceived != null) - { - _outcomeReceived.Set(); - } + _outcomeReceived?.Set(); enlistment.Done(); } public void Rollback(Enlistment enlistment) { + Outcome = EnlistmentOutcome.Aborted; Assert.Equal(EnlistmentOutcome.Aborted, _expectedOutcome); - if (_outcomeReceived != null) - { - _outcomeReceived.Set(); - } + _outcomeReceived?.Set(); enlistment.Done(); } public void InDoubt(Enlistment enlistment) { + Outcome = EnlistmentOutcome.InDoubt; Assert.Equal(EnlistmentOutcome.InDoubt, _expectedOutcome); - if (_outcomeReceived != null) + _outcomeReceived?.Set(); + enlistment.Done(); + } + } + + public class TestPromotableSinglePhaseEnlistment : IPromotableSinglePhaseNotification + { + private readonly Func? _promoteDelegate; + private EnlistmentOutcome _expectedOutcome; + private AutoResetEvent? _outcomeReceived; + + public bool InitializedCalled { get; private set; } + public bool PromoteCalled { get; private set; } + + public TestPromotableSinglePhaseEnlistment(Func? promoteDelegate, EnlistmentOutcome expectedOutcome, AutoResetEvent? outcomeReceived = null) + { + _promoteDelegate = promoteDelegate; + _expectedOutcome = expectedOutcome; + _outcomeReceived = outcomeReceived; + } + + public void Initialize() + => InitializedCalled = true; + + public byte[]? Promote() + { + PromoteCalled = true; + + if (_promoteDelegate is null) { - _outcomeReceived.Set(); + Assert.Fail("Promote called but no promotion delegate was provided"); } - enlistment.Done(); + + return _promoteDelegate(); + } + + public void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment) + { + Assert.Equal(EnlistmentOutcome.Committed, _expectedOutcome); + + _outcomeReceived?.Set(); + + singlePhaseEnlistment.Done(); + } + + public void Rollback(SinglePhaseEnlistment singlePhaseEnlistment) + { + Assert.Equal(EnlistmentOutcome.Aborted, _expectedOutcome); + + _outcomeReceived?.Set(); + + singlePhaseEnlistment.Done(); } } }