Skip to content

Commit a9a5cc3

Browse files
Still attempt to release the session even if in use
1 parent 8a3b7ec commit a9a5cc3

File tree

3 files changed

+87
-18
lines changed

3 files changed

+87
-18
lines changed

src/NHibernate.Test/SystemTransactions/SystemTransactionFixture.cs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,10 @@ public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
649649
[Test]
650650
public void SupportsTransactionTimeout()
651651
{
652-
Assume.That(TestDialect.SupportsTransactionScopeTimeouts, Is.True);
652+
Assume.That(TestDialect.SupportsTransactionScopeTimeouts, Is.True, "The tested dialect is not supported for transaction scope timeouts.");
653+
// ODBC always freezes the session during transaction scopes timeouts.
654+
Assume.That(Sfi.ConnectionProvider.Driver, Is.Not.InstanceOf(typeof(OdbcDriver)), "ODBC is not supported for transaction scope timeouts.");
655+
653656
// Test case adapted from https://github.com/kaksmet/NHibBugRepro
654657

655658
// Create some test data.
@@ -670,7 +673,7 @@ public void SupportsTransactionTimeout()
670673
t.Commit();
671674
}
672675

673-
// Setup unhandler exception catcher
676+
// Setup unhandled exception catcher.
674677
_unhandledExceptions = new ConcurrentBag<object>();
675678
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
676679
try
@@ -697,18 +700,63 @@ public void SupportsTransactionTimeout()
697700
// Assume that is a transaction timeout. It may cause various failures, of which some are hard to identify.
698701
timeoutsCount++;
699702
}
703+
// If in need of checking some specific failures, the following code may be used instead:
704+
/*
705+
catch (Exception ex)
706+
{
707+
var currentEx = ex;
708+
// Depending on where the transaction aborption has broken NHibernate processing, we may
709+
// get various exceptions, like directly a TransactionAbortedException with an inner
710+
// TimeoutException, or a HibernateException encapsulating a TransactionException with a
711+
// timeout, ...
712+
bool isTransactionException, isTimeout;
713+
do
714+
{
715+
isTransactionException = currentEx is System.Transactions.TransactionException;
716+
isTimeout = isTransactionException && currentEx is TransactionAbortedException;
717+
currentEx = currentEx.InnerException;
718+
}
719+
while (!isTransactionException && currentEx != null);
720+
while (!isTimeout && currentEx != null)
721+
{
722+
isTimeout = currentEx is TimeoutException;
723+
currentEx = currentEx?.InnerException;
724+
}
725+
726+
if (!isTimeout)
727+
{
728+
// We may also get a GenericADOException with an InvalidOperationException stating the
729+
// transaction associated to the connection is no more active but not yet suppressed,
730+
// and that for executing some SQL, we need to suppress it. That is a weak way of
731+
// identifying the case, especially with the many localizations of the message.
732+
currentEx = ex;
733+
do
734+
{
735+
isTimeout = currentEx is InvalidOperationException && currentEx.Message.Contains("SQL");
736+
currentEx = currentEx?.InnerException;
737+
}
738+
while (!isTimeout && currentEx != null);
739+
}
740+
741+
if (isTimeout)
742+
timeoutsCount++;
743+
else
744+
throw;
745+
}
746+
*/
700747
}
701748

702-
// Despite the Thread sleep and the count of entities to load, this test may get the timeout only for slightly
703-
// more than 10% of the attempts.
704-
Assert.That(timeoutsCount, Is.GreaterThan(5), "The test should have generated more timeouts.");
705749
Assert.That(
706750
_unhandledExceptions.Count,
707751
Is.EqualTo(0),
708752
"Unhandled exceptions have occurred: {0}",
709753
string.Join(@"
710754
711755
", _unhandledExceptions));
756+
757+
// Despite the Thread sleep and the count of entities to load, this test may get the timeout only for slightly
758+
// more than 10% of the attempts.
759+
Warn.Unless(timeoutsCount, Is.GreaterThan(5), "The test should have generated more timeouts.");
712760
}
713761
finally
714762
{

src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace NHibernate.Test.TestDialects
1+
namespace NHibernate.Test.TestDialects
22
{
33
public class SapSQLAnywhere17TestDialect : TestDialect
44
{
@@ -43,5 +43,11 @@ public SapSQLAnywhere17TestDialect(Dialect.Dialect dialect)
4343
/// Does not support SELECT FOR UPDATE
4444
/// </summary>
4545
public override bool SupportsSelectForUpdate => false;
46+
47+
/// <summary>
48+
/// SQL Anywhere freezes on transaction scope timeout occuring on concurrent threads, always causing the
49+
/// synchronization for end of session processing to timeout.
50+
/// </summary>
51+
public override bool SupportsTransactionScopeTimeouts => false;
4652
}
4753
}

src/NHibernate/Transaction/AdoNetWithSystemTransactionFactory.cs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,9 @@ public virtual void Wait()
243243
// Remove the block then throw.
244244
Unlock();
245245
throw new HibernateException(
246-
$"Synchronization timeout for transaction completion. Either raise" +
247-
$"{Cfg.Environment.SystemTransactionCompletionLockTimeout}, or check all scopes are properly" +
248-
$"disposed and/or all direct System.Transaction.Current changes are properly managed.");
246+
"A synchronization timeout occurred at transaction completion. Either raise " +
247+
$"{Cfg.Environment.SystemTransactionCompletionLockTimeout}, or check all scopes are properly " +
248+
"disposed and/or all direct System.Transaction.Current changes are properly managed.");
249249
}
250250
catch (HibernateException)
251251
{
@@ -452,6 +452,7 @@ protected virtual void CompleteTransaction(bool isCommitted)
452452
// do an early exit here in such case.
453453
if (!IsInActiveTransaction)
454454
return;
455+
var isSessionProcessing = _session.IsProcessing();
455456
try
456457
{
457458
// Allow transaction completed actions to run while others stay blocked.
@@ -460,22 +461,27 @@ protected virtual void CompleteTransaction(bool isCommitted)
460461
// cancelled on a new thread even for non-distributed scopes. So, the session could be doing some processing,
461462
// and will not be interrupted until attempting some usage of the connection. See #3355.
462463
// Thread safety of a concurrent session BeginProcess is ensured by the Wait performed by BeginProcess.
463-
var isProcessing = _session.IsProcessing();
464-
if (isProcessing)
464+
if (isSessionProcessing)
465465
{
466466
var timeOutGuard = new Stopwatch();
467467
timeOutGuard.Start();
468-
while (isProcessing && timeOutGuard.ElapsedMilliseconds < _systemTransactionCompletionLockTimeout)
468+
while (isSessionProcessing && timeOutGuard.ElapsedMilliseconds < _systemTransactionCompletionLockTimeout)
469469
{
470470
// Naïve yield.
471471
Thread.Sleep(10);
472-
isProcessing = _session.IsProcessing();
472+
isSessionProcessing = _session.IsProcessing();
473+
}
474+
if (isSessionProcessing)
475+
{
476+
// Throwing would give up attempting to close the session if need be, which may still succeed. So,
477+
// just log an error.
478+
_logger.Error(
479+
"A synchronization timeout occurred at transaction completion: the session is still processing. Attempting " +
480+
"to finalize the transaction concurrently, which may cause thread safety failure. You may " +
481+
"raise {0} if it is set too low. It may also be a limitation of the data provider, like not " +
482+
"supporting transaction scope timeouts occurring on concurrent threads.",
483+
Cfg.Environment.SystemTransactionCompletionLockTimeout);
473484
}
474-
if (isProcessing)
475-
throw new HibernateException(
476-
$"Synchronization timeout for transaction completion. Either raise" +
477-
$"{Cfg.Environment.SystemTransactionCompletionLockTimeout}, or check all scopes are properly" +
478-
$"disposed and/or all direct System.Transaction.Current changes are properly managed.");
479485
}
480486
using (_session.BeginContext())
481487
{
@@ -516,6 +522,15 @@ protected virtual void CompleteTransaction(bool isCommitted)
516522
// Dispose releases blocked threads by the way.
517523
Dispose();
518524
}
525+
526+
if (isSessionProcessing)
527+
{
528+
throw new HibernateException(
529+
"A synchronization timeout occurred at transaction completion: the session was still processing. You may " +
530+
$"raise {Cfg.Environment.SystemTransactionCompletionLockTimeout} if it is set too low. It may also " +
531+
"be a limitation of the data provider, like not supporting transaction scope timeouts occurring on " +
532+
"concurrent threads.");
533+
}
519534
}
520535

521536
private static void Cleanup(ISessionImplementor session)

0 commit comments

Comments
 (0)