diff --git a/src/libraries/System.Transactions.Local/tests/OleTxTests.cs b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs index 8ff6a221da1e7b..6a97aed542a13f 100644 --- a/src/libraries/System.Transactions.Local/tests/OleTxTests.cs +++ b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs @@ -27,250 +27,230 @@ public OleTxTests(OleTxFixture fixture) [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 (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + => Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } - - using var tx = new CommittableTransaction(); + using var tx = new CommittableTransaction(); - try - { - var enlistment1 = new TestEnlistment(vote1, expectedOutcome1); - var enlistment2 = new TestEnlistment(vote2, expectedOutcome2); + 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); + 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); - } + 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)); - } + Retry(() => Assert.Equal(expectedTxStatus, tx.TransactionInformation.Status)); + }); [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] public void Two_durable_enlistments_rollback() - { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + => Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } - - using var tx = new CommittableTransaction(); + using var tx = new CommittableTransaction(); - var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); - var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Aborted); + 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.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None); + tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None); - tx.Rollback(); + tx.Rollback(); - Assert.False(enlistment1.WasPreparedCalled); - Assert.False(enlistment2.WasPreparedCalled); + Assert.False(enlistment1.WasPreparedCalled); + Assert.False(enlistment2.WasPreparedCalled); - // This matches the .NET Framework behavior - Retry(() => Assert.Equal(TransactionStatus.Aborted, tx.TransactionInformation.Status)); - } + // 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 (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + => Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } - - using var tx = new CommittableTransaction(); + using var tx = new CommittableTransaction(); - if (volatileCount > 0) - { - TestEnlistment[] volatiles = new TestEnlistment[volatileCount]; - for (int i = 0; i < volatileCount; i++) + if (volatileCount > 0) { - // It doesn't matter what we specify for SinglePhaseVote. - volatiles[i] = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); - tx.EnlistVolatile(volatiles[i], EnlistmentOptions.None); + 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); + 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); + // Creation of two phase durable enlistment attempts to promote to MSDTC + tx.EnlistDurable(Guid.NewGuid(), durable, EnlistmentOptions.None); - tx.Commit(); + tx.Commit(); - Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status)); - } + Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status)); + }); protected static bool IsRemoteExecutorSupportedAndNotNano => RemoteExecutor.IsSupported && PlatformDetection.IsNotWindowsNanoServer; [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))] public void Promotion() { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } - - // 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. - using 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"); + // 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. + using var tx = new CommittableTransaction(); - RemoteInvokeHandle? remote1 = null, remote2 = null; + 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"); - 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(); + RemoteInvokeHandle? remote1 = null, remote2 = null; - Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status)); - } - catch - { try { - remote1?.Process.Kill(); - remote2?.Process.Kill(); + 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); - } + 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(); + // 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) - { - using var tx = new CommittableTransaction(); + => Test(() => + { + using 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); + 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); + // 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(); + // 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)); + // 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(); + 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 - } + // 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); - using 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 - } + => Test(() => + { + // Load the export cookie and enlist durably + byte[] exportCookie = File.ReadAllBytes(exportCookieFilePath); + using 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 @@ -309,90 +289,87 @@ public void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment) [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))] public void Recovery() { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } + // 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. - // 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. - - using var tx = new CommittableTransaction(); + using 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); + 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(); + // 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"); + 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 })) + try { - // Wait for the external process to enlist in the transaction, it will signal this EventWaitHandle. - Assert.True(waitHandle.WaitOne(Timeout)); + 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(); - } + 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); - } + // 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); - using var tx = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); + => Test(() => + { + byte[] propagationToken = Convert.FromBase64String(propagationTokenText); + using var tx = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); - var crashingEnlistment = new CrashingEnlistment(recoveryInformationFilePath); - tx.EnlistDurable(Guid.Parse(resourceManagerIdentifierGuid), crashingEnlistment, EnlistmentOptions.None); + 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(); + // 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); - } + // 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 @@ -422,50 +399,71 @@ public void InDoubt(Enlistment enlistment) [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] public void TransmitterPropagationToken() - { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + => Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } + using var tx = new CommittableTransaction(); - using var tx = new CommittableTransaction(); + Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); - Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + var propagationToken = TransactionInterop.GetTransmitterPropagationToken(tx); - var propagationToken = TransactionInterop.GetTransmitterPropagationToken(tx); + Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); - Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + var tx2 = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); - var tx2 = TransactionInterop.GetTransactionFromTransmitterPropagationToken(propagationToken); - - Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); - } + Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); + }); [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] public void GetExportCookie() - { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + => Test(() => { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } + using var tx = new CommittableTransaction(); + + var whereabouts = TransactionInterop.GetWhereabouts(); + + Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + + var exportCookie = TransactionInterop.GetExportCookie(tx, whereabouts); - using var tx = new CommittableTransaction(); + Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); - var whereabouts = TransactionInterop.GetWhereabouts(); + var tx2 = TransactionInterop.GetTransactionFromExportCookie(exportCookie); - Assert.Equal(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); + }); - var exportCookie = TransactionInterop.GetExportCookie(tx, whereabouts); + private static void Test(Action action) + { + // Temporarily skip on 32-bit where we have an issue. + // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 + if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) + { + return; + } - Assert.NotEqual(Guid.Empty, tx.TransactionInformation.DistributedIdentifier); + // In CI, we sometimes get XACT_E_TMNOTAVAILABLE; when it happens, it's typically on the very first + // attempt to connect to MSDTC (flaky/slow on-demand startup of MSDTC), though not only. + // This catches that error and retries. + int nRetries = 5; - var tx2 = TransactionInterop.GetTransactionFromExportCookie(exportCookie); + while (true) + { + try + { + action(); + return; + } + catch (TransactionException e) when (e.InnerException is TransactionManagerCommunicationException) + { + if (--nRetries == 0) + { + throw; + } - Assert.Equal(tx.TransactionInformation.DistributedIdentifier, tx2.TransactionInformation.DistributedIdentifier); + Thread.Sleep(500); + } + } } // MSDTC is aynchronous, i.e. Commit/Rollback may return before the transaction has actually completed; @@ -495,46 +493,21 @@ private static void Retry(Action action) public class OleTxFixture { + // In CI, we sometimes get XACT_E_TMNOTAVAILABLE on the very first attempt to connect to MSDTC; + // this is likely due to on-demand slow startup of MSDTC. Perform pre-test connecting with retry + // to ensure that MSDTC is properly up when the first test runs. public OleTxFixture() - { - if (!Environment.Is64BitProcess || PlatformDetection.IsArm64Process) - { - // Temporarily skip on 32-bit where we have an issue - // ARM64 issue: https://github.com/dotnet/runtime/issues/74170 - return; - } - - // In CI, we sometimes get XACT_E_TMNOTAVAILABLE on the very first attempt to connect to MSDTC; - // this is likely due to on-demand slow startup of MSDTC. Perform pre-test connecting with retry - // to ensure that MSDTC is properly up when the first test runs. - int nRetries = 5; - - while (true) + => Test(() => { - try - { - using var tx = new CommittableTransaction(); + using var tx = new CommittableTransaction(); - var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); - var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); + var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); + var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed); - tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None); - tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None); + tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None); + tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None); - tx.Commit(); - - return; - } - catch (TransactionException e) when (e.InnerException is TransactionManagerCommunicationException) - { - if (--nRetries == 0) - { - throw; - } - - Thread.Sleep(100); - } - } - } + tx.Commit(); + }); } }