Skip to content

Commit ce76c45

Browse files
NH-3023 - Cease closing connection from transaction scope completion whenever possible.
1 parent da7b112 commit ce76c45

File tree

10 files changed

+373
-72
lines changed

10 files changed

+373
-72
lines changed

src/NHibernate.Test/Async/NHSpecificTest/NH3023/DeadlockConnectionPoolIssueTest.cs

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
using System.IO;
1515
using System.Linq;
1616
using System.Text.RegularExpressions;
17+
using System.Threading;
1718
using System.Transactions;
1819
using log4net;
1920
using log4net.Repository.Hierarchy;
21+
using NHibernate.Cfg;
2022
using NHibernate.Dialect;
2123
using NHibernate.Driver;
2224
using NHibernate.Engine;
@@ -25,11 +27,19 @@
2527
namespace NHibernate.Test.NHSpecificTest.NH3023
2628
{
2729
using System.Threading.Tasks;
28-
using System.Threading;
2930
[TestFixture]
30-
public class DeadlockConnectionPoolIssueTestAsync : BugTestCase
31+
public class DeadlockConnectionPoolIssueAsync : BugTestCase
3132
{
32-
private static readonly ILog _log = LogManager.GetLogger(typeof(DeadlockConnectionPoolIssueTestAsync));
33+
private static readonly ILog _log = LogManager.GetLogger(typeof(DeadlockConnectionPoolIssueAsync));
34+
35+
protected virtual bool UseConnectionOnSystemTransactionPrepare => true;
36+
37+
protected override void Configure(Configuration configuration)
38+
{
39+
configuration.SetProperty(
40+
Cfg.Environment.UseConnectionOnSystemTransactionPrepare,
41+
UseConnectionOnSystemTransactionPrepare.ToString());
42+
}
3343

3444
// Uses directly SqlConnection.
3545
protected override bool AppliesTo(ISessionFactoryImplementor factory)
@@ -47,27 +57,31 @@ protected override void OnSetUp()
4757

4858
protected override void OnTearDown()
4959
{
60+
// Before clearing the pool for dodging pool corruption, we need to wait
61+
// for late transaction processing not yet ended.
62+
Thread.Sleep(100);
5063
//
5164
// Hopefully this will clean up the pool so that teardown can succeed
5265
//
5366
SqlConnection.ClearAllPools();
5467

68+
RunScript("db-teardown.sql");
69+
5570
using (var s = OpenSession())
5671
{
5772
s.CreateQuery("delete from System.Object").ExecuteUpdate();
5873
}
59-
RunScript("db-teardown.sql");
6074
}
6175

6276
[Theory]
63-
public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
77+
public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed, bool disposeSessionBeforeScope)
6478
{
6579
var tryCount = 0;
6680
var id = 1;
67-
var missingDeadlock = false;
6881
do
6982
{
7083
tryCount++;
84+
var missingDeadlock = false;
7185

7286
try
7387
{
@@ -78,13 +92,13 @@ public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
7892
// wrong when disposing a connection from transaction scope completion.
7993
// Note that the transaction completion event can execute as soon as the deadlock occurs. It does
8094
// 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))
95+
var session = OpenSession();
96+
var scope = distributed ? CreateDistributedTransactionScope() : new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
97+
try
8498
{
8599
_log.Debug("Session and scope opened");
86100
session.GetSessionImplementation().Factory.TransactionFactory
87-
.EnlistInSystemTransactionIfNeeded(session.GetSessionImplementation());
101+
.EnlistInSystemTransactionIfNeeded(session.GetSessionImplementation());
88102
_log.Debug("Session enlisted");
89103
try
90104
{
@@ -101,17 +115,6 @@ public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
101115
// It did what it was supposed to do.
102116
//
103117
_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");
115118
continue;
116119
}
117120

@@ -130,7 +133,7 @@ public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
130133
new DomainClass
131134
{
132135
Id = id++,
133-
ByteData = new byte[] { 1, 2, 3 }
136+
ByteData = new byte[] {1, 2, 3}
134137
}));
135138

136139
await (session.FlushAsync());
@@ -151,6 +154,57 @@ public async Task ConnectionPoolCorruptionAfterDeadlockAsync(bool distributed)
151154
scope.Complete();
152155
_log.Debug("Scope completed");
153156
}
157+
finally
158+
{
159+
// Check who takes time in the disposing
160+
var chrono = new Stopwatch();
161+
if (disposeSessionBeforeScope)
162+
{
163+
try
164+
{
165+
chrono.Start();
166+
session.Dispose();
167+
_log.Debug("Session disposed");
168+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal session disposal duration");
169+
}
170+
catch (Exception ex)
171+
{
172+
// Log in case it gets hidden by the next finally
173+
_log.Warn("Session disposal failure", ex);
174+
throw;
175+
}
176+
finally
177+
{
178+
chrono.Restart();
179+
scope.Dispose();
180+
_log.Debug("Scope disposed");
181+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal scope disposal duration");
182+
}
183+
}
184+
else
185+
{
186+
try
187+
{
188+
chrono.Start();
189+
scope.Dispose();
190+
_log.Debug("Scope disposed");
191+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal scope disposal duration");
192+
}
193+
catch (Exception ex)
194+
{
195+
// Log in case it gets hidden by the next finally
196+
_log.Warn("Scope disposal failure", ex);
197+
throw;
198+
}
199+
finally
200+
{
201+
chrono.Restart();
202+
session.Dispose();
203+
_log.Debug("Session disposed");
204+
Assert.That(chrono.Elapsed, Is.LessThan(TimeSpan.FromSeconds(2)), "Abnormal session disposal duration");
205+
}
206+
}
207+
}
154208
_log.Debug("Session and scope disposed");
155209
}
156210
catch (AssertionException)

src/NHibernate.Test/Async/SystemTransactions/DistributedSystemTransactionFixture.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,34 @@ public async Task DoNotDeadlockOnAfterTransactionWaitAsync()
641641
Assert.That(interceptor.Exception, Is.Null);
642642
}
643643

644+
[Test]
645+
public async Task EnforceConnectionUsageRulesOnTransactionCompletionAsync()
646+
{
647+
var interceptor = new TransactionCompleteUsingConnectionInterceptor();
648+
// Do not invert session and scope, it would cause an expected failure when
649+
// UseConnectionOnSystemTransactionEvents is false, due to the session being closed.
650+
using (var s = Sfi.WithOptions().Interceptor(interceptor).OpenSession())
651+
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
652+
{
653+
ForceEscalationToDistributedTx.Escalate();
654+
await (s.SaveAsync(new Person()));
655+
656+
await (s.FlushAsync());
657+
tx.Complete();
658+
}
659+
660+
if (UseConnectionOnSystemTransactionPrepare)
661+
{
662+
Assert.That(interceptor.BeforeException, Is.Null);
663+
}
664+
else
665+
{
666+
Assert.That(interceptor.BeforeException, Is.TypeOf<HibernateException>());
667+
}
668+
// Currently always forbidden, whatever UseConnectionOnSystemTransactionEvents.
669+
Assert.That(interceptor.AfterException, Is.TypeOf<HibernateException>());
670+
}
671+
644672
private void DodgeTransactionCompletionDelayIfRequired()
645673
{
646674
if (Sfi.ConnectionProvider.Driver.HasDelayedDistributedTransactionCompletion)

src/NHibernate.Test/Async/SystemTransactions/SystemTransactionFixture.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,35 @@ public async Task DoNotDeadlockOnAfterTransactionWaitAsync()
478478
Assert.That(interceptor.Exception, Is.Null);
479479
}
480480

481+
[Test]
482+
public async Task EnforceConnectionUsageRulesOnTransactionCompletionAsync()
483+
{
484+
var interceptor = new TransactionCompleteUsingConnectionInterceptor();
485+
// Do not invert session and scope, it would cause an expected failure when
486+
// UseConnectionOnSystemTransactionEvents is false, due to the session being closed.
487+
using (var s = WithOptions().Interceptor(interceptor).OpenSession())
488+
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
489+
{
490+
if (!AutoJoinTransaction)
491+
s.JoinTransaction();
492+
await (s.SaveAsync(new Person()));
493+
494+
await (s.FlushAsync());
495+
tx.Complete();
496+
}
497+
498+
if (UseConnectionOnSystemTransactionPrepare)
499+
{
500+
Assert.That(interceptor.BeforeException, Is.Null);
501+
}
502+
else
503+
{
504+
Assert.That(interceptor.BeforeException, Is.TypeOf<HibernateException>());
505+
}
506+
// Currently always forbidden, whatever UseConnectionOnSystemTransactionEvents.
507+
Assert.That(interceptor.AfterException, Is.TypeOf<HibernateException>());
508+
}
509+
481510
protected override ISession OpenSession()
482511
{
483512
if (AutoJoinTransaction)

0 commit comments

Comments
 (0)