Skip to content

Commit fafc546

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

File tree

3 files changed

+144
-46
lines changed

3 files changed

+144
-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: 130 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,61 @@ 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)
101+
return;
102+
_waitEvent.WaitOne();
103+
}
104+
83105
#region IEnlistmentNotification Members
84106

85107
void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
86108
{
87-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
109+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
88110
{
89111
try
90112
{
91113
using (var tx = new TransactionScope(AmbientTransation))
92114
{
93-
sessionImplementor.BeforeTransactionCompletion(null);
94-
if (sessionImplementor.FlushMode != FlushMode.Never && sessionImplementor.ConnectionManager.IsConnected)
115+
_sessionImplementor.BeforeTransactionCompletion(null);
116+
if (_sessionImplementor.FlushMode != FlushMode.Never && _sessionImplementor.ConnectionManager.IsConnected)
95117
{
96-
using (sessionImplementor.ConnectionManager.FlushingFromDtcTransaction)
118+
using (_sessionImplementor.ConnectionManager.FlushingFromDtcTransaction)
97119
{
98-
logger.Debug(string.Format("[session-id={0}] Flushing from Dtc Transaction", sessionImplementor.SessionId));
99-
sessionImplementor.Flush();
120+
_logger.DebugFormat("[session-id={0}] Flushing from Dtc Transaction", _sessionImplementor.SessionId);
121+
_sessionImplementor.Flush();
100122
}
101123
}
102-
logger.Debug("prepared for DTC transaction");
124+
_logger.Debug("prepared for DTC transaction");
103125

104126
tx.Complete();
105127
}
128+
// Lock the session to ensure second phase gets done before the session is used by code following
129+
// the transaction scope disposal.
130+
_waitEvent.Reset();
131+
106132
preparingEnlistment.Prepared();
107133
}
108134
catch (Exception exception)
109135
{
110-
logger.Error("DTC transaction prepare phase failed", exception);
136+
_logger.Error("DTC transaction prepare phase failed", exception);
111137
preparingEnlistment.ForceRollback(exception);
112138
}
113139
}
@@ -124,43 +150,106 @@ void IEnlistmentNotification.InDoubt(Enlistment enlistment)
124150

125151
private void ProcessSecondPhase(Enlistment enlistment, bool? success)
126152
{
127-
using (new SessionIdLoggingContext(sessionImplementor.SessionId))
153+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
128154
{
129-
logger.Debug(success.HasValue
130-
? success.Value ? "committing DTC transaction" : "rolled back DTC transaction"
131-
: "DTC transaction is in doubt");
155+
_logger.Debug(
156+
success.HasValue
157+
? success.Value
158+
? "committing DTC transaction"
159+
: "rolled back DTC transaction"
160+
: "DTC transaction is in doubt");
132161
// we have not much to do here, since it is the actual
133162
// 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)
163+
// Usual cases will raise after transaction actions from TransactionCompleted event.
164+
if (!success.HasValue)
147165
{
148-
sessionImplementor.CloseSessionFromDistributedTransaction();
166+
// In-doubt. A durable ressource has failed and may recover, but we won't wait to know.
167+
RunAfterTransactionActions(false);
149168
}
150-
sessionImplementor.TransactionContext = null;
151169

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.
154170
enlistment.Done();
155171
}
156172
}
157173

158174
#endregion
159175

176+
public void TransactionCompleted(object sender, TransactionEventArgs e)
177+
{
178+
e.Transaction.TransactionCompleted -= TransactionCompleted;
179+
// This event may execute before second phase, so we cannot try to get the success from second phase.
180+
// Using this event is required in case the prepare phase failed and called force rollback: no second
181+
// phase would occur for this ressource.
182+
var wasSuccessful = false;
183+
try
184+
{
185+
wasSuccessful = e.Transaction.TransactionInformation.Status
186+
== TransactionStatus.Committed;
187+
}
188+
catch (ObjectDisposedException ode)
189+
{
190+
_logger.Warn("Completed transaction was disposed, assuming transaction rollback", ode);
191+
}
192+
RunAfterTransactionActions(wasSuccessful);
193+
}
194+
195+
private volatile bool _afterTransactionActionDone;
196+
197+
private void RunAfterTransactionActions(bool wasSuccessful)
198+
{
199+
if (_afterTransactionActionDone)
200+
// Probably called from In-Doubt and TransactionCompleted.
201+
return;
202+
// Allow transaction completed actions to run while others stay blocked.
203+
_bypassWait.Value = true;
204+
try
205+
{
206+
using (new SessionIdLoggingContext(_sessionImplementor.SessionId))
207+
{
208+
// Flag active as false before running actions, otherwise the connection manager will refuse
209+
// releasing the connection.
210+
IsInActiveTransaction = false;
211+
_sessionImplementor.AfterTransactionCompletion(wasSuccessful, null);
212+
if (ShouldCloseSessionOnDistributedTransactionCompleted)
213+
{
214+
_sessionImplementor.CloseSessionFromDistributedTransaction();
215+
}
216+
_sessionImplementor.TransactionContext = null;
217+
}
218+
}
219+
finally
220+
{
221+
_afterTransactionActionDone = true;
222+
// Dispose releases blocked threads by the way.
223+
// Must dispose in case !ShouldCloseSessionOnDistributedTransactionCompleted, since
224+
// we nullify session TransactionContext, causing it to have nothing still holding it.
225+
Dispose();
226+
}
227+
}
228+
229+
private volatile bool _isDisposed;
230+
160231
public void Dispose()
161232
{
162-
if (AmbientTransation != null)
163-
AmbientTransation.Dispose();
233+
if (_isDisposed)
234+
// Avoid disposing twice (happen when ShouldCloseSessionOnDistributedTransactionCompleted).
235+
return;
236+
_isDisposed = true;
237+
Dispose(true);
238+
GC.SuppressFinalize(this);
239+
}
240+
241+
protected virtual void Dispose(bool disposing)
242+
{
243+
if (disposing)
244+
{
245+
if (AmbientTransation != null)
246+
{
247+
AmbientTransation.Dispose();
248+
AmbientTransation = null;
249+
}
250+
_waitEvent.Set();
251+
_waitEvent.Dispose();
252+
}
164253
}
165254
}
166255
}

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)