Skip to content

Commit 18caed2

Browse files
Fix dependent transaction failure (#2197)
Fix #2172
1 parent 3298f99 commit 18caed2

10 files changed

+305
-14
lines changed

src/AsyncGenerator.yml

+12
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,18 @@
183183
applyChanges: true
184184
analyzation:
185185
methodConversion:
186+
- conversion: Ignore
187+
name: CanUseDependentTransaction
188+
containingTypeName: DistributedSystemTransactionFixture
189+
- conversion: Ignore
190+
name: CanUseSessionWithManyDependentTransaction
191+
containingTypeName: DistributedSystemTransactionFixture
192+
- conversion: Ignore
193+
name: CanUseDependentTransaction
194+
containingTypeName: SystemTransactionFixture
195+
- conversion: Ignore
196+
name: CanUseSessionWithManyDependentTransaction
197+
containingTypeName: SystemTransactionFixture
186198
- conversion: Copy
187199
name: AfterTransactionCompletionProcess_EvictsFromCache
188200
- conversion: Copy

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
using System.Threading;
1414
using System.Transactions;
1515
using log4net;
16-
using log4net.Repository.Hierarchy;
1716
using NHibernate.Cfg;
1817
using NHibernate.Engine;
19-
using NHibernate.Linq;
2018
using NHibernate.Test.TransactionTest;
2119
using NUnit.Framework;
20+
using NHibernate.Linq;
2221

2322
namespace NHibernate.Test.SystemTransactions
2423
{

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
using NHibernate.Cfg;
1717
using NHibernate.Driver;
1818
using NHibernate.Engine;
19-
using NHibernate.Linq;
2019
using NHibernate.Test.TransactionTest;
2120
using NUnit.Framework;
21+
using NHibernate.Linq;
2222

2323
namespace NHibernate.Test.SystemTransactions
2424
{

src/NHibernate.Test/SystemTransactions/DistributedSystemTransactionFixture.cs

+111-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
using System.Threading;
44
using System.Transactions;
55
using log4net;
6-
using log4net.Repository.Hierarchy;
76
using NHibernate.Cfg;
87
using NHibernate.Engine;
9-
using NHibernate.Linq;
108
using NHibernate.Test.TransactionTest;
119
using NUnit.Framework;
1210

@@ -711,6 +709,117 @@ public void AdditionalJoinDoesNotThrow()
711709
}
712710
}
713711

712+
[Theory]
713+
public void CanUseDependentTransaction(bool explicitFlush)
714+
{
715+
if (!TestDialect.SupportsDependentTransaction)
716+
Assert.Ignore("Dialect does not support dependent transactions");
717+
IgnoreIfUnsupported(explicitFlush);
718+
719+
try
720+
{
721+
using (var committable = new CommittableTransaction())
722+
{
723+
System.Transactions.Transaction.Current = committable;
724+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
725+
{
726+
System.Transactions.Transaction.Current = clone;
727+
728+
using (var s = OpenSession())
729+
{
730+
if (!AutoJoinTransaction)
731+
s.JoinTransaction();
732+
s.Save(new Person());
733+
734+
if (explicitFlush)
735+
s.Flush();
736+
clone.Complete();
737+
}
738+
}
739+
740+
System.Transactions.Transaction.Current = committable;
741+
committable.Commit();
742+
}
743+
}
744+
finally
745+
{
746+
System.Transactions.Transaction.Current = null;
747+
}
748+
}
749+
750+
[Theory]
751+
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
752+
{
753+
if (!TestDialect.SupportsDependentTransaction)
754+
Assert.Ignore("Dialect does not support dependent transactions");
755+
IgnoreIfUnsupported(explicitFlush);
756+
757+
try
758+
{
759+
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
760+
{
761+
using (var committable = new CommittableTransaction())
762+
{
763+
System.Transactions.Transaction.Current = committable;
764+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
765+
{
766+
System.Transactions.Transaction.Current = clone;
767+
if (!AutoJoinTransaction)
768+
s.JoinTransaction();
769+
// Acquire the connection
770+
var count = s.Query<Person>().Count();
771+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
772+
clone.Complete();
773+
}
774+
775+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
776+
{
777+
System.Transactions.Transaction.Current = clone;
778+
if (!AutoJoinTransaction)
779+
s.JoinTransaction();
780+
s.Save(new Person());
781+
782+
if (explicitFlush)
783+
s.Flush();
784+
785+
clone.Complete();
786+
}
787+
788+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
789+
{
790+
System.Transactions.Transaction.Current = clone;
791+
if (!AutoJoinTransaction)
792+
s.JoinTransaction();
793+
var count = s.Query<Person>().Count();
794+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
795+
clone.Complete();
796+
}
797+
798+
System.Transactions.Transaction.Current = committable;
799+
committable.Commit();
800+
}
801+
}
802+
}
803+
finally
804+
{
805+
System.Transactions.Transaction.Current = null;
806+
}
807+
808+
DodgeTransactionCompletionDelayIfRequired();
809+
810+
using (var s = OpenSession())
811+
{
812+
using (var tx = new TransactionScope())
813+
{
814+
if (!AutoJoinTransaction)
815+
s.JoinTransaction();
816+
var count = s.Query<Person>().Count();
817+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
818+
tx.Complete();
819+
}
820+
}
821+
}
822+
714823
private void DodgeTransactionCompletionDelayIfRequired()
715824
{
716825
if (Sfi.ConnectionProvider.Driver.HasDelayedDistributedTransactionCompletion)

src/NHibernate.Test/SystemTransactions/SystemTransactionFixture.cs

+120-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using NHibernate.Cfg;
77
using NHibernate.Driver;
88
using NHibernate.Engine;
9-
using NHibernate.Linq;
109
using NHibernate.Test.TransactionTest;
1110
using NUnit.Framework;
1211

@@ -522,6 +521,126 @@ public void AdditionalJoinDoesNotThrow()
522521
Assert.DoesNotThrow(() => s.JoinTransaction());
523522
}
524523
}
524+
525+
[Theory]
526+
public void CanUseDependentTransaction(bool explicitFlush)
527+
{
528+
if (!TestDialect.SupportsDependentTransaction)
529+
Assert.Ignore("Dialect does not support dependent transactions");
530+
IgnoreIfUnsupported(explicitFlush);
531+
532+
try
533+
{
534+
using (var committable = new CommittableTransaction())
535+
{
536+
System.Transactions.Transaction.Current = committable;
537+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
538+
{
539+
System.Transactions.Transaction.Current = clone;
540+
541+
using (var s = OpenSession())
542+
{
543+
if (!AutoJoinTransaction)
544+
s.JoinTransaction();
545+
s.Save(new Person());
546+
547+
if (explicitFlush)
548+
s.Flush();
549+
clone.Complete();
550+
}
551+
}
552+
553+
System.Transactions.Transaction.Current = committable;
554+
committable.Commit();
555+
}
556+
}
557+
finally
558+
{
559+
System.Transactions.Transaction.Current = null;
560+
}
561+
}
562+
563+
[Theory]
564+
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
565+
{
566+
if (!TestDialect.SupportsDependentTransaction)
567+
Assert.Ignore("Dialect does not support dependent transactions");
568+
IgnoreIfUnsupported(explicitFlush);
569+
// ODBC with SQL-Server always causes system transactions to go distributed, which causes their transaction completion to run
570+
// asynchronously. But ODBC enlistment also check the previous transaction in a way that do not guard against it
571+
// being concurrently disposed of. See https://github.com/nhibernate/nhibernate-core/pull/1505 for more details.
572+
if (Sfi.ConnectionProvider.Driver is OdbcDriver)
573+
Assert.Ignore("ODBC sometimes fails on second scope by checking the previous transaction status, which may yield an object disposed exception");
574+
// SAP HANA & SQL Anywhere .Net providers always cause system transactions to be distributed, causing them to
575+
// complete on concurrent threads. This creates race conditions when chaining scopes, the subsequent scope usage
576+
// finding the connection still enlisted in the previous transaction, its complete being still not finished
577+
// on its own thread.
578+
if (Sfi.ConnectionProvider.Driver is HanaDriverBase || Sfi.ConnectionProvider.Driver is SapSQLAnywhere17Driver)
579+
Assert.Ignore("SAP HANA and SQL Anywhere scope handling causes concurrency issues preventing chaining scope usages.");
580+
581+
try
582+
{
583+
using (var s = WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
584+
{
585+
using (var committable = new CommittableTransaction())
586+
{
587+
System.Transactions.Transaction.Current = committable;
588+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
589+
{
590+
System.Transactions.Transaction.Current = clone;
591+
if (!AutoJoinTransaction)
592+
s.JoinTransaction();
593+
// Acquire the connection
594+
var count = s.Query<Person>().Count();
595+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
596+
clone.Complete();
597+
}
598+
599+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
600+
{
601+
System.Transactions.Transaction.Current = clone;
602+
if (!AutoJoinTransaction)
603+
s.JoinTransaction();
604+
s.Save(new Person());
605+
606+
if (explicitFlush)
607+
s.Flush();
608+
609+
clone.Complete();
610+
}
611+
612+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
613+
{
614+
System.Transactions.Transaction.Current = clone;
615+
if (!AutoJoinTransaction)
616+
s.JoinTransaction();
617+
var count = s.Query<Person>().Count();
618+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
619+
clone.Complete();
620+
}
621+
622+
System.Transactions.Transaction.Current = committable;
623+
committable.Commit();
624+
}
625+
}
626+
}
627+
finally
628+
{
629+
System.Transactions.Transaction.Current = null;
630+
}
631+
632+
using (var s = OpenSession())
633+
{
634+
using (var tx = new TransactionScope())
635+
{
636+
if (!AutoJoinTransaction)
637+
s.JoinTransaction();
638+
var count = s.Query<Person>().Count();
639+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
640+
tx.Complete();
641+
}
642+
}
643+
}
525644
}
526645

527646
[TestFixture]

src/NHibernate.Test/TestDialect.cs

+8
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ public bool SupportsSqlType(SqlType sqlType)
157157
/// </summary>
158158
public virtual bool SupportsUsingConnectionOnSystemTransactionPrepare => true;
159159

160+
/// <summary>
161+
/// Some databases fail with dependent transaction, typically when their driver tries to access the transaction
162+
/// state from its two PC: the dependent transaction is meant to be disposed of before completing the actual
163+
/// transaction, so it is usually disposed at this point, and its state cannot be read. (Drivers should always
164+
/// clone transactions for avoiding this trouble.)
165+
/// </summary>
166+
public virtual bool SupportsDependentTransaction => true;
167+
160168
/// <summary>
161169
/// Some databases (provider?) fails to compute adequate column types for queries which columns
162170
/// computing include a parameter value.

src/NHibernate.Test/TestDialects/PostgreSQL83TestDialect.cs

+6
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,11 @@ public PostgreSQL83TestDialect(Dialect.Dialect dialect)
1616
/// Npgsql 3.2.4.1.
1717
/// </summary>
1818
public override bool SupportsUsingConnectionOnSystemTransactionPrepare => false;
19+
20+
/// <summary>
21+
/// Npgsql does not clone the transaction in its context, and uses it in its prepare phase. When that was a
22+
/// dependent transaction, it is then usually already disposed of, causing Npgsql to crash.
23+
/// </summary>
24+
public override bool SupportsDependentTransaction => false;
1925
}
2026
}

src/NHibernate/AdoNet/ConnectionManager.cs

+6
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,13 @@ public DbCommand CreateCommand()
456456
public void EnlistIfRequired(System.Transactions.Transaction transaction)
457457
{
458458
if (transaction == _currentSystemTransaction)
459+
{
460+
// Short-circuit after having stored the transaction : they may be equal, but not the same reference.
461+
// And the previous one may be an already disposed dependent clone, in which case we need to update
462+
// our reference.
463+
_currentSystemTransaction = transaction;
459464
return;
465+
}
460466

461467
_currentSystemTransaction = transaction;
462468

src/NHibernate/Async/Transaction/AdoNetWithSystemTransactionFactory.cs

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
using NHibernate.AdoNet;
1717
using NHibernate.Engine;
1818
using NHibernate.Engine.Transaction;
19-
using NHibernate.Impl;
2019
using NHibernate.Util;
2120

2221
namespace NHibernate.Transaction

0 commit comments

Comments
 (0)