-
Notifications
You must be signed in to change notification settings - Fork 933
NH-2176 - Add test case #410
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
Changes from all commits
1f55f46
d184a6e
a0c062d
91c7aa3
88491fc
5d167af
2a4c2a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,8 +87,8 @@ public void WillNotCrashOnDtcPrepareFailure() | |
[Test] | ||
public void Can_roll_back_transaction() | ||
{ | ||
if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions"); | ||
/*if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions");*/ | ||
|
||
var tx = new TransactionScope(); | ||
using (ISession s = sessions.OpenSession()) | ||
|
@@ -113,8 +113,8 @@ public void Can_roll_back_transaction() | |
[Description("Another action inside the transaction do the rollBack outside nh-session-scope.")] | ||
public void RollbackOutsideNh() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one showcase the roadblock for the custom transaction factory: session close within an ongoing transaction scope is rejected by connection manager. The connection manager close does close any pending explicit transaction, then call disconnect which checks there are no pending transaction, explicit or ambient. The ambient check on disconnect should be removed, ambient transaction supports connection disconnect. This need changes in NHibernate project even for testing: the connection manager is not pluggable. |
||
{ | ||
if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions"); | ||
/*if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions");*/ | ||
|
||
try | ||
{ | ||
|
@@ -143,8 +143,8 @@ public void RollbackOutsideNh() | |
[Description("rollback inside nh-session-scope should not commit save and the transaction should be aborted.")] | ||
public void TransactionInsertWithRollBackTask() | ||
{ | ||
if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions"); | ||
/*if (Dialect is FirebirdDialect) | ||
Assert.Ignore("Firebird driver does not support distributed transactions");*/ | ||
|
||
try | ||
{ | ||
|
@@ -169,7 +169,7 @@ public void TransactionInsertWithRollBackTask() | |
} | ||
} | ||
|
||
[Test, Ignore("Not fixed.")] | ||
[Test/*, Ignore("Not fixed.")*/] | ||
[Description(@"Two session in two txscope | ||
(without an explicit NH transaction and without an explicit flush) | ||
and with a rollback in the second dtc and a ForceRollback outside nh-session-scope.")] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ protected override void Configure(Configuration configuration) | |
{ | ||
configuration | ||
.SetProperty(Environment.UseSecondLevelCache, "true") | ||
.SetProperty(Environment.CacheProvider, typeof (HashtableCacheProvider).AssemblyQualifiedName); | ||
.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName); | ||
} | ||
|
||
[Test] | ||
|
@@ -40,9 +40,9 @@ public void When_using_DTC_HiLo_knows_to_create_isolated_DTC_transaction() | |
var generator = sessions.GetIdentifierGenerator(typeof(Person).FullName); | ||
Assert.That(generator, Is.InstanceOf<TableHiLoGenerator>()); | ||
|
||
using(var session = sessions.OpenSession()) | ||
using (var session = sessions.OpenSession()) | ||
{ | ||
var id = generator.Generate((ISessionImplementor) session, new Person()); | ||
var id = generator.Generate((ISessionImplementor)session, new Person()); | ||
} | ||
|
||
// intentionally dispose without committing | ||
|
@@ -56,7 +56,7 @@ public void When_using_DTC_HiLo_knows_to_create_isolated_DTC_transaction() | |
scalar2 = command.ExecuteScalar(); | ||
} | ||
|
||
Assert.AreNotEqual(scalar1, scalar2,"HiLo must run with in its own transaction"); | ||
Assert.AreNotEqual(scalar1, scalar2, "HiLo must run with in its own transaction"); | ||
} | ||
|
||
[Test] | ||
|
@@ -84,43 +84,53 @@ public void When_commiting_items_in_DTC_transaction_will_add_items_to_2nd_level_ | |
{ | ||
using (var s = sessions.OpenSession()) | ||
{ | ||
s.Save(new Nums {ID = 29, NumA = 1, NumB = 3}); | ||
s.Save(new Nums { ID = 29, NumA = 1, NumB = 3 }); | ||
} | ||
tx.Complete(); | ||
} | ||
|
||
using (var tx = new TransactionScope()) | ||
try | ||
{ | ||
using (var s = sessions.OpenSession()) | ||
|
||
using (var tx = new TransactionScope()) | ||
{ | ||
var nums = s.Load<Nums>(29); | ||
Assert.AreEqual(1, nums.NumA); | ||
Assert.AreEqual(3, nums.NumB); | ||
using (var s = sessions.OpenSession()) | ||
{ | ||
var nums = s.Load<Nums>(29); | ||
Assert.AreEqual(1, nums.NumA); | ||
Assert.AreEqual(3, nums.NumB); | ||
} | ||
tx.Complete(); | ||
} | ||
tx.Complete(); | ||
} | ||
|
||
//closing the connection to ensure we can't really use it. | ||
var connection = sessions.ConnectionProvider.GetConnection(); | ||
sessions.ConnectionProvider.CloseConnection(connection); | ||
//closing the connection to ensure we can't really use it. | ||
var connection = sessions.ConnectionProvider.GetConnection(); | ||
sessions.ConnectionProvider.CloseConnection(connection); | ||
// The session is supposed to succeed because the second level cache should have the | ||
// entity to load, allowing the session to not use the connection at all. | ||
// Will fail if transaction manager tries to enlist user supplied connection, due | ||
// to the transaction scope below. | ||
|
||
using (var tx = new TransactionScope()) | ||
{ | ||
using (var s = sessions.OpenSession(connection)) | ||
using (var tx = new TransactionScope()) | ||
{ | ||
var nums = s.Load<Nums>(29); | ||
Assert.AreEqual(1, nums.NumA); | ||
Assert.AreEqual(3, nums.NumB); | ||
using (var s = sessions.OpenSession(connection)) | ||
{ | ||
Nums nums = null; | ||
Assert.DoesNotThrow(() => nums = s.Load<Nums>(29), "Failed loading entity from second level cache."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving an explicit message instead of "caching failure" => then "failing to tear down", and letting not much clues. |
||
Assert.AreEqual(1, nums.NumA); | ||
Assert.AreEqual(3, nums.NumB); | ||
} | ||
tx.Complete(); | ||
} | ||
tx.Complete(); | ||
} | ||
|
||
using (var s = sessions.OpenSession()) | ||
using (var tx = s.BeginTransaction()) | ||
finally | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoiding additional tear down failure. |
||
{ | ||
var nums = s.Load<Nums>(29); | ||
s.Delete(nums); | ||
tx.Commit(); | ||
using (var s = sessions.OpenSession()) | ||
using (var tx = s.BeginTransaction()) | ||
{ | ||
var nums = s.Load<Nums>(29); | ||
s.Delete(nums); | ||
tx.Commit(); | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Concurrent; | ||
using System.Data.Common; | ||
using System.Transactions; | ||
using NHibernate.Cfg; | ||
using NHibernate.Dialect; | ||
using NHibernate.Engine; | ||
using NHibernate.Engine.Transaction; | ||
using NHibernate.Transaction; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.NH2176 | ||
{ | ||
[TestFixture] | ||
public class Fixture : BugTestCase | ||
{ | ||
protected override void Configure(Configuration configuration) | ||
{ | ||
//configuration.SetProperty(Cfg.Environment.TransactionStrategy, "NHibernate.Test.NHSpecificTest.NH2176.CustomAdoNetTransactionFactory, NHibernate.Test"); | ||
} | ||
|
||
protected override void OnSetUp() | ||
{ | ||
base.OnSetUp(); | ||
using (var s = OpenSession()) | ||
using (var tx = s.BeginTransaction()) | ||
{ | ||
var steve = new Person { Name = "Steve" }; | ||
var peter = new Person { Name = "Peter" }; | ||
var simon = new Person { Name = "Simon" }; | ||
var paul = new Person { Name = "Paul" }; | ||
var john = new Person { Name = "John" }; | ||
var eric = new Person { Name = "Eric" }; | ||
|
||
s.Save(steve); | ||
s.Save(peter); | ||
s.Save(simon); | ||
s.Save(paul); | ||
s.Save(john); | ||
s.Save(eric); | ||
|
||
tx.Commit(); | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
base.OnTearDown(); | ||
using (var s = OpenSession()) | ||
using (var tx = s.BeginTransaction()) | ||
{ | ||
s.Delete("from Person"); | ||
tx.Commit(); | ||
} | ||
} | ||
|
||
// Whilst this bug seems specific to Oracle I think it is valid to run the | ||
// test against all database types. | ||
[Test] | ||
public void MultipleConsecutiveTransactionScopesCanBeUsedInsideASingleSession() | ||
{ | ||
using (var s = OpenSession()) | ||
{ | ||
// usually fails after just a few loops in oracle | ||
// this can be run for 10000 loops in sql server without problem | ||
for (var i = 0; i < 100; ++i) | ||
{ | ||
Console.WriteLine(i.ToString()); | ||
|
||
using (var scope = new TransactionScope()) | ||
{ | ||
var criteria = s.CreateCriteria<Person>(); | ||
var people = criteria.List<Person>(); | ||
|
||
Assert.That(people.Count, Is.EqualTo(6)); | ||
|
||
scope.Complete(); | ||
} | ||
|
||
// The exception is caused by a race condition between two threads. | ||
// This can be demonstrated by uncommenting the following line which | ||
// causes the test to run without an exception. | ||
//System.Threading.Thread.Sleep(1000); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Unfortunately, cannot derive and override NHibernate impl, methods are not virtual. | ||
public class CustomAdoNetTransactionFactory : ITransactionFactory | ||
{ | ||
private readonly AdoNetTransactionFactory _adoNetTransactionFactory = | ||
new AdoNetTransactionFactory(); | ||
|
||
private readonly ConcurrentDictionary<DbConnection, System.Transactions.Transaction> _sessionsTransaction = | ||
new ConcurrentDictionary<DbConnection, System.Transactions.Transaction>(); | ||
|
||
public void Configure(IDictionary props) { } | ||
|
||
public ITransaction CreateTransaction(ISessionImplementor session) | ||
{ | ||
return new AdoTransaction(session); | ||
} | ||
|
||
public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session) | ||
{ | ||
// No session enlistment. This disables automatic flushes before ambient transaction | ||
// commits. Explicit Flush calls required. | ||
// Still make sure the session connection is enlisted, in case it was acquired before | ||
// transaction scope start. | ||
// Will not support nested transaction scope. (Will throw, while current NHibernate | ||
// just stay in previous scope.) | ||
// Will cause an "earlier than required" connection acquisition. | ||
// It is required to enlist with null when the scope is ended, otherwise using | ||
// the transaction without a new scope will fail by attempting to use it inside | ||
// the completed scope. | ||
// If an explicit transaction is ongoing, we must not enlist. We should not enlist | ||
// either if the connection was supplied by user (let him handle that in such case), | ||
// but there are currently no ways to know this from here. | ||
if (!session.ConnectionManager.Transaction.IsActive) | ||
{ | ||
// Enlist is called terribly frequently, and in some circumstances, it will | ||
// not support to be called with the same value. So track what was the previous | ||
// call and do not call it again if unneeded. | ||
// (And Sql/OleDb/Odbc/Oracle manage/PostgreSql/MySql/Firebird/SQLite connections | ||
// support multiple calls with the same ongoing transaction, but some others may not.) | ||
var current = GetCurrentTransaction(); | ||
var connection = session.Connection; | ||
System.Transactions.Transaction previous; | ||
if (!_sessionsTransaction.TryGetValue(connection, out previous) || previous != current) | ||
{ | ||
_sessionsTransaction.AddOrUpdate(connection, current, (s, t) => current); | ||
if (current == null && | ||
// This will need an ad-hoc property on Dialect base class instead. | ||
(session.Factory.Dialect is SQLiteDialect || session.Factory.Dialect is MsSqlCeDialect)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this shall be a dialect setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, as per comment above.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw the comment only after I commented:) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way it looks like a bunch of connections do not need that, they are able of "unenlisting" from ended transaction by themselves. But not all. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it should be driver setting, not dialect. Because SqlServer via ODBC requires this change, and SqlServer via native does not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, it is not a trouble for Sql Server since it supports it in both case. But that is a trouble for SQLite since it will require it if accessed through ODBC (I have just checked, there is an ODBC driver for that) but it will not support it if accessed through its managed driver. |
||
{ | ||
// Some connections does not support enlisting with null | ||
// Let them with their previous transaction if any, the application | ||
// will fail if the connection was left with a completed transaction due to this. | ||
return; | ||
} | ||
session.Connection.EnlistTransaction(current); | ||
} | ||
} | ||
} | ||
|
||
public bool IsInDistributedActiveTransaction(ISessionImplementor session) | ||
{ | ||
// Avoid agressive connection release while a transaction is ongoing. Allow | ||
// auto-flushes (flushes before queries on dirtied entities). | ||
return GetCurrentTransaction() != null; | ||
} | ||
|
||
public void ExecuteWorkInIsolation(ISessionImplementor session, IIsolatedWork work, | ||
bool transacted) | ||
{ | ||
using (var tx = new TransactionScope(TransactionScopeOption.Suppress)) | ||
{ | ||
// instead of duplicating the logic, we suppress the DTC transaction | ||
// and create our own transaction instead | ||
_adoNetTransactionFactory.ExecuteWorkInIsolation(session, work, | ||
transacted); | ||
tx.Complete(); | ||
} | ||
} | ||
|
||
private System.Transactions.Transaction GetCurrentTransaction() | ||
{ | ||
try | ||
{ | ||
return System.Transactions.Transaction.Current; | ||
} | ||
catch (InvalidOperationException) | ||
{ | ||
// This damn thing may yield an invalid operation exception (instead of null | ||
// or of a completed transaction) if we are between scope.Complete() and | ||
// scope.Dispose(). This happen when having completed the scope then disposing | ||
// the session and only after that disposing the scope. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another bug fixed in custom transaction factory: accessing Transaction.Current may throw, need to catch. |
||
// Instead of testing System.Transactions.Transaction.Current here, storing in | ||
// connection manager the ambient transaction associated to the connection | ||
// (and updating it when enlisting) then checking that stored transaction would | ||
// reduce testes on Transaction.Current. | ||
return null; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" | ||
namespace="NHibernate.Test.NHSpecificTest.NH2176" | ||
assembly="NHibernate.Test" | ||
> | ||
<class name="Person" table="NH2176_Person"> | ||
|
||
<id name="Id" column="PersonId"> | ||
<generator class="identity"/> | ||
</id> | ||
|
||
<property name="Name" column="Name" /> | ||
|
||
</class> | ||
</hibernate-mapping> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
namespace NHibernate.Test.NHSpecificTest.NH2176 | ||
{ | ||
public class Person | ||
{ | ||
public virtual int Id { get; set; } | ||
public virtual string Name { get; set; } | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my local test Firebird supports them (at least does not throw), so just letting them run to test with teamcity too.