Skip to content

Commit cdb969d

Browse files
NH-3023 - Test case for pool corruption after deadlock
1 parent 7f478eb commit cdb969d

File tree

9 files changed

+797
-0
lines changed

9 files changed

+797
-0
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Data.SqlClient;
13+
using System.Diagnostics;
14+
using System.IO;
15+
using System.Linq;
16+
using System.Text.RegularExpressions;
17+
using System.Transactions;
18+
using log4net;
19+
using log4net.Repository.Hierarchy;
20+
using NHibernate.Dialect;
21+
using NHibernate.Driver;
22+
using NHibernate.Engine;
23+
using NUnit.Framework;
24+
25+
namespace NHibernate.Test.NHSpecificTest.NH3023
26+
{
27+
using System.Threading.Tasks;
28+
using System.Threading;
29+
[TestFixture]
30+
public class DeadlockConnectionPoolIssueTestAsync : BugTestCase
31+
{
32+
private static readonly ILog _log = LogManager.GetLogger(typeof(DeadlockConnectionPoolIssueTestAsync));
33+
34+
// Uses directly SqlConnection.
35+
protected override bool AppliesTo(ISessionFactoryImplementor factory)
36+
=> factory.ConnectionProvider.Driver is SqlClientDriver && base.AppliesTo(factory);
37+
38+
protected override bool AppliesTo(Dialect.Dialect dialect)
39+
=> dialect is MsSql2000Dialect && base.AppliesTo(dialect);
40+
41+
protected override void OnSetUp()
42+
{
43+
RunScript("db-seed.sql");
44+
45+
((Logger)_log.Logger).Level = log4net.Core.Level.Debug;
46+
}
47+
48+
protected override void OnTearDown()
49+
{
50+
//
51+
// Hopefully this will clean up the pool so that teardown can succeed
52+
//
53+
SqlConnection.ClearAllPools();
54+
55+
using (var s = OpenSession())
56+
{
57+
s.CreateQuery("delete from System.Object").ExecuteUpdate();
58+
}
59+
RunScript("db-teardown.sql");
60+
}
61+
62+
[Theory]
63+
public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
64+
{
65+
var tryCount = 0;
66+
var id = 1;
67+
var missingDeadlock = false;
68+
do
69+
{
70+
tryCount++;
71+
72+
try
73+
{
74+
_log.DebugFormat("Starting loop {0}", tryCount);
75+
// When the connection is released from transaction completion, the scope disposal after deadlock
76+
// takes up to 30 seconds (not at first try, but at subsequent tries). With additional logs, it
77+
// appears this delay occurs at connection closing. Definitely, there is something which can go
78+
// wrong when disposing a connection from transaction scope completion.
79+
// Note that the transaction completion event can execute as soon as the deadlock occurs. It does
80+
// not wait for the scope disposal.
81+
using (var session = OpenSession())
82+
//using (var session = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
83+
using (var scope = distributed ? CreateDistributedTransactionScope() : new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
84+
{
85+
_log.Debug("Session and scope opened");
86+
session.GetSessionImplementation().Factory.TransactionFactory
87+
.EnlistInSystemTransactionIfNeeded(session.GetSessionImplementation());
88+
_log.Debug("Session enlisted");
89+
try
90+
{
91+
await (new DeadlockHelper().ForceDeadlockOnConnectionAsync((SqlConnection)session.Connection));
92+
}
93+
catch (SqlException x)
94+
{
95+
//
96+
// Deadlock error code is 1205.
97+
//
98+
if (x.Errors.Cast<SqlError>().Any(e => e.Number == 1205))
99+
{
100+
//
101+
// It did what it was supposed to do.
102+
//
103+
_log.InfoFormat("Expected deadlock on attempt {0}. {1}", tryCount, x.Message);
104+
105+
// Check who takes time in the disposing
106+
var chrono = new Stopwatch();
107+
chrono.Start();
108+
scope.Dispose();
109+
_log.Debug("Scope disposed");
110+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal scope disposal duration");
111+
chrono.Restart();
112+
session.Dispose();
113+
_log.Debug("Session disposed");
114+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal session disposal duration");
115+
continue;
116+
}
117+
118+
//
119+
// ? This shouldn't happen
120+
//
121+
Assert.Fail("Surprising exception when trying to force a deadlock: {0}", x);
122+
}
123+
124+
_log.WarnFormat("Initial session seemingly not deadlocked at attempt {0}", tryCount);
125+
missingDeadlock = true;
126+
127+
try
128+
{
129+
await (session.SaveAsync(
130+
new DomainClass
131+
{
132+
Id = id++,
133+
ByteData = new byte[] { 1, 2, 3 }
134+
}));
135+
136+
await (session.FlushAsync());
137+
if (tryCount < 10)
138+
{
139+
_log.InfoFormat("Initial session still usable, trying again");
140+
continue;
141+
}
142+
_log.InfoFormat("Initial session still usable after {0} attempts, finishing test", tryCount);
143+
}
144+
catch (Exception ex)
145+
{
146+
_log.Error("Failed to continue using the session after lacking deadlock.", ex);
147+
// This exception would hide the transaction failure, if any.
148+
//throw;
149+
}
150+
_log.Debug("Completing scope");
151+
scope.Complete();
152+
_log.Debug("Scope completed");
153+
}
154+
_log.Debug("Session and scope disposed");
155+
}
156+
catch (AssertionException)
157+
{
158+
throw;
159+
}
160+
catch (Exception x)
161+
{
162+
_log.Error($"Initial session failed at attempt {tryCount}.", x);
163+
}
164+
165+
var subsequentFailedRequests = 0;
166+
167+
for (var i = 1; i <= 10; i++)
168+
{
169+
//
170+
// The error message will vary on subsequent requests, so we'll somewhat
171+
// arbitrarily try 10
172+
//
173+
174+
try
175+
{
176+
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
177+
{
178+
using (var session = OpenSession())
179+
{
180+
await (session.SaveAsync(
181+
new DomainClass
182+
{
183+
Id = id++,
184+
ByteData = new byte[] { 1, 2, 3 }
185+
}));
186+
187+
await (session.FlushAsync());
188+
}
189+
190+
scope.Complete();
191+
}
192+
}
193+
catch (Exception x)
194+
{
195+
subsequentFailedRequests++;
196+
_log.Error($"Subsequent session {i} failed.", x);
197+
}
198+
}
199+
200+
Assert.Fail("{0}; {1} subsequent requests failed.",
201+
missingDeadlock
202+
? "Deadlock not reported on initial request, and initial request failed"
203+
: "Initial request failed",
204+
subsequentFailedRequests);
205+
206+
} while (tryCount < 3);
207+
//
208+
// I'll change this to while(true) sometimes so I don't have to keep running the test
209+
//
210+
}
211+
212+
private static TransactionScope CreateDistributedTransactionScope()
213+
{
214+
var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
215+
//
216+
// Forces promotion to distributed transaction
217+
//
218+
TransactionInterop.GetTransmitterPropagationToken(System.Transactions.Transaction.Current);
219+
return scope;
220+
}
221+
222+
private async Task RunScriptAsync(string script, CancellationToken cancellationToken = default(CancellationToken))
223+
{
224+
var cxnString = cfg.Properties["connection.connection_string"] + "; Pooling=No";
225+
// Disable connection pooling so this won't be hindered by
226+
// problems encountered during the actual test
227+
228+
string sql;
229+
using (var reader = new StreamReader(GetType().Assembly.GetManifestResourceStream(GetType().Namespace + "." + script)))
230+
{
231+
sql = await (reader.ReadToEndAsync());
232+
}
233+
234+
using (var cxn = new SqlConnection(cxnString))
235+
{
236+
await (cxn.OpenAsync(cancellationToken));
237+
238+
foreach (var batch in Regex.Split(sql, @"^go\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline)
239+
.Where(b => !string.IsNullOrEmpty(b)))
240+
{
241+
242+
using (var cmd = new System.Data.SqlClient.SqlCommand(batch, cxn))
243+
{
244+
await (cmd.ExecuteNonQueryAsync(cancellationToken));
245+
}
246+
}
247+
}
248+
}
249+
250+
private void RunScript(string script)
251+
{
252+
var cxnString = cfg.Properties["connection.connection_string"] + "; Pooling=No";
253+
// Disable connection pooling so this won't be hindered by
254+
// problems encountered during the actual test
255+
256+
string sql;
257+
using (var reader = new StreamReader(GetType().Assembly.GetManifestResourceStream(GetType().Namespace + "." + script)))
258+
{
259+
sql = reader.ReadToEnd();
260+
}
261+
262+
using (var cxn = new SqlConnection(cxnString))
263+
{
264+
cxn.Open();
265+
266+
foreach (var batch in Regex.Split(sql, @"^go\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline)
267+
.Where(b => !string.IsNullOrEmpty(b)))
268+
{
269+
270+
using (var cmd = new System.Data.SqlClient.SqlCommand(batch, cxn))
271+
{
272+
cmd.ExecuteNonQuery();
273+
}
274+
}
275+
}
276+
}
277+
}
278+
}

0 commit comments

Comments
 (0)