Skip to content

Commit 2a4c2a5

Browse files
NH-2176 - adding thread synchronization in the mix.
1 parent 5d167af commit 2a4c2a5

File tree

3 files changed

+163
-46
lines changed

3 files changed

+163
-46
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,13 @@ public void ShouldBeAbleToReleaseSuppliedConnectionAfterDistributedTransaction()
9292
connection?.Dispose();
9393
}
9494

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);
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();
100102

101103
// Prior to the patch, an InvalidOperationException exception would occur in the
102104
// TransactionCompleted delegate at this point with the message, "Disconnect cannot

src/NHibernate/Transaction/AdoNetWithDistributedTransactionFactory.cs

Lines changed: 149 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Threading;
34
using System.Transactions;
45
using NHibernate.Engine;
56
using NHibernate.Engine.Transaction;
@@ -9,9 +10,9 @@ namespace NHibernate.Transaction
910
{
1011
public class AdoNetWithDistributedTransactionFactory : ITransactionFactory
1112
{
12-
private static readonly IInternalLogger logger = LoggerProvider.LoggerFor(typeof(ITransactionFactory));
13+
private static readonly IInternalLogger _logger = LoggerProvider.LoggerFor(typeof(ITransactionFactory));
1314

14-
private readonly AdoNetTransactionFactory adoNetTransactionFactory = new AdoNetTransactionFactory();
15+
private readonly AdoNetTransactionFactory _adoNetTransactionFactory = new AdoNetTransactionFactory();
1516

1617
public void Configure(IDictionary props)
1718
{
@@ -24,8 +25,13 @@ public ITransaction CreateTransaction(ISessionImplementor session)
2425

2526
public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
2627
{
28+
// Ensure the session does not run on a thread supposed to be blocked, waiting
29+
// for transaction completion.
30+
session.TransactionContext?.WaitOne();
2731
if (session.TransactionContext != null)
32+
{
2833
return;
34+
}
2935

3036
var transaction = System.Transactions.Transaction.Current;
3137
if (transaction == null)
@@ -40,12 +46,15 @@ public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
4046
}
4147
var transactionContext = new DistributedTransactionContext(session, transaction);
4248
session.TransactionContext = transactionContext;
43-
logger.DebugFormat("enlisted into DTC transaction: {0}",
44-
transactionContext.AmbientTransation.IsolationLevel);
49+
_logger.DebugFormat(
50+
"enlisted into DTC transaction: {0}",
51+
transactionContext.AmbientTransation.IsolationLevel);
4552
session.AfterTransactionBegin(null);
4653

47-
transactionContext.AmbientTransation.EnlistVolatile(transactionContext,
48-
EnlistmentOptions.EnlistDuringPrepareRequired);
54+
transactionContext.AmbientTransation.TransactionCompleted += transactionContext.TransactionCompleted;
55+
transactionContext.AmbientTransation.EnlistVolatile(
56+
transactionContext,
57+
EnlistmentOptions.EnlistDuringPrepareRequired);
4958
}
5059

5160
public bool IsInDistributedActiveTransaction(ISessionImplementor session)
@@ -61,7 +70,7 @@ public void ExecuteWorkInIsolation(ISessionImplementor session, IIsolatedWork wo
6170
{
6271
// instead of duplicating the logic, we suppress the DTC transaction and create
6372
// our own transaction instead
64-
adoNetTransactionFactory.ExecuteWorkInIsolation(session, work, transacted);
73+
_adoNetTransactionFactory.ExecuteWorkInIsolation(session, work, transacted);
6574
tx.Complete();
6675
}
6776
}
@@ -70,44 +79,80 @@ public class DistributedTransactionContext : ITransactionContext, IEnlistmentNot
7079
{
7180
public System.Transactions.Transaction AmbientTransation { get; set; }
7281
public bool ShouldCloseSessionOnDistributedTransactionCompleted { get; set; }
73-
private readonly ISessionImplementor sessionImplementor;
82+
83+
private readonly ISessionImplementor _sessionImplementor;
84+
private readonly ManualResetEvent _waitEvent = new ManualResetEvent(true);
85+
private readonly AsyncLocal<bool> _bypassWait = new AsyncLocal<bool>();
86+
7487
public bool IsInActiveTransaction;
7588

76-
public DistributedTransactionContext(ISessionImplementor sessionImplementor, System.Transactions.Transaction transaction)
89+
public DistributedTransactionContext(
90+
ISessionImplementor sessionImplementor,
91+
System.Transactions.Transaction transaction)
7792
{
78-
this.sessionImplementor = sessionImplementor;
93+
_sessionImplementor = sessionImplementor;
7994
AmbientTransation = transaction.Clone();
8095
IsInActiveTransaction = true;
8196
}
8297

98+
public void WaitOne()
99+
{
100+
if (_bypassWait.Value || _isDisposed)
101+
return;
102+
try
103+
{
104+
if (!_waitEvent.WaitOne(5000))
105+
{
106+
// A call occurring after transaction scope disposal should not have to wait long, since
107+
// the scope disposal is supposed to block until the transaction has completed: I hope
108+
// that it at least ensure IO are done, even if experience shows DTC lets the scope
109+
// disposal leave before having finished with volatile ressources and
110+
// TransactionCompleted event.
111+
_waitEvent.Set();
112+
throw new HibernateException(
113+
"Synchronization timeout for transaction completion. This is very likely a bug in NHibernate.");
114+
}
115+
}
116+
catch(Exception ex)
117+
{
118+
_logger.Warn(
119+
"Synchronization failure, assuming it has been concurrently disposed and do not need sync anymore.",
120+
ex);
121+
}
122+
}
123+
83124
#region IEnlistmentNotification Members
84125

85126
void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
86127
{
87-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
128+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
88129
{
89130
try
90131
{
91132
using (var tx = new TransactionScope(AmbientTransation))
92133
{
93-
sessionImplementor.BeforeTransactionCompletion(null);
94-
if (sessionImplementor.FlushMode != FlushMode.Never && sessionImplementor.ConnectionManager.IsConnected)
134+
_sessionImplementor.BeforeTransactionCompletion(null);
135+
if (_sessionImplementor.FlushMode != FlushMode.Never && _sessionImplementor.ConnectionManager.IsConnected)
95136
{
96-
using (sessionImplementor.ConnectionManager.FlushingFromDtcTransaction)
137+
using (_sessionImplementor.ConnectionManager.FlushingFromDtcTransaction)
97138
{
98-
logger.Debug(string.Format("[session-id={0}] Flushing from Dtc Transaction", sessionImplementor.SessionId));
99-
sessionImplementor.Flush();
139+
_logger.DebugFormat("[session-id={0}] Flushing from Dtc Transaction", _sessionImplementor.SessionId);
140+
_sessionImplementor.Flush();
100141
}
101142
}
102-
logger.Debug("prepared for DTC transaction");
143+
_logger.Debug("prepared for DTC transaction");
103144

104145
tx.Complete();
105146
}
147+
// Lock the session to ensure second phase gets done before the session is used by code following
148+
// the transaction scope disposal.
149+
_waitEvent.Reset();
150+
106151
preparingEnlistment.Prepared();
107152
}
108153
catch (Exception exception)
109154
{
110-
logger.Error("DTC transaction prepare phase failed", exception);
155+
_logger.Error("DTC transaction prepare phase failed", exception);
111156
preparingEnlistment.ForceRollback(exception);
112157
}
113158
}
@@ -124,43 +169,106 @@ void IEnlistmentNotification.InDoubt(Enlistment enlistment)
124169

125170
private void ProcessSecondPhase(Enlistment enlistment, bool? success)
126171
{
127-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
172+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
128173
{
129-
logger.Debug(success.HasValue
130-
? success.Value ? "committing DTC transaction" : "rolled back DTC transaction"
131-
: "DTC transaction is in doubt");
174+
_logger.Debug(
175+
success.HasValue
176+
? success.Value
177+
? "committing DTC transaction"
178+
: "rolled back DTC transaction"
179+
: "DTC transaction is in doubt");
132180
// we have not much to do here, since it is the actual
133181
// DB connection that will commit/rollback the transaction
134-
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)
182+
// Usual cases will raise after transaction actions from TransactionCompleted event.
183+
if (!success.HasValue)
147184
{
148-
sessionImplementor.CloseSessionFromDistributedTransaction();
185+
// In-doubt. A durable ressource has failed and may recover, but we won't wait to know.
186+
RunAfterTransactionActions(false);
149187
}
150-
sessionImplementor.TransactionContext = null;
151188

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.
154189
enlistment.Done();
155190
}
156191
}
157192

158193
#endregion
159194

195+
public void TransactionCompleted(object sender, TransactionEventArgs e)
196+
{
197+
e.Transaction.TransactionCompleted -= TransactionCompleted;
198+
// This event may execute before second phase, so we cannot try to get the success from second phase.
199+
// Using this event is required in case the prepare phase failed and called force rollback: no second
200+
// phase would occur for this ressource.
201+
var wasSuccessful = false;
202+
try
203+
{
204+
wasSuccessful = e.Transaction.TransactionInformation.Status
205+
== TransactionStatus.Committed;
206+
}
207+
catch (ObjectDisposedException ode)
208+
{
209+
_logger.Warn("Completed transaction was disposed, assuming transaction rollback", ode);
210+
}
211+
RunAfterTransactionActions(wasSuccessful);
212+
}
213+
214+
private volatile bool _afterTransactionActionDone;
215+
216+
private void RunAfterTransactionActions(bool wasSuccessful)
217+
{
218+
if (_afterTransactionActionDone)
219+
// Probably called from In-Doubt and TransactionCompleted.
220+
return;
221+
// Allow transaction completed actions to run while others stay blocked.
222+
_bypassWait.Value = true;
223+
try
224+
{
225+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
226+
{
227+
// Flag active as false before running actions, otherwise the connection manager will refuse
228+
// releasing the connection.
229+
IsInActiveTransaction = false;
230+
_sessionImplementor.AfterTransactionCompletion(wasSuccessful, null);
231+
if (ShouldCloseSessionOnDistributedTransactionCompleted)
232+
{
233+
_sessionImplementor.CloseSessionFromDistributedTransaction();
234+
}
235+
_sessionImplementor.TransactionContext = null;
236+
}
237+
}
238+
finally
239+
{
240+
_afterTransactionActionDone = true;
241+
// Dispose releases blocked threads by the way.
242+
// Must dispose in case !ShouldCloseSessionOnDistributedTransactionCompleted, since
243+
// we nullify session TransactionContext, causing it to have nothing still holding it.
244+
Dispose();
245+
}
246+
}
247+
248+
private volatile bool _isDisposed;
249+
160250
public void Dispose()
161251
{
162-
if (AmbientTransation != null)
163-
AmbientTransation.Dispose();
252+
if (_isDisposed)
253+
// Avoid disposing twice (happen when ShouldCloseSessionOnDistributedTransactionCompleted).
254+
return;
255+
_isDisposed = true;
256+
Dispose(true);
257+
GC.SuppressFinalize(this);
258+
}
259+
260+
protected virtual void Dispose(bool disposing)
261+
{
262+
if (disposing)
263+
{
264+
if (AmbientTransation != null)
265+
{
266+
AmbientTransation.Dispose();
267+
AmbientTransation = null;
268+
}
269+
_waitEvent.Set();
270+
_waitEvent.Dispose();
271+
}
164272
}
165273
}
166274
}

src/NHibernate/Transaction/ITransactionContext.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,12 @@ namespace NHibernate.Transaction
1010
public interface ITransactionContext : IDisposable
1111
{
1212
bool ShouldCloseSessionOnDistributedTransactionCompleted { get; set; }
13+
/// <summary>
14+
/// With some transaction factory, synchronization of session may be required. This method should be called
15+
/// by session before each of its usage where a concurrent transaction completion action could cause a thread
16+
/// safety issue. This method is already called by ITransactionFactory.EnlistInDistributedTransactionIfNeeded
17+
/// if the factory requires synchronization.
18+
/// </summary>
19+
void WaitOne();
1320
}
1421
}

0 commit comments

Comments
 (0)