Skip to content

Commit 678e335

Browse files
NH-2176 - best attempt I can imagine for fixing current transaction handling.
1 parent 87124c2 commit 678e335

File tree

6 files changed

+285
-124
lines changed

6 files changed

+285
-124
lines changed

src/NHibernate.Test/NHSpecificTest/DtcFailures/DtcFailuresFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ public void TransactionInsertWithRollBackTask()
169169
}
170170
}
171171

172-
[Test, Ignore("Not fixed.")]
172+
[Test/*, Ignore("Not fixed.")*/]
173173
[Description(@"Two session in two txscope
174174
(without an explicit NH transaction and without an explicit flush)
175175
and with a rollback in the second dtc and a ForceRollback outside nh-session-scope.")]

src/NHibernate.Test/NHSpecificTest/NH1632/Fixture.cs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ protected override void Configure(Configuration configuration)
2020
{
2121
configuration
2222
.SetProperty(Environment.UseSecondLevelCache, "true")
23-
.SetProperty(Environment.CacheProvider, typeof (HashtableCacheProvider).AssemblyQualifiedName);
23+
.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName);
2424
}
2525

2626
[Test]
@@ -40,9 +40,9 @@ public void When_using_DTC_HiLo_knows_to_create_isolated_DTC_transaction()
4040
var generator = sessions.GetIdentifierGenerator(typeof(Person).FullName);
4141
Assert.That(generator, Is.InstanceOf<TableHiLoGenerator>());
4242

43-
using(var session = sessions.OpenSession())
43+
using (var session = sessions.OpenSession())
4444
{
45-
var id = generator.Generate((ISessionImplementor) session, new Person());
45+
var id = generator.Generate((ISessionImplementor)session, new Person());
4646
}
4747

4848
// intentionally dispose without committing
@@ -56,7 +56,7 @@ public void When_using_DTC_HiLo_knows_to_create_isolated_DTC_transaction()
5656
scalar2 = command.ExecuteScalar();
5757
}
5858

59-
Assert.AreNotEqual(scalar1, scalar2,"HiLo must run with in its own transaction");
59+
Assert.AreNotEqual(scalar1, scalar2, "HiLo must run with in its own transaction");
6060
}
6161

6262
[Test]
@@ -84,43 +84,53 @@ public void When_commiting_items_in_DTC_transaction_will_add_items_to_2nd_level_
8484
{
8585
using (var s = sessions.OpenSession())
8686
{
87-
s.Save(new Nums {ID = 29, NumA = 1, NumB = 3});
87+
s.Save(new Nums { ID = 29, NumA = 1, NumB = 3 });
8888
}
8989
tx.Complete();
9090
}
91-
92-
using (var tx = new TransactionScope())
91+
try
9392
{
94-
using (var s = sessions.OpenSession())
93+
94+
using (var tx = new TransactionScope())
9595
{
96-
var nums = s.Load<Nums>(29);
97-
Assert.AreEqual(1, nums.NumA);
98-
Assert.AreEqual(3, nums.NumB);
96+
using (var s = sessions.OpenSession())
97+
{
98+
var nums = s.Load<Nums>(29);
99+
Assert.AreEqual(1, nums.NumA);
100+
Assert.AreEqual(3, nums.NumB);
101+
}
102+
tx.Complete();
99103
}
100-
tx.Complete();
101-
}
102104

103-
//closing the connection to ensure we can't really use it.
104-
var connection = sessions.ConnectionProvider.GetConnection();
105-
sessions.ConnectionProvider.CloseConnection(connection);
105+
//closing the connection to ensure we can't really use it.
106+
var connection = sessions.ConnectionProvider.GetConnection();
107+
sessions.ConnectionProvider.CloseConnection(connection);
108+
// The session is supposed to succeed because the second level cache should have the
109+
// entity to load, allowing the session to not use the connection at all.
110+
// Will fail if transaction manager tries to enlist user supplied connection, due
111+
// to the transaction scope below.
106112

107-
using (var tx = new TransactionScope())
108-
{
109-
using (var s = sessions.WithOptions().Connection(connection).OpenSession())
113+
using (var tx = new TransactionScope())
110114
{
111-
var nums = s.Load<Nums>(29);
112-
Assert.AreEqual(1, nums.NumA);
113-
Assert.AreEqual(3, nums.NumB);
115+
using (var s = sessions.WithOptions().Connection(connection).OpenSession())
116+
{
117+
Nums nums = null;
118+
Assert.DoesNotThrow(() => nums = s.Load<Nums>(29), "Failed loading entity from second level cache.");
119+
Assert.AreEqual(1, nums.NumA);
120+
Assert.AreEqual(3, nums.NumB);
121+
}
122+
tx.Complete();
114123
}
115-
tx.Complete();
116124
}
117-
118-
using (var s = sessions.OpenSession())
119-
using (var tx = s.BeginTransaction())
125+
finally
120126
{
121-
var nums = s.Load<Nums>(29);
122-
s.Delete(nums);
123-
tx.Commit();
127+
using (var s = sessions.OpenSession())
128+
using (var tx = s.BeginTransaction())
129+
{
130+
var nums = s.Load<Nums>(29);
131+
s.Delete(nums);
132+
tx.Commit();
133+
}
124134
}
125135
}
126136

src/NHibernate.Test/NHSpecificTest/NH2420/Fixture.cs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Data.Odbc;
44
using System.Data.SqlClient;
55
using System.Configuration;
6+
using System.Threading;
67
using System.Transactions;
78
using NHibernate.Dialect;
89
using NHibernate.Driver;
@@ -55,41 +56,56 @@ public void ShouldBeAbleToReleaseSuppliedConnectionAfterDistributedTransaction()
5556
{
5657
string connectionString = FetchConnectionStringFromConfiguration();
5758
ISession s;
58-
using (var ts = new TransactionScope())
59+
DbConnection connection = null;
60+
try
5961
{
60-
// Enlisting DummyEnlistment as a durable resource manager will start
61-
// a DTC transaction
62-
System.Transactions.Transaction.Current.EnlistDurable(
63-
DummyEnlistment.Id,
64-
new DummyEnlistment(),
65-
EnlistmentOptions.None);
62+
using (var ts = new TransactionScope())
63+
{
64+
// Enlisting DummyEnlistment as a durable resource manager will start
65+
// a DTC transaction
66+
System.Transactions.Transaction.Current.EnlistDurable(
67+
DummyEnlistment.Id,
68+
new DummyEnlistment(),
69+
EnlistmentOptions.None);
6670

67-
DbConnection connection;
68-
if (sessions.ConnectionProvider.Driver.GetType() == typeof(OdbcDriver))
69-
connection = new OdbcConnection(connectionString);
70-
else
71-
connection = new SqlConnection(connectionString);
71+
if (sessions.ConnectionProvider.Driver.GetType() == typeof(OdbcDriver))
72+
connection = new OdbcConnection(connectionString);
73+
else
74+
connection = new SqlConnection(connectionString);
7275

73-
using (connection)
74-
{
7576
connection.Open();
7677
using (s = Sfi.WithOptions().Connection(connection).OpenSession())
7778
{
78-
s.Save(new MyTable { String = "hello!" });
79+
s.Save(new MyTable {String = "hello!"});
7980
}
80-
connection.Close();
81-
}
81+
// The ts disposal may try to flush the session, which, depending on the native generator
82+
// implementation for current dialect, may have something to do and will then try to use
83+
// the supplied connection. dispose connection here => flaky test, failing for dialects
84+
// not mandating an immediate insert on native generator save.
85+
// Delaying the connection disposal to after ts disposal.
8286

83-
ts.Complete();
87+
ts.Complete();
88+
}
89+
}
90+
finally
91+
{
92+
connection?.Dispose();
8493
}
8594

95+
// It appears neither the second phase of the 2PC nor TransactionCompleted
96+
// event are guaranteed to be executed before exiting transaction scope disposal.
97+
// When having only 2PC, the second phase tends to occur after reaching that point
98+
// here. When having TransactionCompleted event, this event and the second phase
99+
// tend to occur before reaching here. But some other NH cases demonstrate that
100+
// TransactionCompleted may also occur "too late".
101+
((ISessionImplementor) s).TransactionContext?.WaitOne();
102+
86103
// Prior to the patch, an InvalidOperationException exception would occur in the
87104
// TransactionCompleted delegate at this point with the message, "Disconnect cannot
88105
// be called while a transaction is in progress". Although the exception can be
89106
// seen reported in the IDE, NUnit fails to see it. The TransactionCompleted event
90107
// fires *after* the transaction is committed and so it doesn't affect the success
91108
// of the transaction.
92-
93109
Assert.That(s.IsConnected, Is.False);
94110
Assert.That(((ISessionImplementor)s).ConnectionManager.IsConnected, Is.False);
95111
Assert.That(((ISessionImplementor)s).IsClosed, Is.True);

src/NHibernate/AdoNet/ConnectionManager.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public interface Callback
2929

3030
[NonSerialized]
3131
private DbConnection connection;
32+
[NonSerialized]
33+
private DbConnection _dtcBackupConnection;
34+
[NonSerialized]
35+
private System.Transactions.Transaction _connectionAmbientTransaction;
36+
[NonSerialized]
37+
private System.Transactions.Transaction _dtcBackupConnectionAmbientTransaction;
3238
// Whether we own the connection, i.e. connect and disconnect automatically.
3339
private bool ownConnection;
3440

@@ -184,6 +190,7 @@ public DbConnection GetConnection()
184190
if (ownConnection)
185191
{
186192
connection = Factory.ConnectionProvider.GetConnection();
193+
_connectionAmbientTransaction = System.Transactions.Transaction.Current;
187194
if (Factory.Statistics.IsStatisticsEnabled)
188195
{
189196
Factory.StatisticsImplementor.Connect();
@@ -394,10 +401,29 @@ public IBatcher Batcher
394401
get { return batcher; }
395402
}
396403

404+
/// <summary>
405+
/// Tell if according to current ambient transaction, the connection should be explicitly enlisted
406+
/// for ensuring it to participate in transaction. Always <see langword="false" /> for supplied
407+
/// connection, enlistment is user business in such case.
408+
/// </summary>
409+
public bool RequireExplicitEnlistment
410+
=> ownConnection && connection != null &&
411+
_connectionAmbientTransaction != System.Transactions.Transaction.Current;
412+
397413
public IDisposable FlushingFromDtcTransaction
398414
{
399415
get
400416
{
417+
if (ownConnection)
418+
{
419+
if (Batcher.HasOpenResources)
420+
throw new InvalidOperationException("Batcher still has opened ressources at time of Flush from DTC.");
421+
// Swap out current connection for avoiding using it concurrently to its own 2PC
422+
_dtcBackupConnection = connection;
423+
_dtcBackupConnectionAmbientTransaction = _connectionAmbientTransaction;
424+
connection = null;
425+
_connectionAmbientTransaction = null;
426+
}
401427
flushingFromDtcTransaction = true;
402428
return new StopFlushingFromDtcTransaction(this);
403429
}
@@ -415,6 +441,17 @@ public StopFlushingFromDtcTransaction(ConnectionManager manager)
415441
public void Dispose()
416442
{
417443
manager.flushingFromDtcTransaction = false;
444+
445+
if (manager.ownConnection)
446+
{
447+
// Release the connection potentially acquired for flushing from DTC.
448+
manager.DisconnectOwnConnection();
449+
// Swap back current connection
450+
manager.connection = manager._dtcBackupConnection;
451+
manager._connectionAmbientTransaction = manager._dtcBackupConnectionAmbientTransaction;
452+
manager._dtcBackupConnection = null;
453+
manager._dtcBackupConnectionAmbientTransaction = null;
454+
}
418455
}
419456
}
420457

0 commit comments

Comments
 (0)