1
1
using System ;
2
2
using System . Collections ;
3
+ using System . Threading ;
3
4
using System . Transactions ;
4
5
using NHibernate . Engine ;
5
6
using NHibernate . Engine . Transaction ;
@@ -9,9 +10,9 @@ namespace NHibernate.Transaction
9
10
{
10
11
public class AdoNetWithDistributedTransactionFactory : ITransactionFactory
11
12
{
12
- private static readonly IInternalLogger logger = LoggerProvider . LoggerFor ( typeof ( ITransactionFactory ) ) ;
13
+ private static readonly IInternalLogger _logger = LoggerProvider . LoggerFor ( typeof ( ITransactionFactory ) ) ;
13
14
14
- private readonly AdoNetTransactionFactory adoNetTransactionFactory = new AdoNetTransactionFactory ( ) ;
15
+ private readonly AdoNetTransactionFactory _adoNetTransactionFactory = new AdoNetTransactionFactory ( ) ;
15
16
16
17
public void Configure ( IDictionary props )
17
18
{
@@ -24,8 +25,13 @@ public ITransaction CreateTransaction(ISessionImplementor session)
24
25
25
26
public void EnlistInDistributedTransactionIfNeeded ( ISessionImplementor session )
26
27
{
28
+ // Ensure the session does not run on a thread supposed to be blocked, waiting
29
+ // for transaction completion.
30
+ session . TransactionContext ? . WaitOne ( ) ;
27
31
if ( session . TransactionContext != null )
32
+ {
28
33
return ;
34
+ }
29
35
30
36
var transaction = System . Transactions . Transaction . Current ;
31
37
if ( transaction == null )
@@ -40,12 +46,15 @@ public void EnlistInDistributedTransactionIfNeeded(ISessionImplementor session)
40
46
}
41
47
var transactionContext = new DistributedTransactionContext ( session , transaction ) ;
42
48
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 ) ;
45
52
session . AfterTransactionBegin ( null ) ;
46
53
47
- transactionContext . AmbientTransation . EnlistVolatile ( transactionContext ,
48
- EnlistmentOptions . EnlistDuringPrepareRequired ) ;
54
+ transactionContext . AmbientTransation . TransactionCompleted += transactionContext . TransactionCompleted ;
55
+ transactionContext . AmbientTransation . EnlistVolatile (
56
+ transactionContext ,
57
+ EnlistmentOptions . EnlistDuringPrepareRequired ) ;
49
58
}
50
59
51
60
public bool IsInDistributedActiveTransaction ( ISessionImplementor session )
@@ -61,7 +70,7 @@ public void ExecuteWorkInIsolation(ISessionImplementor session, IIsolatedWork wo
61
70
{
62
71
// instead of duplicating the logic, we suppress the DTC transaction and create
63
72
// our own transaction instead
64
- adoNetTransactionFactory . ExecuteWorkInIsolation ( session , work , transacted ) ;
73
+ _adoNetTransactionFactory . ExecuteWorkInIsolation ( session , work , transacted ) ;
65
74
tx . Complete ( ) ;
66
75
}
67
76
}
@@ -70,44 +79,80 @@ public class DistributedTransactionContext : ITransactionContext, IEnlistmentNot
70
79
{
71
80
public System . Transactions . Transaction AmbientTransation { get ; set ; }
72
81
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
+
74
87
public bool IsInActiveTransaction ;
75
88
76
- public DistributedTransactionContext ( ISessionImplementor sessionImplementor , System . Transactions . Transaction transaction )
89
+ public DistributedTransactionContext (
90
+ ISessionImplementor sessionImplementor ,
91
+ System . Transactions . Transaction transaction )
77
92
{
78
- this . sessionImplementor = sessionImplementor ;
93
+ _sessionImplementor = sessionImplementor ;
79
94
AmbientTransation = transaction . Clone ( ) ;
80
95
IsInActiveTransaction = true ;
81
96
}
82
97
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
+
83
124
#region IEnlistmentNotification Members
84
125
85
126
void IEnlistmentNotification . Prepare ( PreparingEnlistment preparingEnlistment )
86
127
{
87
- using ( new SessionIdLoggingContext ( sessionImplementor . SessionId ) )
128
+ using ( new SessionIdLoggingContext ( _sessionImplementor . SessionId ) )
88
129
{
89
130
try
90
131
{
91
132
using ( var tx = new TransactionScope ( AmbientTransation ) )
92
133
{
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 )
95
136
{
96
- using ( sessionImplementor . ConnectionManager . FlushingFromDtcTransaction )
137
+ using ( _sessionImplementor . ConnectionManager . FlushingFromDtcTransaction )
97
138
{
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 ( ) ;
100
141
}
101
142
}
102
- logger . Debug ( "prepared for DTC transaction" ) ;
143
+ _logger . Debug ( "prepared for DTC transaction" ) ;
103
144
104
145
tx . Complete ( ) ;
105
146
}
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
+
106
151
preparingEnlistment . Prepared ( ) ;
107
152
}
108
153
catch ( Exception exception )
109
154
{
110
- logger . Error ( "DTC transaction prepare phase failed" , exception ) ;
155
+ _logger . Error ( "DTC transaction prepare phase failed" , exception ) ;
111
156
preparingEnlistment . ForceRollback ( exception ) ;
112
157
}
113
158
}
@@ -124,43 +169,106 @@ void IEnlistmentNotification.InDoubt(Enlistment enlistment)
124
169
125
170
private void ProcessSecondPhase ( Enlistment enlistment , bool ? success )
126
171
{
127
- using ( new SessionIdLoggingContext ( sessionImplementor . SessionId ) )
172
+ using ( new SessionIdLoggingContext ( _sessionImplementor . SessionId ) )
128
173
{
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" ) ;
132
180
// we have not much to do here, since it is the actual
133
181
// 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 )
147
184
{
148
- sessionImplementor . CloseSessionFromDistributedTransaction ( ) ;
185
+ // In-doubt. A durable ressource has failed and may recover, but we won't wait to know.
186
+ RunAfterTransactionActions ( false ) ;
149
187
}
150
- sessionImplementor . TransactionContext = null ;
151
188
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
189
enlistment . Done ( ) ;
155
190
}
156
191
}
157
192
158
193
#endregion
159
194
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
+
160
250
public void Dispose ( )
161
251
{
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
+ }
164
272
}
165
273
}
166
274
}
0 commit comments