Skip to content

Commit ab3507a

Browse files
fixup! Fix dependent transaction failure
Fix chaining dependent transaction usage on the same session
1 parent e8d2f12 commit ab3507a

File tree

5 files changed

+339
-2
lines changed

5 files changed

+339
-2
lines changed

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,79 @@ public async Task CanUseDependentTransactionAsync(bool explicitFlush)
722722
sysTran.Transaction.Current = null;
723723
}
724724

725+
[Theory]
726+
public async Task CanUseSessionWithManyDependentTransactionAsync(bool explicitFlush)
727+
{
728+
if (!TestDialect.SupportsDependentTransaction)
729+
Assert.Ignore("Dialect does not support dependent transactions");
730+
IgnoreIfUnsupported(explicitFlush);
731+
732+
try
733+
{
734+
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
735+
{
736+
using (var committable = new CommittableTransaction())
737+
{
738+
sysTran.Transaction.Current = committable;
739+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
740+
{
741+
sysTran.Transaction.Current = clone;
742+
if (!AutoJoinTransaction)
743+
s.JoinTransaction();
744+
// Acquire the connection
745+
var count = await (s.Query<Person>().CountAsync());
746+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
747+
clone.Complete();
748+
}
749+
750+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
751+
{
752+
sysTran.Transaction.Current = clone;
753+
if (!AutoJoinTransaction)
754+
s.JoinTransaction();
755+
await (s.SaveAsync(new Person()));
756+
757+
if (explicitFlush)
758+
await (s.FlushAsync());
759+
760+
clone.Complete();
761+
}
762+
763+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
764+
{
765+
sysTran.Transaction.Current = clone;
766+
if (!AutoJoinTransaction)
767+
s.JoinTransaction();
768+
var count = await (s.Query<Person>().CountAsync());
769+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
770+
clone.Complete();
771+
}
772+
773+
sysTran.Transaction.Current = committable;
774+
committable.Commit();
775+
}
776+
}
777+
}
778+
finally
779+
{
780+
sysTran.Transaction.Current = null;
781+
}
782+
783+
await (DodgeTransactionCompletionDelayIfRequiredAsync());
784+
785+
using (var s = OpenSession())
786+
{
787+
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
788+
{
789+
if (!AutoJoinTransaction)
790+
s.JoinTransaction();
791+
var count = await (s.Query<Person>().CountAsync());
792+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
793+
tx.Complete();
794+
}
795+
}
796+
}
797+
725798
private Task DodgeTransactionCompletionDelayIfRequiredAsync(CancellationToken cancellationToken = default(CancellationToken))
726799
{
727800
try

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,88 @@ public async Task CanUseDependentTransactionAsync(bool explicitFlush)
562562
sysTran.Transaction.Current = null;
563563
}
564564
}
565+
566+
[Theory]
567+
public async Task CanUseSessionWithManyDependentTransactionAsync(bool explicitFlush)
568+
{
569+
if (!TestDialect.SupportsDependentTransaction)
570+
Assert.Ignore("Dialect does not support dependent transactions");
571+
IgnoreIfUnsupported(explicitFlush);
572+
// ODBC with SQL-Server always causes system transactions to go distributed, which causes their transaction completion to run
573+
// asynchronously. But ODBC enlistment also check the previous transaction in a way that do not guard against it
574+
// being concurrently disposed of. See https://github.com/nhibernate/nhibernate-core/pull/1505 for more details.
575+
if (Sfi.ConnectionProvider.Driver is OdbcDriver)
576+
Assert.Ignore("ODBC sometimes fails on second scope by checking the previous transaction status, which may yield an object disposed exception");
577+
// SAP HANA & SQL Anywhere .Net providers always cause system transactions to be distributed, causing them to
578+
// complete on concurrent threads. This creates race conditions when chaining scopes, the subsequent scope usage
579+
// finding the connection still enlisted in the previous transaction, its complete being still not finished
580+
// on its own thread.
581+
if (Sfi.ConnectionProvider.Driver is HanaDriverBase || Sfi.ConnectionProvider.Driver is SapSQLAnywhere17Driver)
582+
Assert.Ignore("SAP HANA and SQL Anywhere scope handling causes concurrency issues preventing chaining scope usages.");
583+
584+
try
585+
{
586+
using (var s = WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
587+
{
588+
using (var committable = new CommittableTransaction())
589+
{
590+
sysTran.Transaction.Current = committable;
591+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
592+
{
593+
sysTran.Transaction.Current = clone;
594+
if (!AutoJoinTransaction)
595+
s.JoinTransaction();
596+
// Acquire the connection
597+
var count = await (s.Query<Person>().CountAsync());
598+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
599+
clone.Complete();
600+
}
601+
602+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
603+
{
604+
sysTran.Transaction.Current = clone;
605+
if (!AutoJoinTransaction)
606+
s.JoinTransaction();
607+
await (s.SaveAsync(new Person()));
608+
609+
if (explicitFlush)
610+
await (s.FlushAsync());
611+
612+
clone.Complete();
613+
}
614+
615+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
616+
{
617+
sysTran.Transaction.Current = clone;
618+
if (!AutoJoinTransaction)
619+
s.JoinTransaction();
620+
var count = await (s.Query<Person>().CountAsync());
621+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
622+
clone.Complete();
623+
}
624+
625+
sysTran.Transaction.Current = committable;
626+
committable.Commit();
627+
}
628+
}
629+
}
630+
finally
631+
{
632+
sysTran.Transaction.Current = null;
633+
}
634+
635+
using (var s = OpenSession())
636+
{
637+
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
638+
{
639+
if (!AutoJoinTransaction)
640+
s.JoinTransaction();
641+
var count = await (s.Query<Person>().CountAsync());
642+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
643+
tx.Complete();
644+
}
645+
}
646+
}
565647
}
566648

567649
[TestFixture]

src/NHibernate.Test/SystemTransactions/DistributedSystemTransactionFixture.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,79 @@ public void CanUseDependentTransaction(bool explicitFlush)
744744
sysTran.Transaction.Current = null;
745745
}
746746

747+
[Theory]
748+
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
749+
{
750+
if (!TestDialect.SupportsDependentTransaction)
751+
Assert.Ignore("Dialect does not support dependent transactions");
752+
IgnoreIfUnsupported(explicitFlush);
753+
754+
try
755+
{
756+
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
757+
{
758+
using (var committable = new CommittableTransaction())
759+
{
760+
sysTran.Transaction.Current = committable;
761+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
762+
{
763+
sysTran.Transaction.Current = clone;
764+
if (!AutoJoinTransaction)
765+
s.JoinTransaction();
766+
// Acquire the connection
767+
var count = s.Query<Person>().Count();
768+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
769+
clone.Complete();
770+
}
771+
772+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
773+
{
774+
sysTran.Transaction.Current = clone;
775+
if (!AutoJoinTransaction)
776+
s.JoinTransaction();
777+
s.Save(new Person());
778+
779+
if (explicitFlush)
780+
s.Flush();
781+
782+
clone.Complete();
783+
}
784+
785+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
786+
{
787+
sysTran.Transaction.Current = clone;
788+
if (!AutoJoinTransaction)
789+
s.JoinTransaction();
790+
var count = s.Query<Person>().Count();
791+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
792+
clone.Complete();
793+
}
794+
795+
sysTran.Transaction.Current = committable;
796+
committable.Commit();
797+
}
798+
}
799+
}
800+
finally
801+
{
802+
sysTran.Transaction.Current = null;
803+
}
804+
805+
DodgeTransactionCompletionDelayIfRequired();
806+
807+
using (var s = OpenSession())
808+
{
809+
using (var tx = new TransactionScope())
810+
{
811+
if (!AutoJoinTransaction)
812+
s.JoinTransaction();
813+
var count = s.Query<Person>().Count();
814+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
815+
tx.Complete();
816+
}
817+
}
818+
}
819+
747820
private void DodgeTransactionCompletionDelayIfRequired()
748821
{
749822
if (Sfi.ConnectionProvider.Driver.HasDelayedDistributedTransactionCompletion)

src/NHibernate.Test/SystemTransactions/SystemTransactionFixture.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,88 @@ public void CanUseDependentTransaction(bool explicitFlush)
560560
sysTran.Transaction.Current = null;
561561
}
562562
}
563+
564+
[Theory]
565+
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
566+
{
567+
if (!TestDialect.SupportsDependentTransaction)
568+
Assert.Ignore("Dialect does not support dependent transactions");
569+
IgnoreIfUnsupported(explicitFlush);
570+
// ODBC with SQL-Server always causes system transactions to go distributed, which causes their transaction completion to run
571+
// asynchronously. But ODBC enlistment also check the previous transaction in a way that do not guard against it
572+
// being concurrently disposed of. See https://github.com/nhibernate/nhibernate-core/pull/1505 for more details.
573+
if (Sfi.ConnectionProvider.Driver is OdbcDriver)
574+
Assert.Ignore("ODBC sometimes fails on second scope by checking the previous transaction status, which may yield an object disposed exception");
575+
// SAP HANA & SQL Anywhere .Net providers always cause system transactions to be distributed, causing them to
576+
// complete on concurrent threads. This creates race conditions when chaining scopes, the subsequent scope usage
577+
// finding the connection still enlisted in the previous transaction, its complete being still not finished
578+
// on its own thread.
579+
if (Sfi.ConnectionProvider.Driver is HanaDriverBase || Sfi.ConnectionProvider.Driver is SapSQLAnywhere17Driver)
580+
Assert.Ignore("SAP HANA and SQL Anywhere scope handling causes concurrency issues preventing chaining scope usages.");
581+
582+
try
583+
{
584+
using (var s = WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
585+
{
586+
using (var committable = new CommittableTransaction())
587+
{
588+
sysTran.Transaction.Current = committable;
589+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
590+
{
591+
sysTran.Transaction.Current = clone;
592+
if (!AutoJoinTransaction)
593+
s.JoinTransaction();
594+
// Acquire the connection
595+
var count = s.Query<Person>().Count();
596+
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
597+
clone.Complete();
598+
}
599+
600+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
601+
{
602+
sysTran.Transaction.Current = clone;
603+
if (!AutoJoinTransaction)
604+
s.JoinTransaction();
605+
s.Save(new Person());
606+
607+
if (explicitFlush)
608+
s.Flush();
609+
610+
clone.Complete();
611+
}
612+
613+
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
614+
{
615+
sysTran.Transaction.Current = clone;
616+
if (!AutoJoinTransaction)
617+
s.JoinTransaction();
618+
var count = s.Query<Person>().Count();
619+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
620+
clone.Complete();
621+
}
622+
623+
sysTran.Transaction.Current = committable;
624+
committable.Commit();
625+
}
626+
}
627+
}
628+
finally
629+
{
630+
sysTran.Transaction.Current = null;
631+
}
632+
633+
using (var s = OpenSession())
634+
{
635+
using (var tx = new TransactionScope())
636+
{
637+
if (!AutoJoinTransaction)
638+
s.JoinTransaction();
639+
var count = s.Query<Person>().Count();
640+
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
641+
tx.Complete();
642+
}
643+
}
644+
}
563645
}
564646

565647
[TestFixture]

src/NHibernate/Transaction/AdoNetWithSystemTransactionFactory.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,41 @@ protected virtual void Unlock()
288288
if (status != TransactionStatus.Active || _preparing)
289289
return status;
290290

291-
// The clone status can be out of date when active and not in prepare phase, in case of rollback.
291+
// The clone status can be out of date when active and not in prepare phase, in case of rollback or
292+
// dependent clone usage.
292293
// In such case the original transaction is already disposed, and trying to check its status will
293294
// trigger a dispose exception.
294295
return _originalTransaction.TransactionInformation.Status;
295296
}
296297
catch (ObjectDisposedException ode)
297298
{
298-
_logger.Warn(ode, "Enlisted transaction status is maybe wrongly active, original transaction being already disposed. Will assume neither active nor committed.");
299+
// For ruling out the dependent clone case when possible, we check if the current transaction is
300+
// equal to the context one (System.Transactions.Transaction does override equality for this), and
301+
// in such case, we check the state of the current transaction instead. (The state of the current
302+
// transaction if equal can only be the same, but it will be inaccessible in case of rollback, due
303+
// to the current transaction being already disposed.)
304+
// The current transaction may not be reachable during 2PC phases and transaction completion events,
305+
// but in such cases the context transaction is either no more active or in prepare phase, which is
306+
// already covered by _preparing test.
307+
try
308+
{
309+
var currentTransaction = System.Transactions.Transaction.Current;
310+
if (!ReferenceEquals(currentTransaction, _originalTransaction) &&
311+
currentTransaction == EnlistedTransaction)
312+
return currentTransaction.TransactionInformation.Status;
313+
}
314+
catch (ObjectDisposedException)
315+
{
316+
// Just ignore that one, no use to log two dispose exceptions which are indeed the same.
317+
}
318+
catch (InvalidOperationException ioe)
319+
{
320+
_logger.Warn(ioe, "Attempting to dodge a disposed transaction trouble, current" +
321+
"transaction was unreachable.");
322+
}
323+
324+
_logger.Warn(ode, "Enlisted transaction status is maybe wrongly active, original " +
325+
"transaction being already disposed. Will assume neither active nor committed.");
299326
return null;
300327
}
301328
}

0 commit comments

Comments
 (0)