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,61 @@ 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 )
101
+ return ;
102
+ _waitEvent . WaitOne ( ) ;
103
+ }
104
+
83
105
#region IEnlistmentNotification Members
84
106
85
107
void IEnlistmentNotification . Prepare ( PreparingEnlistment preparingEnlistment )
86
108
{
87
- using ( new SessionIdLoggingContext ( sessionImplementor . SessionId ) )
109
+ using ( new SessionIdLoggingContext ( _sessionImplementor . SessionId ) )
88
110
{
89
111
try
90
112
{
91
113
using ( var tx = new TransactionScope ( AmbientTransation ) )
92
114
{
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 )
95
117
{
96
- using ( sessionImplementor . ConnectionManager . FlushingFromDtcTransaction )
118
+ using ( _sessionImplementor . ConnectionManager . FlushingFromDtcTransaction )
97
119
{
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 ( ) ;
100
122
}
101
123
}
102
- logger . Debug ( "prepared for DTC transaction" ) ;
124
+ _logger . Debug ( "prepared for DTC transaction" ) ;
103
125
104
126
tx . Complete ( ) ;
105
127
}
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
+
106
132
preparingEnlistment . Prepared ( ) ;
107
133
}
108
134
catch ( Exception exception )
109
135
{
110
- logger . Error ( "DTC transaction prepare phase failed" , exception ) ;
136
+ _logger . Error ( "DTC transaction prepare phase failed" , exception ) ;
111
137
preparingEnlistment . ForceRollback ( exception ) ;
112
138
}
113
139
}
@@ -124,43 +150,106 @@ void IEnlistmentNotification.InDoubt(Enlistment enlistment)
124
150
125
151
private void ProcessSecondPhase ( Enlistment enlistment , bool ? success )
126
152
{
127
- using ( new SessionIdLoggingContext ( sessionImplementor . SessionId ) )
153
+ using ( new SessionIdLoggingContext ( _sessionImplementor . SessionId ) )
128
154
{
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" ) ;
132
161
// we have not much to do here, since it is the actual
133
162
// 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 )
147
165
{
148
- sessionImplementor . CloseSessionFromDistributedTransaction ( ) ;
166
+ // In-doubt. A durable ressource has failed and may recover, but we won't wait to know.
167
+ RunAfterTransactionActions ( false ) ;
149
168
}
150
- sessionImplementor . TransactionContext = null ;
151
169
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
170
enlistment . Done ( ) ;
155
171
}
156
172
}
157
173
158
174
#endregion
159
175
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
+
160
231
public void Dispose ( )
161
232
{
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
+ }
164
253
}
165
254
}
166
255
}
0 commit comments