Skip to content

Commit f25e96d

Browse files
Fix | Fix deadlock in transaction (.NET Framework) (#1242)
1 parent 601d9f8 commit f25e96d

File tree

2 files changed

+76
-39
lines changed

2 files changed

+76
-39
lines changed

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

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -391,21 +391,23 @@ public void SinglePhaseCommit(SysTx.SinglePhaseEnlistment enlistment)
391391
#else
392392
{
393393
#endif //DEBUG
394-
lock (connection)
394+
// If the connection is doomed, we can be certain that the
395+
// transaction will eventually be rolled back, and we shouldn't
396+
// attempt to commit it.
397+
if (connection.IsConnectionDoomed)
395398
{
396-
// If the connection is doomed, we can be certain that the
397-
// transaction will eventually be rolled back or has already been aborted externally, and we shouldn't
398-
// attempt to commit it.
399-
if (connection.IsConnectionDoomed)
399+
lock (connection)
400400
{
401401
_active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
402402
_connection = null;
403-
404-
enlistment.Aborted(SQL.ConnectionDoomed());
405403
}
406-
else
404+
enlistment.Aborted(SQL.ConnectionDoomed());
405+
}
406+
else
407+
{
408+
Exception commitException;
409+
lock (connection)
407410
{
408-
Exception commitException;
409411
try
410412
{
411413
// Now that we've acquired the lock, make sure we still have valid state for this operation.
@@ -434,40 +436,40 @@ public void SinglePhaseCommit(SysTx.SinglePhaseEnlistment enlistment)
434436
ADP.TraceExceptionWithoutRethrow(e);
435437
connection.DoomThisConnection();
436438
}
437-
if (commitException != null)
439+
}
440+
if (commitException != null)
441+
{
442+
// connection.ExecuteTransaction failed with exception
443+
if (_internalTransaction.IsCommitted)
438444
{
439-
// connection.ExecuteTransaction failed with exception
440-
if (_internalTransaction.IsCommitted)
441-
{
442-
// Even though we got an exception, the transaction
443-
// was committed by the server.
444-
enlistment.Committed();
445-
}
446-
else if (_internalTransaction.IsAborted)
447-
{
448-
// The transaction was aborted, report that to
449-
// SysTx.
450-
enlistment.Aborted(commitException);
451-
}
452-
else
453-
{
454-
// The transaction is still active, we cannot
455-
// know the state of the transaction.
456-
enlistment.InDoubt(commitException);
457-
}
458-
459-
// We eat the exception. This is called on the SysTx
460-
// thread, not the applications thread. If we don't
461-
// eat the exception an UnhandledException will occur,
462-
// causing the process to FailFast.
445+
// Even though we got an exception, the transaction
446+
// was committed by the server.
447+
enlistment.Committed();
463448
}
464-
465-
connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
466-
if (commitException == null)
449+
else if (_internalTransaction.IsAborted)
467450
{
468-
// connection.ExecuteTransaction succeeded
469-
enlistment.Committed();
451+
// The transaction was aborted, report that to
452+
// SysTx.
453+
enlistment.Aborted(commitException);
470454
}
455+
else
456+
{
457+
// The transaction is still active, we cannot
458+
// know the state of the transaction.
459+
enlistment.InDoubt(commitException);
460+
}
461+
462+
// We eat the exception. This is called on the SysTx
463+
// thread, not the applications thread. If we don't
464+
// eat the exception an UnhandledException will occur,
465+
// causing the process to FailFast.
466+
}
467+
468+
connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
469+
if (commitException == null)
470+
{
471+
// connection.ExecuteTransaction succeeded
472+
enlistment.Committed();
471473
}
472474
}
473475
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Data;
7+
using System.Threading.Tasks;
78
using System.Transactions;
89
using Xunit;
910

@@ -46,6 +47,30 @@ public static void TestManualEnlistment_Enlist_TxScopeComplete()
4647
RunTestSet(TestCase_ManualEnlistment_Enlist_TxScopeComplete);
4748
}
4849

50+
[SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)]
51+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
52+
public static void TestEnlistmentPrepare_TxScopeComplete()
53+
{
54+
try
55+
{
56+
using TransactionScope txScope = new(TransactionScopeOption.RequiresNew, new TransactionOptions()
57+
{
58+
IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
59+
Timeout = TransactionManager.DefaultTimeout
60+
}, TransactionScopeAsyncFlowOption.Enabled);
61+
62+
using SqlConnection connection = new(DataTestUtility.TCPConnectionString);
63+
connection.Open();
64+
System.Transactions.Transaction.Current.EnlistDurable(EnlistmentForPrepare.s_id, new EnlistmentForPrepare(), EnlistmentOptions.None);
65+
txScope.Complete();
66+
Assert.False(true, "Expected exception not thrown.");
67+
}
68+
catch (Exception e)
69+
{
70+
Assert.True(e is TransactionAbortedException);
71+
}
72+
}
73+
4974
private static void TestCase_AutoEnlistment_TxScopeComplete()
5075
{
5176
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(ConnectionString);
@@ -168,6 +193,16 @@ private static void TestCase_ManualEnlistment_Enlist_TxScopeComplete()
168193
Assert.True(string.Equals(result.Rows[0][0], InputCol2));
169194
}
170195

196+
class EnlistmentForPrepare : IEnlistmentNotification
197+
{
198+
public static readonly Guid s_id = Guid.NewGuid();
199+
// fail during prepare, this will cause scope.Complete to throw
200+
public void Prepare(PreparingEnlistment preparingEnlistment) => preparingEnlistment.ForceRollback();
201+
public void Commit(Enlistment enlistment) => enlistment.Done();
202+
public void Rollback(Enlistment enlistment) => enlistment.Done();
203+
public void InDoubt(Enlistment enlistment) => enlistment.Done();
204+
}
205+
171206
private static string TestTableName;
172207
private static string ConnectionString;
173208
private const int InputCol1 = 1;

0 commit comments

Comments
 (0)