Skip to content

Commit 25e5c48

Browse files
NH-4011 - Adjusting transaction handling according to MSDTC tests findings.
1 parent a61dbc7 commit 25e5c48

File tree

4 files changed

+39
-37
lines changed

4 files changed

+39
-37
lines changed

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ public async Task CanUseSessionWithManyScopesAsync([Values(false, true)] bool ex
416416
{
417417
// Note that this fails with ConnectionReleaseMode.OnClose and SqlServer:
418418
// System.Data.SqlClient.SqlException : Microsoft Distributed Transaction Coordinator (MS DTC) has stopped this transaction.
419+
// Not much an issue since it is advised to not use ConnectionReleaseMode.OnClose.
419420
using (var s = OpenSession())
420421
//using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
421422
{
@@ -482,7 +483,11 @@ public async Task CanUseSessionWithManyScopesAsync([Values(false, true)] bool ex
482483
[Test]
483484
public async Task CanUseSessionOutsideOfScopeAfterScopeAsync([Values(false, true)] bool explicitFlush)
484485
{
485-
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
486+
// Note that this fails with ConnectionReleaseMode.OnClose and Npgsql (< 3.2.5?):
487+
// NpgsqlOperationInProgressException: The connection is already in state 'Executing'
488+
// Not much an issue since it is advised to not use ConnectionReleaseMode.OnClose.
489+
using (var s = OpenSession())
490+
//using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
486491
{
487492
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
488493
{

src/NHibernate.Test/SystemTransactions/DistributedSystemTransactionFixture.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ public void CanUseSessionWithManyScopes([Values(false, true)] bool explicitFlush
445445
{
446446
// Note that this fails with ConnectionReleaseMode.OnClose and SqlServer:
447447
// System.Data.SqlClient.SqlException : Microsoft Distributed Transaction Coordinator (MS DTC) has stopped this transaction.
448+
// Not much an issue since it is advised to not use ConnectionReleaseMode.OnClose.
448449
using (var s = OpenSession())
449450
//using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
450451
{
@@ -511,7 +512,11 @@ public void CanUseSessionWithManyScopes([Values(false, true)] bool explicitFlush
511512
[Test]
512513
public void CanUseSessionOutsideOfScopeAfterScope([Values(false, true)] bool explicitFlush)
513514
{
514-
using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
515+
// Note that this fails with ConnectionReleaseMode.OnClose and Npgsql (< 3.2.5?):
516+
// NpgsqlOperationInProgressException: The connection is already in state 'Executing'
517+
// Not much an issue since it is advised to not use ConnectionReleaseMode.OnClose.
518+
using (var s = OpenSession())
519+
//using (var s = Sfi.WithOptions().ConnectionReleaseMode(ConnectionReleaseMode.OnClose).OpenSession())
515520
{
516521
using (var tx = new TransactionScope())
517522
{

src/NHibernate/AdoNet/ConnectionManager.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,9 @@ public void EnlistIfRequired(System.Transactions.Transaction transaction)
441441
_connection.EnlistTransaction(transaction);
442442
}
443443

444-
public IDisposable BeginFlushingFromSystemTransaction(bool isDistributed)
444+
public IDisposable BeginFlushingFromSystemTransaction()
445445
{
446-
var needSwapping = _ownConnection && isDistributed &&
446+
var needSwapping = _ownConnection &&
447447
Factory.Dialect.SupportsConcurrentWritingConnectionsInSameTransaction;
448448
if (needSwapping)
449449
{

src/NHibernate/Transaction/AdoNetWithSystemTransactionFactory.cs

+25-33
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ protected virtual void JoinSystemTransaction(ISessionImplementor session, System
9292
originatingSession.TransactionContext = transactionContext;
9393

9494
_logger.DebugFormat(
95-
"enlisted into DTC transaction: {0}",
95+
"enlisted into system transaction: {0}",
9696
transactionContext.EnlistedTransaction.IsolationLevel);
9797

9898
originatingSession.AfterTransactionBegin(null);
@@ -124,19 +124,20 @@ public class SystemTransactionContext : ITransactionContext, IEnlistmentNotifica
124124
public bool IsInActiveTransaction { get; internal set; } = true;
125125

126126
private readonly ISessionImplementor _sessionImplementor;
127+
private readonly System.Transactions.Transaction _originalTransaction;
127128
private readonly ManualResetEventSlim _lock = new ManualResetEventSlim(true);
128129
private volatile bool _needCompletionLocking = true;
129130
// Required for not locking the completion phase itself when locking session usages from concurrent threads.
130131
private readonly AsyncLocal<bool> _bypassLock = new AsyncLocal<bool>();
131132
private readonly int _systemTransactionCompletionLockTimeout;
132-
private bool IsDistributed => EnlistedTransaction.TransactionInformation.DistributedIdentifier != Guid.Empty;
133133

134134
public SystemTransactionContext(
135135
ISessionImplementor sessionImplementor,
136136
System.Transactions.Transaction transaction,
137137
int systemTransactionCompletionLockTimeout)
138138
{
139139
_sessionImplementor = sessionImplementor;
140+
_originalTransaction = transaction;
140141
EnlistedTransaction = transaction.Clone();
141142
EnlistedTransaction.TransactionCompleted += TransactionCompleted;
142143
_systemTransactionCompletionLockTimeout = systemTransactionCompletionLockTimeout;
@@ -159,10 +160,11 @@ public void Wait()
159160
if (_lock.Wait(_systemTransactionCompletionLockTimeout))
160161
return;
161162
// A call occurring after transaction scope disposal should not have to wait long, since
162-
// the scope disposal is supposed to block until the transaction has completed: I hope
163-
// that it at least ensures IO are done, even if experience shows DTC lets the scope
164-
// disposal leave before having finished with volatile ressources and
165-
// TransactionCompleted event.
163+
// the scope disposal is supposed to block until the transaction has completed. When not
164+
// distributed, all is done, no wait. When distributed, with MSDTC, the scope disposal is
165+
// left after all prepare phases, and the complete of all resources including the NHibernate
166+
// one is concurrently raised. So the wait should indeed only have to wait after NHibernate
167+
// AfterTransaction events.
166168
// Remove the block then throw.
167169
Unlock();
168170
throw new HibernateException(
@@ -204,11 +206,18 @@ private void Unlock()
204206
{
205207
try
206208
{
207-
return EnlistedTransaction.TransactionInformation.Status;
209+
// Cloned transaction is not disposed "unexpectedly", its status is accessible till context disposal.
210+
var status = EnlistedTransaction.TransactionInformation.Status;
211+
if (status != TransactionStatus.Active)
212+
return status;
213+
214+
// The clone status can be out of date when active, check the original one (which could be disposed if
215+
// the clone is out of date).
216+
return _originalTransaction.TransactionInformation.Status;
208217
}
209218
catch (ObjectDisposedException ode)
210219
{
211-
_logger.Warn("Completed transaction was already disposed, unable to get its status.", ode);
220+
_logger.Warn("Enlisted transaction status was wrongly active, original transaction being already disposed. Will assume neither active nor committed.", ode);
212221
return null;
213222
}
214223
}
@@ -225,7 +234,7 @@ void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
225234
{
226235
if (_sessionImplementor.ConnectionManager.IsConnected)
227236
{
228-
using (_sessionImplementor.ConnectionManager.BeginFlushingFromSystemTransaction(IsDistributed))
237+
using (_sessionImplementor.ConnectionManager.BeginFlushingFromSystemTransaction())
229238
{
230239
// Required when both connection auto-enlistment and session auto-enlistment are disabled.
231240
_sessionImplementor.JoinTransaction();
@@ -274,12 +283,7 @@ private void ProcessSecondPhase(Enlistment enlistment, bool? success)
274283
: "System transaction is in doubt");
275284
// we have not much to do here, since it is the actual
276285
// DB connection that will commit/rollback the transaction
277-
// Usual cases will raise after transaction actions from TransactionCompleted event.
278-
if (!success.HasValue)
279-
{
280-
// In-doubt. A durable ressource has failed and may recover, but we won't wait to know.
281-
RunAfterTransactionActions(false);
282-
}
286+
// After transaction actions are raised from TransactionCompleted event.
283287

284288
enlistment.Done();
285289
}
@@ -289,28 +293,15 @@ private void ProcessSecondPhase(Enlistment enlistment, bool? success)
289293

290294
private void TransactionCompleted(object sender, TransactionEventArgs e)
291295
{
292-
EnlistedTransaction.TransactionCompleted -= TransactionCompleted;
293296
// This event may execute before second phase, so we cannot try to get the success from second phase.
294297
// Using this event is required by example in case the prepare phase failed and called force rollback:
295298
// no second phase would occur for this ressource. Maybe this may happen in some other circumstances
296299
// too.
297-
var wasSuccessful = GetTransactionStatus() == TransactionStatus.Committed;
298-
299-
RunAfterTransactionActions(wasSuccessful);
300-
}
301-
302-
private volatile bool _afterTransactionActionsDone;
303-
304-
private void RunAfterTransactionActions(bool wasSuccessful)
305-
{
306-
if (_afterTransactionActionsDone)
307-
// Probably called from In-Doubt and TransactionCompleted.
308-
return;
309-
_afterTransactionActionsDone = true;
310-
// Allow transaction completed actions to run while others stay blocked.
311-
_bypassLock.Value = true;
312300
try
313301
{
302+
EnlistedTransaction.TransactionCompleted -= TransactionCompleted;
303+
// Allow transaction completed actions to run while others stay blocked.
304+
_bypassLock.Value = true;
314305
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
315306
{
316307
// Flag active as false before running actions, otherwise the connection manager will refuse
@@ -324,6 +315,7 @@ private void RunAfterTransactionActions(bool wasSuccessful)
324315
if (!ShouldCloseSessionOnSystemTransactionCompleted)
325316
_sessionImplementor.ConnectionManager.EnlistIfRequired(null);
326317

318+
var wasSuccessful = GetTransactionStatus() == TransactionStatus.Committed;
327319
_sessionImplementor.AfterTransactionCompletion(wasSuccessful, null);
328320
foreach (var dependentSession in _sessionImplementor.ConnectionManager.DependentSessions)
329321
dependentSession.AfterTransactionCompletion(wasSuccessful, null);
@@ -368,7 +360,7 @@ private static void Cleanup(ISessionImplementor session)
368360
// No dispose, done later.
369361
}
370362

371-
private volatile bool _isDisposed;
363+
private bool _isDisposed;
372364

373365
public void Dispose()
374366
{
@@ -385,7 +377,7 @@ protected virtual void Dispose(bool disposing)
385377
if (disposing)
386378
{
387379
Unlock();
388-
EnlistedTransaction?.Dispose();
380+
EnlistedTransaction.Dispose();
389381
_lock.Dispose();
390382
}
391383
}

0 commit comments

Comments
 (0)