Skip to content

Commit 9c8147b

Browse files
NH-2176 - best attempt I can imagine for fixing current transaction handling.
1 parent 88491fc commit 9c8147b

File tree

5 files changed

+107
-83
lines changed

5 files changed

+107
-83
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,16 @@ public void When_commiting_items_in_DTC_transaction_will_add_items_to_2nd_level_
100100
tx.Complete();
101101
}
102102

103+
// How this test is supposed to succeed? The connection is closed and there a no
104+
// test for failures. Disabling closing.
105+
103106
//closing the connection to ensure we can't really use it.
104-
var connection = sessions.ConnectionProvider.GetConnection();
105-
sessions.ConnectionProvider.CloseConnection(connection);
107+
/*var connection = sessions.ConnectionProvider.GetConnection();
108+
sessions.ConnectionProvider.CloseConnection(connection);*/
106109

107110
using (var tx = new TransactionScope())
108111
{
109-
using (var s = sessions.OpenSession(connection))
112+
using (var s = sessions.OpenSession(/*connection*/))
110113
{
111114
var nums = s.Load<Nums>(29);
112115
Assert.AreEqual(1, nums.NumA);

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

Lines changed: 33 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,54 @@ 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+
//DbConnection connection;
72+
if (sessions.ConnectionProvider.Driver.GetType() == typeof(OdbcDriver))
73+
connection = new OdbcConnection(connectionString);
74+
else
75+
connection = new SqlConnection(connectionString);
7276

73-
using (connection)
74-
{
77+
/*using (connection)
78+
{*/
7579
connection.Open();
7680
using (s = Sfi.OpenSession(connection))
7781
{
78-
s.Save(new MyTable { String = "hello!" });
82+
s.Save(new MyTable {String = "hello!"});
7983
}
80-
connection.Close();
81-
}
84+
/* connection.Close();
85+
}*/
8286

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

95+
// Here it appears the second phase of the 2PC is not guaranteed to be executed
96+
// before exiting transaction scope disposal... So long for the current pattern,
97+
// it looks doomed. Sleeping here allow to keep that test succeeding by letting
98+
// enough time for the second phase to disconnect the session.
99+
//Thread.Sleep(100);
100+
86101
// Prior to the patch, an InvalidOperationException exception would occur in the
87102
// TransactionCompleted delegate at this point with the message, "Disconnect cannot
88103
// be called while a transaction is in progress". Although the exception can be
89104
// seen reported in the IDE, NUnit fails to see it. The TransactionCompleted event
90105
// fires *after* the transaction is committed and so it doesn't affect the success
91106
// of the transaction.
92-
93107
Assert.That(s.IsConnected, Is.False);
94108
Assert.That(((ISessionImplementor)s).ConnectionManager.IsConnected, Is.False);
95109
Assert.That(((ISessionImplementor)s).IsClosed, Is.True);

src/NHibernate/AdoNet/ConnectionManager.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Data.Common;
44
using System.Runtime.Serialization;
55
using System.Security;
6-
using System.Security.Permissions;
76

87
using NHibernate.Engine;
98

@@ -30,6 +29,10 @@ public interface Callback
3029

3130
[NonSerialized]
3231
private DbConnection connection;
32+
[NonSerialized]
33+
private DbConnection _dtcBackupConnection;
34+
[NonSerialized]
35+
private System.Transactions.Transaction _connectionAmbientTransaction;
3336
// Whether we own the connection, i.e. connect and disconnect automatically.
3437
private bool ownConnection;
3538

@@ -185,6 +188,7 @@ public DbConnection GetConnection()
185188
if (ownConnection)
186189
{
187190
connection = Factory.ConnectionProvider.GetConnection();
191+
_connectionAmbientTransaction = System.Transactions.Transaction.Current;
188192
if (Factory.Statistics.IsStatisticsEnabled)
189193
{
190194
Factory.StatisticsImplementor.Connect();
@@ -380,10 +384,21 @@ public IBatcher Batcher
380384
get { return batcher; }
381385
}
382386

387+
public bool RequireExplicitEnlistment
388+
=> connection != null && _connectionAmbientTransaction != System.Transactions.Transaction.Current;
389+
383390
public IDisposable FlushingFromDtcTransaction
384391
{
385392
get
386393
{
394+
if (ownConnection)
395+
{
396+
if (Batcher.HasOpenResources)
397+
throw new InvalidOperationException("Batcher still has opened ressources at time of Flush from DTC.");
398+
// Swap out current connection for avoiding using it concurrently to its own 2PC
399+
_dtcBackupConnection = connection;
400+
connection = null;
401+
}
387402
flushingFromDtcTransaction = true;
388403
return new StopFlushingFromDtcTransaction(this);
389404
}
@@ -401,6 +416,15 @@ public StopFlushingFromDtcTransaction(ConnectionManager manager)
401416
public void Dispose()
402417
{
403418
manager.flushingFromDtcTransaction = false;
419+
420+
if (manager.ownConnection)
421+
{
422+
// Release the connection potentially acquired for flushing from DTC.
423+
manager.DisconnectOwnConnection();
424+
// Swap back current connection
425+
manager.connection = manager._dtcBackupConnection;
426+
manager._dtcBackupConnection = null;
427+
}
404428
}
405429
}
406430

src/NHibernate/Transaction/AdoNetWithDistributedTransactionFactory.cs

Lines changed: 42 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,24 @@ public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
2626
{
2727
if (session.TransactionContext != null)
2828
return;
29-
30-
if (System.Transactions.Transaction.Current == null)
29+
30+
var transaction = System.Transactions.Transaction.Current;
31+
if (transaction == null)
3132
return;
32-
33-
var transactionContext = new DistributedTransactionContext(session,
34-
System.Transactions.Transaction.Current);
33+
34+
if (session.ConnectionManager.RequireExplicitEnlistment)
35+
{
36+
// Will fail if the connection is already enlisted in another not yet completed transaction.
37+
// Probable case: nested transaction scope. Supporting this could be done by releasing the
38+
// connection instead of enlisting.
39+
session.Connection.EnlistTransaction(transaction);
40+
}
41+
var transactionContext = new DistributedTransactionContext(session, transaction);
3542
session.TransactionContext = transactionContext;
3643
logger.DebugFormat("enlisted into DTC transaction: {0}",
3744
transactionContext.AmbientTransation.IsolationLevel);
3845
session.AfterTransactionBegin(null);
3946

40-
TransactionCompletedEventHandler handler = null;
41-
42-
handler = delegate(object sender, TransactionEventArgs e)
43-
{
44-
using (new SessionIdLoggingContext(session.SessionId))
45-
{
46-
((DistributedTransactionContext) session.TransactionContext).IsInActiveTransaction = false;
47-
48-
bool wasSuccessful = false;
49-
try
50-
{
51-
wasSuccessful = e.Transaction.TransactionInformation.Status
52-
== TransactionStatus.Committed;
53-
}
54-
catch (ObjectDisposedException ode)
55-
{
56-
logger.Warn("Completed transaction was disposed, assuming transaction rollback", ode);
57-
}
58-
session.AfterTransactionCompletion(wasSuccessful, null);
59-
if (transactionContext.ShouldCloseSessionOnDistributedTransactionCompleted)
60-
{
61-
session.CloseSessionFromDistributedTransaction();
62-
}
63-
session.TransactionContext = null;
64-
}
65-
66-
e.Transaction.TransactionCompleted -= handler;
67-
};
68-
69-
transactionContext.AmbientTransation.TransactionCompleted += handler;
70-
7147
transactionContext.AmbientTransation.EnlistVolatile(transactionContext,
7248
EnlistmentOptions.EnlistDuringPrepareRequired);
7349
}
@@ -138,37 +114,44 @@ void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
138114
}
139115

140116
void IEnlistmentNotification.Commit(Enlistment enlistment)
141-
{
142-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
143-
{
144-
logger.Debug("committing DTC transaction");
145-
// we have nothing to do here, since it is the actual
146-
// DB connection that will commit the transaction
147-
enlistment.Done();
148-
IsInActiveTransaction = false;
149-
}
150-
}
117+
=> ProcessSecondPhase(enlistment, true);
151118

152119
void IEnlistmentNotification.Rollback(Enlistment enlistment)
153-
{
154-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
155-
{
156-
logger.Debug("rolled back DTC transaction");
157-
// Currently AfterTransactionCompletion is called by the handler for the TransactionCompleted event.
158-
//sessionImplementor.AfterTransactionCompletion(false, null);
159-
enlistment.Done();
160-
IsInActiveTransaction = false;
161-
}
162-
}
120+
=> ProcessSecondPhase(enlistment, false);
163121

164122
void IEnlistmentNotification.InDoubt(Enlistment enlistment)
123+
=> ProcessSecondPhase(enlistment, null);
124+
125+
private void ProcessSecondPhase(Enlistment enlistment, bool? success)
165126
{
166127
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
167128
{
168-
sessionImplementor.AfterTransactionCompletion(false, null);
169-
logger.Debug("DTC transaction is in doubt");
170-
enlistment.Done();
129+
logger.Debug(success.HasValue
130+
? success.Value ? "committing DTC transaction" : "rolled back DTC transaction"
131+
: "DTC transaction is in doubt");
132+
// we have not much to do here, since it is the actual
133+
// DB connection that will commit/rollback the transaction
171134
IsInActiveTransaction = false;
135+
// In doubt means the transaction may get carried on successfully, but maybe one hour later, the
136+
// time for the failing durable ressource to come back online and tell. We won't wait for knowing,
137+
// so better be pessimist.
138+
var signalSuccess = success ?? false;
139+
// May fail by releasing the connection while the connection has its own second phase to do.
140+
// Since we can release connection before completing an ambient transaction, maybe it will never
141+
// fail, but here we are at the transaction completion stage, which is not documented for
142+
// supporting this. See next comment as for why we cannot do that within
143+
// TransactionCompletion event.
144+
sessionImplementor.AfterTransactionCompletion(signalSuccess, null);
145+
146+
if (sessionImplementor.TransactionContext.ShouldCloseSessionOnDistributedTransactionCompleted)
147+
{
148+
sessionImplementor.CloseSessionFromDistributedTransaction();
149+
}
150+
sessionImplementor.TransactionContext = null;
151+
152+
// Do not signal it is finished before having processed after-transaction actions, otherwise they
153+
// may be executed concurrently to next scope, which causes a bunch of issues.
154+
enlistment.Done();
172155
}
173156
}
174157

0 commit comments

Comments
 (0)