Skip to content

Commit a425331

Browse files
authored
[5.0.2] Fix | Release connection lock before signaling SinglePhaseCommit completion (#1975)
1 parent d9b6414 commit a425331

File tree

3 files changed

+51
-7
lines changed

3 files changed

+51
-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
@@ -192,6 +192,7 @@
192192
<Compile Include="SQL\SqlNotificationTest\SqlNotificationTest.cs" />
193193
<Compile Include="SQL\SqlSchemaInfoTest\SqlSchemaInfoTest.cs" />
194194
<Compile Include="SQL\SqlStatisticsTest\SqlStatisticsTest.cs" />
195+
<Compile Include="SQL\TransactionTest\DistributedTransactionTest.cs" />
195196
<Compile Include="SQL\TransactionTest\TransactionTest.cs" />
196197
<Compile Include="SQL\TransactionTest\TransactionEnlistmentTest.cs" />
197198
<Compile Include="SQL\UdtTest\SqlServerTypesTest.cs" />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.Threading.Tasks;
6+
using System.Transactions;
7+
using Xunit;
8+
9+
#if NET7_0_OR_GREATER
10+
11+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
12+
{
13+
[PlatformSpecific(TestPlatforms.Windows)]
14+
public class DistributedTransactionTest
15+
{
16+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), Timeout = 10000)]
17+
public async Task Delegated_transaction_deadlock_in_SinglePhaseCommit()
18+
{
19+
TransactionManager.ImplicitDistributedTransactions = true;
20+
using var transaction = new CommittableTransaction();
21+
22+
// Uncommenting the following makes the deadlock go away as a workaround. If the transaction is promoted before
23+
// the first SqlClient enlistment, it never goes into the delegated state.
24+
// _ = TransactionInterop.GetTransmitterPropagationToken(transaction);
25+
await using var conn = new SqlConnection(DataTestUtility.TCPConnectionString);
26+
await conn.OpenAsync();
27+
conn.EnlistTransaction(transaction);
28+
29+
// Enlisting the transaction in second connection causes the transaction to be promoted.
30+
// After this, the transaction state will be "delegated" (delegated to SQL Server), and the commit below will
31+
// trigger a call to SqlDelegatedTransaction.SinglePhaseCommit.
32+
await using var conn2 = new SqlConnection(DataTestUtility.TCPConnectionString);
33+
await conn2.OpenAsync();
34+
conn2.EnlistTransaction(transaction);
35+
36+
// Possible deadlock
37+
transaction.Commit();
38+
}
39+
}
40+
}
41+
42+
#endif

0 commit comments

Comments
 (0)