|
19 | 19 | using NHibernate.Engine;
|
20 | 20 | using NHibernate.Test.TransactionTest;
|
21 | 21 | using NUnit.Framework;
|
| 22 | + |
| 23 | +using SysTran = System.Transactions; |
22 | 24 | using NHibernate.Linq;
|
23 | 25 |
|
24 | 26 | namespace NHibernate.Test.SystemTransactions
|
@@ -524,6 +526,116 @@ public async Task EnforceConnectionUsageRulesOnTransactionCompletionAsync()
|
524 | 526 | // Currently always forbidden, whatever UseConnectionOnSystemTransactionEvents.
|
525 | 527 | Assert.That(interceptor.AfterException, Is.TypeOf<HibernateException>());
|
526 | 528 | }
|
| 529 | + |
| 530 | + // This test check a concurrency issue hard to reproduce. If it is flaky, it has to be considered failing. |
| 531 | + // In such case, raise triesCount to investigate it locally with more chances of triggering the trouble. |
| 532 | + [Test] |
| 533 | + public async Task SupportsTransactionTimeoutAsync() |
| 534 | + { |
| 535 | + // Test case adapted from https://github.com/kaksmet/NHibBugRepro |
| 536 | + |
| 537 | + // Create some test data. |
| 538 | + const int entitiesCount = 5000; |
| 539 | + using (var s = OpenSession()) |
| 540 | + using (var t = s.BeginTransaction()) |
| 541 | + { |
| 542 | + for (var i = 0; i < entitiesCount; i++) |
| 543 | + { |
| 544 | + var person = new Person |
| 545 | + { |
| 546 | + NotNullData = Guid.NewGuid().ToString() |
| 547 | + }; |
| 548 | + |
| 549 | + await (s.SaveAsync(person)); |
| 550 | + } |
| 551 | + |
| 552 | + await (t.CommitAsync()); |
| 553 | + } |
| 554 | + |
| 555 | + // Setup unhandler exception catcher |
| 556 | + _unhandledExceptionCount = 0; |
| 557 | + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; |
| 558 | + try |
| 559 | + { |
| 560 | + // Generate transaction timeouts. |
| 561 | + const int triesCount = 100; |
| 562 | + var txOptions = new TransactionOptions { Timeout = TimeSpan.FromMilliseconds(1) }; |
| 563 | + var timeoutsCount = 0; |
| 564 | + for (var i = 0; i < triesCount; i++) |
| 565 | + { |
| 566 | + try |
| 567 | + { |
| 568 | + using var txScope = new TransactionScope(TransactionScopeOption.Required, txOptions, TransactionScopeAsyncFlowOption.Enabled); |
| 569 | + using var session = OpenSession(); |
| 570 | + var data = await (session.CreateCriteria<Person>().ListAsync()); |
| 571 | + Assert.That(data, Has.Count.EqualTo(entitiesCount)); |
| 572 | + await (Task.Delay(2)); |
| 573 | + var count = await (session.Query<Person>().CountAsync()); |
| 574 | + Assert.That(count, Is.EqualTo(entitiesCount)); |
| 575 | + txScope.Complete(); |
| 576 | + } |
| 577 | + catch (Exception ex) |
| 578 | + { |
| 579 | + var currentEx = ex; |
| 580 | + // Depending on where the transaction aborption has broken NHibernate processing, we may |
| 581 | + // get various exceptions, like directly a TransactionAbortedException with an inner |
| 582 | + // TimeoutException, or a HibernateException encapsulating a TransactionException with a |
| 583 | + // timeout, ... |
| 584 | + bool isTransactionException; |
| 585 | + do |
| 586 | + { |
| 587 | + isTransactionException = currentEx is SysTran.TransactionException; |
| 588 | + currentEx = currentEx.InnerException; |
| 589 | + } |
| 590 | + while (!isTransactionException && currentEx != null); |
| 591 | + bool isTimeout; |
| 592 | + do |
| 593 | + { |
| 594 | + isTimeout = currentEx is TimeoutException; |
| 595 | + currentEx = currentEx?.InnerException; |
| 596 | + } |
| 597 | + while (!isTimeout && currentEx != null); |
| 598 | + |
| 599 | + if (!isTimeout) |
| 600 | + { |
| 601 | + // We may also get a GenericADOException with an InvalidOperationException stating the |
| 602 | + // transaction associated to the connection is no more active but not yet suppressed, |
| 603 | + // and that for executing some SQL, we need to suppress it. That is a weak way of |
| 604 | + // identifying the case, especially with the possibility of localization of the message. |
| 605 | + currentEx = ex; |
| 606 | + do |
| 607 | + { |
| 608 | + isTimeout = currentEx is InvalidOperationException && currentEx.Message.Contains("SQL"); |
| 609 | + currentEx = currentEx?.InnerException; |
| 610 | + } |
| 611 | + while (!isTimeout && currentEx != null); |
| 612 | + } |
| 613 | + |
| 614 | + if (isTimeout) |
| 615 | + timeoutsCount++; |
| 616 | + else |
| 617 | + throw; |
| 618 | + } |
| 619 | + } |
| 620 | + |
| 621 | + // Despite the Thread sleep and the count of entities to load, this test does get the timeout only for slightly |
| 622 | + // more than 10% of the attempts. |
| 623 | + Assert.That(timeoutsCount, Is.GreaterThan(5)); |
| 624 | + Assert.That(_unhandledExceptionCount, Is.EqualTo(0)); |
| 625 | + } |
| 626 | + finally |
| 627 | + { |
| 628 | + AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; |
| 629 | + } |
| 630 | + } |
| 631 | + |
| 632 | + private int _unhandledExceptionCount; |
| 633 | + |
| 634 | + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) |
| 635 | + { |
| 636 | + _unhandledExceptionCount++; |
| 637 | + Assert.Warn("Unhandled exception: {0}", e.ExceptionObject); |
| 638 | + } |
527 | 639 | }
|
528 | 640 |
|
529 | 641 | [TestFixture]
|
|
0 commit comments