9
9
10
10
11
11
using System ;
12
+ using System . Collections . Concurrent ;
12
13
using System . Collections . Generic ;
13
14
using System . Diagnostics ;
14
15
using System . Linq ;
@@ -30,6 +31,13 @@ public class SystemTransactionFixtureAsync : SystemTransactionFixtureBase
30
31
protected override bool UseConnectionOnSystemTransactionPrepare => true ;
31
32
protected override bool AutoJoinTransaction => true ;
32
33
34
+ protected override void OnTearDown ( )
35
+ {
36
+ base . OnTearDown ( ) ;
37
+ // The SupportsTransactionTimeout test may change this, restore it to its default value.
38
+ FailOnNotClosedSession = true ;
39
+ }
40
+
33
41
[ Test ]
34
42
public async Task WillNotCrashOnPrepareFailureAsync ( )
35
43
{
@@ -524,6 +532,152 @@ public async Task EnforceConnectionUsageRulesOnTransactionCompletionAsync()
524
532
// Currently always forbidden, whatever UseConnectionOnSystemTransactionEvents.
525
533
Assert . That ( interceptor . AfterException , Is . TypeOf < HibernateException > ( ) ) ;
526
534
}
535
+
536
+ // This test check a concurrency issue hard to reproduce. If it is flaky, it has to be considered failing.
537
+ // In such case, raise triesCount to investigate it locally with more chances of triggering the trouble.
538
+ [ Test ]
539
+ public async Task SupportsTransactionTimeoutAsync ( )
540
+ {
541
+ Assume . That ( TestDialect . SupportsTransactionScopeTimeouts , Is . True , "The tested dialect is not supported for transaction scope timeouts." ) ;
542
+ // Other special cases: ODBC and SAP SQL Anywhere succeed this test only with transaction.ignore_session_synchronization_failures
543
+ // enabled.
544
+ // They freeze the session during the transaction cancellation. To avoid the test to be very long, the synchronization
545
+ // lock timeout should be lowered too.
546
+
547
+ // A concurrency issue exists with the legacy setting allowing to use the session from transaction completion, which
548
+ // may cause session leaks. Ignore them.
549
+ FailOnNotClosedSession = ! UseConnectionOnSystemTransactionPrepare ;
550
+
551
+ // Test case adapted from https://github.com/kaksmet/NHibBugRepro
552
+
553
+ // Create some test data.
554
+ const int entitiesCount = 5000 ;
555
+ using ( var s = OpenSession ( ) )
556
+ using ( var t = s . BeginTransaction ( ) )
557
+ {
558
+ for ( var i = 0 ; i < entitiesCount ; i ++ )
559
+ {
560
+ var person = new Person
561
+ {
562
+ NotNullData = Guid . NewGuid ( ) . ToString ( )
563
+ } ;
564
+
565
+ await ( s . SaveAsync ( person ) ) ;
566
+ }
567
+
568
+ await ( t . CommitAsync ( ) ) ;
569
+ }
570
+
571
+ // Setup unhandled exception catcher.
572
+ _unhandledExceptions = new ConcurrentBag < object > ( ) ;
573
+ AppDomain . CurrentDomain . UnhandledException += CurrentDomain_UnhandledException ;
574
+ try
575
+ {
576
+ // Generate transaction timeouts.
577
+ const int triesCount = 100 ;
578
+ var txOptions = new TransactionOptions { Timeout = TimeSpan . FromMilliseconds ( 1 ) } ;
579
+ var timeoutsCount = 0 ;
580
+ for ( var i = 0 ; i < triesCount ; i ++ )
581
+ {
582
+ try
583
+ {
584
+ using var txScope = new TransactionScope ( TransactionScopeOption . Required , txOptions , TransactionScopeAsyncFlowOption . Enabled ) ;
585
+ using var session = OpenSession ( ) ;
586
+ var data = await ( session . CreateCriteria < Person > ( ) . ListAsync ( ) ) ;
587
+ Assert . That ( data , Has . Count . EqualTo ( entitiesCount ) , "Unexpected count of loaded entities." ) ;
588
+ await ( Task . Delay ( 2 ) ) ;
589
+ var count = await ( session . Query < Person > ( ) . CountAsync ( ) ) ;
590
+ Assert . That ( count , Is . EqualTo ( entitiesCount ) , "Unexpected entities count." ) ;
591
+ txScope . Complete ( ) ;
592
+ }
593
+ catch
594
+ {
595
+ // Assume that is a transaction timeout. It may cause various failures, of which some are hard to identify.
596
+ timeoutsCount ++ ;
597
+ }
598
+ // If in need of checking some specific failures, the following code may be used instead:
599
+ /*
600
+ catch (Exception ex)
601
+ {
602
+ var currentEx = ex;
603
+ // Depending on where the transaction aborption has broken NHibernate processing, we may
604
+ // get various exceptions, like directly a TransactionAbortedException with an inner
605
+ // TimeoutException, or a HibernateException encapsulating a TransactionException with a
606
+ // timeout, ...
607
+ bool isTransactionException, isTimeout;
608
+ do
609
+ {
610
+ isTransactionException = currentEx is System.Transactions.TransactionException;
611
+ isTimeout = isTransactionException && currentEx is TransactionAbortedException;
612
+ currentEx = currentEx.InnerException;
613
+ }
614
+ while (!isTransactionException && currentEx != null);
615
+ while (!isTimeout && currentEx != null)
616
+ {
617
+ isTimeout = currentEx is TimeoutException;
618
+ currentEx = currentEx?.InnerException;
619
+ }
620
+
621
+ if (!isTimeout)
622
+ {
623
+ // We may also get a GenericADOException with an InvalidOperationException stating the
624
+ // transaction associated to the connection is no more active but not yet suppressed,
625
+ // and that for executing some SQL, we need to suppress it. That is a weak way of
626
+ // identifying the case, especially with the many localizations of the message.
627
+ currentEx = ex;
628
+ do
629
+ {
630
+ isTimeout = currentEx is InvalidOperationException && currentEx.Message.Contains("SQL");
631
+ currentEx = currentEx?.InnerException;
632
+ }
633
+ while (!isTimeout && currentEx != null);
634
+ }
635
+
636
+ if (isTimeout)
637
+ timeoutsCount++;
638
+ else
639
+ throw;
640
+ }
641
+ */
642
+ }
643
+
644
+ Assert . That (
645
+ _unhandledExceptions . Count ,
646
+ Is . EqualTo ( 0 ) ,
647
+ "Unhandled exceptions have occurred: {0}" ,
648
+ string . Join ( @"
649
+
650
+ " , _unhandledExceptions ) ) ;
651
+
652
+ // Despite the Thread sleep and the count of entities to load, this test may get the timeout only for slightly
653
+ // more than 10% of the attempts.
654
+ Warn . Unless ( timeoutsCount , Is . GreaterThan ( 5 ) , "The test should have generated more timeouts." ) ;
655
+ }
656
+ finally
657
+ {
658
+ AppDomain . CurrentDomain . UnhandledException -= CurrentDomain_UnhandledException ;
659
+ }
660
+ }
661
+
662
+ private ConcurrentBag < object > _unhandledExceptions ;
663
+
664
+ private void CurrentDomain_UnhandledException ( object sender , UnhandledExceptionEventArgs e )
665
+ {
666
+ if ( e . ExceptionObject is Exception exception )
667
+ {
668
+ // Ascertain NHibernate is involved. Some unhandled exceptions occur due to the
669
+ // TransactionScope timeout operating on an unexpected thread for the data provider.
670
+ var isNHibernateInvolved = false ;
671
+ while ( exception != null && ! isNHibernateInvolved )
672
+ {
673
+ isNHibernateInvolved = exception . StackTrace != null && exception . StackTrace . ToLowerInvariant ( ) . Contains ( "nhibernate" ) ;
674
+ exception = exception . InnerException ;
675
+ }
676
+ if ( ! isNHibernateInvolved )
677
+ return ;
678
+ }
679
+ _unhandledExceptions . Add ( e . ExceptionObject ) ;
680
+ }
527
681
}
528
682
529
683
[ TestFixture ]
0 commit comments