Skip to content

Commit b7167c0

Browse files
Ports Fix deadlock in transaction (#1242) to .NET (#2161)
1 parent 85ba241 commit b7167c0

File tree

4 files changed

+58
-52
lines changed

4 files changed

+58
-52
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs

+44-42
Original file line numberDiff line numberDiff line change
@@ -348,31 +348,34 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
348348
#endif
349349
try
350350
{
351-
Exception commitException = null;
352-
353-
lock (connection)
351+
// If the connection is doomed, we can be certain that the
352+
// transaction will eventually be rolled back or has already been aborted externally, and we shouldn't
353+
// attempt to commit it.
354+
if (connection.IsConnectionDoomed)
354355
{
355-
// If the connection is doomed, we can be certain that the
356-
// transaction will eventually be rolled back or has already been aborted externally, and we shouldn't
357-
// attempt to commit it.
358-
if (connection.IsConnectionDoomed)
356+
lock (connection)
359357
{
360358
_active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
361359
_connection = null;
362-
363-
enlistment.Aborted(SQL.ConnectionDoomed());
364360
}
365-
else
361+
362+
enlistment.Aborted(SQL.ConnectionDoomed());
363+
}
364+
else
365+
{
366+
Exception commitException;
367+
lock (connection)
366368
{
367369
try
368370
{
369371
// Now that we've acquired the lock, make sure we still have valid state for this operation.
370372
ValidateActiveOnConnection(connection);
371373

372374
_active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
373-
_connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event
375+
_connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event
374376

375377
connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true);
378+
commitException = null;
376379
}
377380
catch (SqlException e)
378381
{
@@ -391,42 +394,41 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
391394
ADP.TraceExceptionWithoutRethrow(e);
392395
connection.DoomThisConnection();
393396
}
394-
if (commitException != null)
397+
}
398+
if (commitException != null)
399+
{
400+
// connection.ExecuteTransaction failed with exception
401+
if (_internalTransaction.IsCommitted)
395402
{
396-
// connection.ExecuteTransaction failed with exception
397-
if (_internalTransaction.IsCommitted)
398-
{
399-
// Even though we got an exception, the transaction
400-
// was committed by the server.
401-
enlistment.Committed();
402-
}
403-
else if (_internalTransaction.IsAborted)
404-
{
405-
// The transaction was aborted, report that to
406-
// SysTx.
407-
enlistment.Aborted(commitException);
408-
}
409-
else
410-
{
411-
// The transaction is still active, we cannot
412-
// know the state of the transaction.
413-
enlistment.InDoubt(commitException);
414-
}
415-
416-
// We eat the exception. This is called on the SysTx
417-
// thread, not the applications thread. If we don't
418-
// eat the exception an UnhandledException will occur,
419-
// causing the process to FailFast.
403+
// Even though we got an exception, the transaction
404+
// was committed by the server.
405+
enlistment.Committed();
406+
}
407+
else if (_internalTransaction.IsAborted)
408+
{
409+
// The transaction was aborted, report that to
410+
// SysTx.
411+
enlistment.Aborted(commitException);
412+
}
413+
else
414+
{
415+
// The transaction is still active, we cannot
416+
// know the state of the transaction.
417+
enlistment.InDoubt(commitException);
420418
}
421419

422-
connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
420+
// We eat the exception. This is called on the SysTx
421+
// thread, not the applications thread. If we don't
422+
// eat the exception an UnhandledException will occur,
423+
// causing the process to FailFast.
423424
}
424-
}
425425

426-
if (commitException == null)
427-
{
428-
// connection.ExecuteTransaction succeeded
429-
enlistment.Committed();
426+
connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
427+
if (commitException == null)
428+
{
429+
// connection.ExecuteTransaction succeeded
430+
enlistment.Committed();
431+
}
430432
}
431433
}
432434
catch (System.OutOfMemoryException e)

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs

-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@ public void SinglePhaseCommit(SysTx.SinglePhaseEnlistment enlistment)
387387
Debug.Assert(null != enlistment, "null enlistment?");
388388
SqlInternalConnection connection = GetValidConnection();
389389

390-
391390
if (null != connection)
392391
{
393392
SqlConnection usersConnection = connection.Connection;

src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs

+11
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,17 @@ public static bool IsTargetReadyForAeWithKeyStore()
439439
;
440440
}
441441

442+
public static bool IsSupportingDistributedTransactions()
443+
{
444+
#if NET7_0_OR_GREATER
445+
return OperatingSystem.IsWindows() && System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture != System.Runtime.InteropServices.Architecture.X86 && IsNotAzureServer();
446+
#elif NETFRAMEWORK
447+
return IsNotAzureServer();
448+
#else
449+
return false;
450+
#endif
451+
}
452+
442453
public static bool IsUsingManagedSNI() => UseManagedSNIOnWindows;
443454

444455
public static bool IsNotUsingManagedSNIOnWindows() => !UseManagedSNIOnWindows;

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionEnlistmentTest.cs

+3-9
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,10 @@ public static void TestManualEnlistment_Enlist_TxScopeComplete()
4747
RunTestSet(TestCase_ManualEnlistment_Enlist_TxScopeComplete);
4848
}
4949

50-
[SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)]
51-
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
50+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSupportingDistributedTransactions))]
5251
public static void TestEnlistmentPrepare_TxScopeComplete()
5352
{
54-
try
53+
Assert.Throws<TransactionAbortedException>( () =>
5554
{
5655
using TransactionScope txScope = new(TransactionScopeOption.RequiresNew, new TransactionOptions()
5756
{
@@ -63,12 +62,7 @@ public static void TestEnlistmentPrepare_TxScopeComplete()
6362
connection.Open();
6463
System.Transactions.Transaction.Current.EnlistDurable(EnlistmentForPrepare.s_id, new EnlistmentForPrepare(), EnlistmentOptions.None);
6564
txScope.Complete();
66-
Assert.False(true, "Expected exception not thrown.");
67-
}
68-
catch (Exception e)
69-
{
70-
Assert.True(e is TransactionAbortedException);
71-
}
65+
});
7266
}
7367

7468
private static void TestCase_AutoEnlistment_TxScopeComplete()

0 commit comments

Comments
 (0)