-
Notifications
You must be signed in to change notification settings - Fork 300
/
Copy pathDistributedTransactionTest.Windows.cs
181 lines (155 loc) · 7.89 KB
/
DistributedTransactionTest.Windows.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Data;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Transactions;
using Microsoft.Data.SqlClient.TestUtilities;
using Xunit;
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
{
[PlatformSpecific(TestPlatforms.Windows)]
public class DistributedTransactionTestWindows
{
#if NET
private static bool s_DelegatedTransactionCondition => DataTestUtility.AreConnStringsSetup() && DataTestUtility.IsNotAzureServer() && DataTestUtility.IsNotX86Architecture;
[ConditionalFact(nameof(s_DelegatedTransactionCondition), Timeout = 10000)]
public async Task Delegated_transaction_deadlock_in_SinglePhaseCommit()
{
TransactionManager.ImplicitDistributedTransactions = true;
using var transaction = new CommittableTransaction();
// Uncommenting the following makes the deadlock go away as a workaround. If the transaction is promoted before
// the first SqlClient enlistment, it never goes into the delegated state.
// _ = TransactionInterop.GetTransmitterPropagationToken(transaction);
await using var conn = new SqlConnection(DataTestUtility.TCPConnectionString);
await conn.OpenAsync();
conn.EnlistTransaction(transaction);
// Enlisting the transaction in second connection causes the transaction to be promoted.
// After this, the transaction state will be "delegated" (delegated to SQL Server), and the commit below will
// trigger a call to SqlDelegatedTransaction.SinglePhaseCommit.
await using var conn2 = new SqlConnection(DataTestUtility.TCPConnectionString);
await conn2.OpenAsync();
conn2.EnlistTransaction(transaction);
// Possible deadlock
transaction.Commit();
}
#endif
private static bool s_EnlistedTransactionPreservedWhilePooledCondition => DataTestUtility.AreConnStringsSetup() && DataTestUtility.IsNotX86Architecture;
[ConditionalFact(nameof(s_EnlistedTransactionPreservedWhilePooledCondition), Timeout = 10000)]
public void Test_EnlistedTransactionPreservedWhilePooled()
{
#if NET
TransactionManager.ImplicitDistributedTransactions = true;
#endif
RunTestSet(EnlistedTransactionPreservedWhilePooled);
}
private void EnlistedTransactionPreservedWhilePooled()
{
Exception commandException = null;
Exception transactionException = null;
try
{
using (TransactionScope txScope = new TransactionScope(TransactionScopeOption.Required, TimeSpan.MaxValue))
{
// Leave first connection open so that the transaction is promoted
SqlConnection rootConnection = new SqlConnection(ConnectionString);
rootConnection.Open();
using (SqlCommand command = rootConnection.CreateCommand())
{
command.CommandText = $"INSERT INTO {TestTableName} VALUES ({InputCol1}, '{InputCol2}')";
command.ExecuteNonQuery();
}
// Closing and reopening cycles the connection through the pool.
// We want to verify that the transaction state is preserved through this cycle.
SqlConnection enlistedConnection = new SqlConnection(ConnectionString);
enlistedConnection.Open();
enlistedConnection.Close();
enlistedConnection.Open();
// Forcibly kill the root connection to mimic gateway's behavior when using the proxy connection policy
// https://techcommunity.microsoft.com/blog/azuredbsupport/azure-sql-database-idle-sessions-are-killed-after-about-30-minutes-when-proxy-co/3268601
// Can also represent a general server-side, process failure
KillProcess(rootConnection.ServerProcessId);
using (SqlCommand command = enlistedConnection.CreateCommand())
{
command.CommandText = $"INSERT INTO {TestTableName} VALUES ({InputCol1}, '{InputCol2}')";
try
{
command.ExecuteNonQuery();
}
catch (Exception ex)
{
commandException = ex;
}
}
txScope.Complete();
}
}
catch (Exception ex)
{
transactionException = ex;
}
if (Utils.IsAzureSqlServer(new SqlConnectionStringBuilder((ConnectionString)).DataSource))
{
// Even if an application swallows the command exception, completing the transaction should indicate that it failed.
Assert.IsType<TransactionInDoubtException>(transactionException);
// See https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors-3000-to-3999?view=sql-server-ver16
// Error 3971 corresponds to "The server failed to resume the transaction."
Assert.Equal(3971, ((SqlException)commandException).Number);
}
else
{
Assert.IsType<TransactionAbortedException>(transactionException);
#if NETFRAMEWORK
// See https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors-8000-to-8999?view=sql-server-ver16
// The distributed transaction failed
Assert.Equal(8525, ((SqlException)commandException).Number);
#else
Assert.IsType<InvalidOperationException>(commandException);
#endif
}
// Verify that nothing made it into the database
DataTable result = DataTestUtility.RunQuery(ConnectionString, $"select col2 from {TestTableName} where col1 = {InputCol1}");
Assert.True(result.Rows.Count == 0);
}
private void KillProcess(int serverProcessId)
{
using (TransactionScope txScope = new TransactionScope(TransactionScopeOption.Suppress))
{
using (SqlConnection connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (SqlCommand command = connection.CreateCommand())
{
command.CommandText = $"KILL {serverProcessId}";
command.ExecuteNonQuery();
}
}
txScope.Complete();
}
}
private static string TestTableName;
private static string ConnectionString;
private const int InputCol1 = 1;
private const string InputCol2 = "One";
private static void RunTestSet(Action TestCase)
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString);
builder.Pooling = true;
builder.MaxPoolSize = 5;
builder.Enlist = true;
ConnectionString = builder.ConnectionString;
TestTableName = DataTestUtility.GenerateObjectName();
DataTestUtility.RunNonQuery(ConnectionString, $"create table {TestTableName} (col1 int, col2 text)");
try
{
TestCase();
}
finally
{
DataTestUtility.RunNonQuery(ConnectionString, $"drop table {TestTableName}");
}
}
}
}