Skip to content

Commit 9f8117a

Browse files
[3.1.2] Backport: Release connection lock before signaling SinglePhaseCommit completion (#1912)
[3.1.2] Backport: Release connection lock before signaling SinglePhaseCommit completion #1912
1 parent 523a1ee commit 9f8117a

File tree

4 files changed

+56
-7
lines changed

4 files changed

+56
-7
lines changed

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
340340
RuntimeHelpers.PrepareConstrainedRegions();
341341
try
342342
{
343+
Exception commitException = null;
344+
343345
lock (connection)
344346
{
345347
// If the connection is doomed, we can be certain that the
@@ -354,7 +356,6 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
354356
}
355357
else
356358
{
357-
Exception commitException;
358359
try
359360
{
360361
// Now that we've acquired the lock, make sure we still have valid state for this operation.
@@ -364,7 +365,6 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
364365
_connection = null; // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event
365366

366367
connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, System.Data.IsolationLevel.Unspecified, _internalTransaction, true);
367-
commitException = null;
368368
}
369369
catch (SqlException e)
370370
{
@@ -412,13 +412,14 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment)
412412
}
413413

414414
connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
415-
if (commitException == null)
416-
{
417-
// connection.ExecuteTransaction succeeded
418-
enlistment.Committed();
419-
}
420415
}
421416
}
417+
418+
if (commitException == null)
419+
{
420+
// connection.ExecuteTransaction succeeded
421+
enlistment.Committed();
422+
}
422423
}
423424
catch (System.OutOfMemoryException e)
424425
{

src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
<Compile Include="SQL\RandomStressTest\RandomStressTest.cs" />
203203
<Compile Include="SQL\SplitPacketTest\SplitPacketTest.cs" />
204204
<Compile Include="SQL\SqlNotificationTest\SqlNotificationTest.cs" />
205+
<Compile Include="SQL\TransactionTest\DistributedTransactionTest.cs" />
205206
<Compile Include="SQL\TransactionTest\TransactionTest.cs" />
206207
<Compile Include="SQL\TransactionTest\TransactionEnlistmentTest.cs" />
207208
<Compile Include="SQL\UdtTest\SqlServerTypesTest.cs" />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Threading.Tasks;
7+
using System.Transactions;
8+
using Xunit;
9+
10+
#if NET7_0_OR_GREATER
11+
12+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
13+
{
14+
[PlatformSpecific(TestPlatforms.Windows)]
15+
public class DistributedTransactionTest
16+
{
17+
private static bool s_DelegatedTransactionCondition => DataTestUtility.AreConnStringsSetup() && DataTestUtility.IsNotAzureServer() && PlatformDetection.IsNotX86Process;
18+
19+
[ConditionalFact(nameof(s_DelegatedTransactionCondition), Timeout = 10000)]
20+
public async Task Delegated_transaction_deadlock_in_SinglePhaseCommit()
21+
{
22+
TransactionManager.ImplicitDistributedTransactions = true;
23+
using var transaction = new CommittableTransaction();
24+
25+
// Uncommenting the following makes the deadlock go away as a workaround. If the transaction is promoted before
26+
// the first SqlClient enlistment, it never goes into the delegated state.
27+
// _ = TransactionInterop.GetTransmitterPropagationToken(transaction);
28+
await using var conn = new SqlConnection(DataTestUtility.TCPConnectionString);
29+
await conn.OpenAsync();
30+
conn.EnlistTransaction(transaction);
31+
32+
// Enlisting the transaction in second connection causes the transaction to be promoted.
33+
// After this, the transaction state will be "delegated" (delegated to SQL Server), and the commit below will
34+
// trigger a call to SqlDelegatedTransaction.SinglePhaseCommit.
35+
await using var conn2 = new SqlConnection(DataTestUtility.TCPConnectionString);
36+
await conn2.OpenAsync();
37+
conn2.EnlistTransaction(transaction);
38+
39+
// Possible deadlock
40+
transaction.Commit();
41+
}
42+
}
43+
}
44+
45+
#endif

src/Microsoft.Data.SqlClient/tests/tools/Microsoft.DotNet.XUnitExtensions/Extensions/PlatformDetection.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ public static partial class PlatformDetection
1010
{
1111
public static bool IsArmProcess => RuntimeInformation.ProcessArchitecture == Architecture.Arm;
1212
public static bool IsNotArmProcess => !IsArmProcess;
13+
public static bool IsX86Process => RuntimeInformation.ProcessArchitecture == Architecture.X86;
14+
public static bool IsNotX86Process => !IsX86Process;
1315
}
1416
}

0 commit comments

Comments
 (0)