Skip to content

Fix dependent transaction failure #2197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/AsyncGenerator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@
applyChanges: true
analyzation:
methodConversion:
- conversion: Ignore
name: CanUseDependentTransaction
containingTypeName: DistributedSystemTransactionFixture
- conversion: Ignore
name: CanUseSessionWithManyDependentTransaction
containingTypeName: DistributedSystemTransactionFixture
- conversion: Ignore
name: CanUseDependentTransaction
containingTypeName: SystemTransactionFixture
- conversion: Ignore
name: CanUseSessionWithManyDependentTransaction
containingTypeName: SystemTransactionFixture
- conversion: Copy
name: AfterTransactionCompletionProcess_EvictsFromCache
- conversion: Copy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
using System.Threading;
using System.Transactions;
using log4net;
using log4net.Repository.Hierarchy;
using NHibernate.Cfg;
using NHibernate.Engine;
using NHibernate.Linq;
using NHibernate.Test.TransactionTest;
using NUnit.Framework;
using NHibernate.Linq;

namespace NHibernate.Test.SystemTransactions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
using NHibernate.Cfg;
using NHibernate.Driver;
using NHibernate.Engine;
using NHibernate.Linq;
using NHibernate.Test.TransactionTest;
using NUnit.Framework;
using NHibernate.Linq;

namespace NHibernate.Test.SystemTransactions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
using System.Threading;
using System.Transactions;
using log4net;
using log4net.Repository.Hierarchy;
using NHibernate.Cfg;
using NHibernate.Engine;
using NHibernate.Linq;
using NHibernate.Test.TransactionTest;
using NUnit.Framework;

Expand Down Expand Up @@ -711,6 +709,117 @@ public void AdditionalJoinDoesNotThrow()
}
}

[Theory]
public void CanUseDependentTransaction(bool explicitFlush)
{
if (!TestDialect.SupportsDependentTransaction)
Assert.Ignore("Dialect does not support dependent transactions");
IgnoreIfUnsupported(explicitFlush);

try
{
using (var committable = new CommittableTransaction())
{
System.Transactions.Transaction.Current = committable;
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;

using (var s = OpenSession())
{
if (!AutoJoinTransaction)
s.JoinTransaction();
s.Save(new Person());

if (explicitFlush)
s.Flush();
clone.Complete();
}
}

System.Transactions.Transaction.Current = committable;
committable.Commit();
}
}
finally
{
System.Transactions.Transaction.Current = null;
}
}

[Theory]
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
{
if (!TestDialect.SupportsDependentTransaction)
Assert.Ignore("Dialect does not support dependent transactions");
IgnoreIfUnsupported(explicitFlush);

try
{
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
{
using (var committable = new CommittableTransaction())
{
System.Transactions.Transaction.Current = committable;
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
// Acquire the connection
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
clone.Complete();
}

using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
s.Save(new Person());

if (explicitFlush)
s.Flush();

clone.Complete();
}

using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
clone.Complete();
}

System.Transactions.Transaction.Current = committable;
committable.Commit();
}
}
}
finally
{
System.Transactions.Transaction.Current = null;
}

DodgeTransactionCompletionDelayIfRequired();

using (var s = OpenSession())
{
using (var tx = new TransactionScope())
{
if (!AutoJoinTransaction)
s.JoinTransaction();
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
tx.Complete();
}
}
}

private void DodgeTransactionCompletionDelayIfRequired()
{
if (Sfi.ConnectionProvider.Driver.HasDelayedDistributedTransactionCompletion)
Expand Down
121 changes: 120 additions & 1 deletion src/NHibernate.Test/SystemTransactions/SystemTransactionFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using NHibernate.Cfg;
using NHibernate.Driver;
using NHibernate.Engine;
using NHibernate.Linq;
using NHibernate.Test.TransactionTest;
using NUnit.Framework;

Expand Down Expand Up @@ -522,6 +521,126 @@ public void AdditionalJoinDoesNotThrow()
Assert.DoesNotThrow(() => s.JoinTransaction());
}
}

[Theory]
public void CanUseDependentTransaction(bool explicitFlush)
{
if (!TestDialect.SupportsDependentTransaction)
Assert.Ignore("Dialect does not support dependent transactions");
IgnoreIfUnsupported(explicitFlush);

try
{
using (var committable = new CommittableTransaction())
{
System.Transactions.Transaction.Current = committable;
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;

using (var s = OpenSession())
{
if (!AutoJoinTransaction)
s.JoinTransaction();
s.Save(new Person());

if (explicitFlush)
s.Flush();
clone.Complete();
}
}

System.Transactions.Transaction.Current = committable;
committable.Commit();
}
}
finally
{
System.Transactions.Transaction.Current = null;
}
}

[Theory]
public void CanUseSessionWithManyDependentTransaction(bool explicitFlush)
{
if (!TestDialect.SupportsDependentTransaction)
Assert.Ignore("Dialect does not support dependent transactions");
IgnoreIfUnsupported(explicitFlush);
// ODBC with SQL-Server always causes system transactions to go distributed, which causes their transaction completion to run
// asynchronously. But ODBC enlistment also check the previous transaction in a way that do not guard against it
// being concurrently disposed of. See https://github.com/nhibernate/nhibernate-core/pull/1505 for more details.
if (Sfi.ConnectionProvider.Driver is OdbcDriver)
Assert.Ignore("ODBC sometimes fails on second scope by checking the previous transaction status, which may yield an object disposed exception");
// SAP HANA & SQL Anywhere .Net providers always cause system transactions to be distributed, causing them to
// complete on concurrent threads. This creates race conditions when chaining scopes, the subsequent scope usage
// finding the connection still enlisted in the previous transaction, its complete being still not finished
// on its own thread.
if (Sfi.ConnectionProvider.Driver is HanaDriverBase || Sfi.ConnectionProvider.Driver is SapSQLAnywhere17Driver)
Assert.Ignore("SAP HANA and SQL Anywhere scope handling causes concurrency issues preventing chaining scope usages.");

try
{
using (var s = WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
{
using (var committable = new CommittableTransaction())
{
System.Transactions.Transaction.Current = committable;
using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
// Acquire the connection
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(0), "Unexpected initial entity count.");
clone.Complete();
}

using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
s.Save(new Person());

if (explicitFlush)
s.Flush();

clone.Complete();
}

using (var clone = committable.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
System.Transactions.Transaction.Current = clone;
if (!AutoJoinTransaction)
s.JoinTransaction();
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after committed insert.");
clone.Complete();
}

System.Transactions.Transaction.Current = committable;
committable.Commit();
}
}
}
finally
{
System.Transactions.Transaction.Current = null;
}

using (var s = OpenSession())
{
using (var tx = new TransactionScope())
{
if (!AutoJoinTransaction)
s.JoinTransaction();
var count = s.Query<Person>().Count();
Assert.That(count, Is.EqualTo(1), "Unexpected entity count after global commit.");
tx.Complete();
}
}
}
}

[TestFixture]
Expand Down
8 changes: 8 additions & 0 deletions src/NHibernate.Test/TestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ public bool SupportsSqlType(SqlType sqlType)
/// </summary>
public virtual bool SupportsUsingConnectionOnSystemTransactionPrepare => true;

/// <summary>
/// Some databases fail with dependent transaction, typically when their driver tries to access the transaction
/// state from its two PC: the dependent transaction is meant to be disposed of before completing the actual
/// transaction, so it is usually disposed at this point, and its state cannot be read. (Drivers should always
/// clone transactions for avoiding this trouble.)
/// </summary>
public virtual bool SupportsDependentTransaction => true;

/// <summary>
/// Some databases (provider?) fails to compute adequate column types for queries which columns
/// computing include a parameter value.
Expand Down
6 changes: 6 additions & 0 deletions src/NHibernate.Test/TestDialects/PostgreSQL83TestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@ public PostgreSQL83TestDialect(Dialect.Dialect dialect)
/// Npgsql 3.2.4.1.
/// </summary>
public override bool SupportsUsingConnectionOnSystemTransactionPrepare => false;

/// <summary>
/// Npgsql does not clone the transaction in its context, and uses it in its prepare phase. When that was a
/// dependent transaction, it is then usually already disposed of, causing Npgsql to crash.
/// </summary>
public override bool SupportsDependentTransaction => false;
}
}
6 changes: 6 additions & 0 deletions src/NHibernate/AdoNet/ConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,13 @@ public DbCommand CreateCommand()
public void EnlistIfRequired(System.Transactions.Transaction transaction)
{
if (transaction == _currentSystemTransaction)
{
// Short-circuit after having stored the transaction : they may be equal, but not the same reference.
// And the previous one may be an already disposed dependent clone, in which case we need to update
// our reference.
_currentSystemTransaction = transaction;
return;
}

_currentSystemTransaction = transaction;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using NHibernate.AdoNet;
using NHibernate.Engine;
using NHibernate.Engine.Transaction;
using NHibernate.Impl;
using NHibernate.Util;

namespace NHibernate.Transaction
Expand Down
Loading