-
Notifications
You must be signed in to change notification settings - Fork 932
/
AdoNetWithSystemTransactionFactory.cs
588 lines (533 loc) · 22.2 KB
/
AdoNetWithSystemTransactionFactory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Transactions;
using NHibernate.AdoNet;
using NHibernate.Engine;
using NHibernate.Engine.Transaction;
using NHibernate.Impl;
using NHibernate.Util;
namespace NHibernate.Transaction
{
/// <summary>
/// <see cref="ITransaction"/> factory implementation supporting system
/// <see cref="System.Transactions.Transaction"/>.
/// </summary>
public partial class AdoNetWithSystemTransactionFactory : AdoNetTransactionFactory
{
private static readonly INHibernateLogger _logger = NHibernateLogger.For(typeof(ITransactionFactory));
/// <summary>
/// See <see cref="Cfg.Environment.SystemTransactionCompletionLockTimeout"/>.
/// </summary>
protected int SystemTransactionCompletionLockTimeout { get; private set; }
/// <summary>
/// See <see cref="Cfg.Environment.UseConnectionOnSystemTransactionPrepare"/>.
/// </summary>
protected bool UseConnectionOnSystemTransactionPrepare { get; private set; }
/// <inheritdoc />
public override void Configure(IDictionary<string, string> props)
{
base.Configure(props);
SystemTransactionCompletionLockTimeout =
PropertiesHelper.GetInt32(Cfg.Environment.SystemTransactionCompletionLockTimeout, props, 5000);
if (SystemTransactionCompletionLockTimeout < -1)
throw new HibernateException(
$"Invalid {Cfg.Environment.SystemTransactionCompletionLockTimeout} value: {SystemTransactionCompletionLockTimeout}. It can not be less than -1.");
UseConnectionOnSystemTransactionPrepare =
PropertiesHelper.GetBoolean(Cfg.Environment.UseConnectionOnSystemTransactionPrepare, props, true);
}
/// <inheritdoc />
public override void EnlistInSystemTransactionIfNeeded(ISessionImplementor session)
{
if (session == null)
throw new ArgumentNullException(nameof(session));
if (!session.ConnectionManager.ShouldAutoJoinTransaction)
{
return;
}
JoinSystemTransaction(session, System.Transactions.Transaction.Current);
}
/// <inheritdoc />
public override void ExplicitJoinSystemTransaction(ISessionImplementor session)
{
if (session == null)
throw new ArgumentNullException(nameof(session));
var transaction = System.Transactions.Transaction.Current;
if (transaction == null)
throw new HibernateException("No current system transaction to join.");
JoinSystemTransaction(session, transaction);
}
/// <summary>
/// Enlist the session in the supplied transaction.
/// </summary>
/// <param name="session">The session to enlist.</param>
/// <param name="transaction">The transaction to enlist with. Can be <see langword="null"/>.</param>
protected virtual void JoinSystemTransaction(ISessionImplementor session, System.Transactions.Transaction transaction)
{
// Handle the transaction on the originating session only.
var originatingSession = session.ConnectionManager.Session;
if (originatingSession.TransactionContext == null ||
// Support connection switch when connection auto-enlistment is not enabled
originatingSession.ConnectionManager.ProcessingFromSystemTransaction)
{
originatingSession.ConnectionManager.EnlistIfRequired(transaction);
}
if (transaction == null)
return;
if (originatingSession.TransactionContext != null)
{
if (session.TransactionContext == null)
{
// New dependent session
EnlistDependentSession(session, originatingSession.TransactionContext);
}
return;
}
var transactionContext = CreateAndEnlistMainContext(originatingSession, transaction);
originatingSession.TransactionContext = transactionContext;
_logger.Debug(
"Enlisted into system transaction: {0}",
transaction.IsolationLevel);
originatingSession.AfterTransactionBegin(null);
foreach (var dependentSession in originatingSession.ConnectionManager.DependentSessions)
{
EnlistDependentSession(dependentSession, transactionContext);
}
}
/// <summary>
/// Create a transaction context for enlisting a session with a <see cref="System.Transactions.Transaction"/>,
/// and enlist the context in the transaction.
/// </summary>
/// <param name="session">The session to be enlisted.</param>
/// <param name="transaction">The transaction into which the context has to be enlisted.</param>
/// <returns>The created transaction context.</returns>
protected virtual ITransactionContext CreateAndEnlistMainContext(
ISessionImplementor session,
System.Transactions.Transaction transaction)
{
var transactionContext = new SystemTransactionContext(
session, transaction, SystemTransactionCompletionLockTimeout,
UseConnectionOnSystemTransactionPrepare);
transactionContext.EnlistedTransaction.EnlistVolatile(
transactionContext,
UseConnectionOnSystemTransactionPrepare
? EnlistmentOptions.EnlistDuringPrepareRequired
: EnlistmentOptions.None);
return transactionContext;
}
private void EnlistDependentSession(ISessionImplementor dependentSession, ITransactionContext mainContext)
{
dependentSession.TransactionContext = CreateDependentContext(dependentSession, mainContext);
dependentSession.AfterTransactionBegin(null);
}
/// <summary>
/// Create a transaction context for a dependent session.
/// </summary>
/// <param name="dependentSession">The dependent session.</param>
/// <param name="mainContext">The context of the session owning the <see cref="ConnectionManager"/>.</param>
/// <returns>A dependent context for the session.</returns>
protected virtual ITransactionContext CreateDependentContext(ISessionImplementor dependentSession, ITransactionContext mainContext)
{
return new DependentContext(mainContext);
}
/// <inheritdoc />
public override bool IsInActiveSystemTransaction(ISessionImplementor session)
=> session?.TransactionContext?.IsInActiveTransaction ?? false;
/// <inheritdoc />
public override void ExecuteWorkInIsolation(ISessionImplementor session, IIsolatedWork work, bool transacted)
{
using (var tx = new TransactionScope(TransactionScopeOption.Suppress))
{
base.ExecuteWorkInIsolation(session, work, transacted);
tx.Complete();
}
}
/// <summary>
/// Transaction context for enlisting a session with a system <see cref="System.Transactions.Transaction"/>.
/// It is meant for being the concrete class enlisted in the transaction.
/// </summary>
public class SystemTransactionContext : ITransactionContext, IEnlistmentNotification
{
/// <summary>
/// The transaction in which this context is enlisted.
/// </summary>
protected internal System.Transactions.Transaction EnlistedTransaction { get; }
/// <inheritdoc />
public bool ShouldCloseSessionOnSystemTransactionCompleted { get; set; }
/// <inheritdoc />
public bool IsInActiveTransaction { get; protected set; } = true;
/// <inheritdoc />
public virtual bool CanFlushOnSystemTransactionCompleted => _useConnectionOnSystemTransactionPrepare;
private readonly ISessionImplementor _session;
private readonly bool _useConnectionOnSystemTransactionPrepare;
private readonly System.Transactions.Transaction _originalTransaction;
private readonly ManualResetEventSlim _lock = new ManualResetEventSlim(true);
private volatile bool _needCompletionLocking = true;
// Required for not locking the completion phase itself when locking session usages from concurrent threads.
private static readonly AsyncLocal<bool> _bypassLock = new AsyncLocal<bool>();
private readonly int _systemTransactionCompletionLockTimeout;
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="session">The session to enlist with the transaction.</param>
/// <param name="transaction">The transaction into which the context will be enlisted.</param>
/// <param name="systemTransactionCompletionLockTimeout">See <see cref="Cfg.Environment.SystemTransactionCompletionLockTimeout"/>.</param>
/// <param name="useConnectionOnSystemTransactionPrepare">See <see cref="Cfg.Environment.UseConnectionOnSystemTransactionPrepare"/>.</param>
public SystemTransactionContext(
ISessionImplementor session,
System.Transactions.Transaction transaction,
int systemTransactionCompletionLockTimeout,
bool useConnectionOnSystemTransactionPrepare)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_originalTransaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
EnlistedTransaction = transaction.Clone();
_systemTransactionCompletionLockTimeout = systemTransactionCompletionLockTimeout;
_useConnectionOnSystemTransactionPrepare = useConnectionOnSystemTransactionPrepare;
}
/// <inheritdoc />
public virtual void Wait()
{
if (_isDisposed)
return;
if (_needCompletionLocking && GetTransactionStatus() != TransactionStatus.Active)
{
// Rollback case may end the transaction without a prepare phase, apply the lock.
Lock();
}
if (_bypassLock.Value)
return;
try
{
if (_lock.Wait(_systemTransactionCompletionLockTimeout))
return;
// A call occurring after transaction scope disposal should not have to wait long, since
// the scope disposal is supposed to block until the transaction has completed. When not
// distributed, all is done, no wait. When distributed, with MSDTC, the scope disposal is
// left after all prepare phases, and the complete of all resources including the NHibernate
// one is concurrently raised. So the wait should indeed only have to wait after NHibernate
// AfterTransaction events.
// Remove the block then throw.
Unlock();
throw new HibernateException(
"Synchronization timeout for transaction completion. Either raise {Cfg.Environment.SystemTransactionCompletionLockTimeout}, or this may be a bug in NHibernate.");
}
catch (HibernateException)
{
throw;
}
catch (Exception ex)
{
_logger.Warn(
ex,
"Synchronization failure, assuming it has been concurrently disposed and does not need sync anymore.");
}
}
/// <summary>
/// Lock the context, causing <see cref="Wait"/> to block until released. Do nothing if the context
/// has already been locked once.
/// </summary>
protected virtual void Lock()
{
if (!_needCompletionLocking || _isDisposed)
return;
_needCompletionLocking = false;
_lock.Reset();
}
/// <summary>
/// Unlock the context, causing <see cref="Wait"/> to cease blocking. Do nothing if the context
/// is not locked.
/// </summary>
protected virtual void Unlock()
{
_lock.Set();
_bypassLock.Value = false;
}
/// <summary>
/// Safely get the <see cref="TransactionStatus"/> of the context transaction.
/// </summary>
/// <returns>The <see cref="TransactionStatus"/> of the context transaction, or <see langword="null"/>
/// if it cannot be obtained.</returns>
/// <remarks>The status may no more be obtainable during transaction completion events in case of
/// rollback.</remarks>
protected TransactionStatus? GetTransactionStatus()
{
try
{
// Cloned transaction is not disposed "unexpectedly", its status is accessible till context disposal.
var status = EnlistedTransaction.TransactionInformation.Status;
if (status != TransactionStatus.Active)
return status;
// The clone status can be out of date when active, check the original one (which could be disposed if
// the clone is out of date).
return _originalTransaction.TransactionInformation.Status;
}
catch (ObjectDisposedException ode)
{
_logger.Warn(ode, "Enlisted transaction status was wrongly active, original transaction being already disposed. Will assume neither active nor committed.");
return null;
}
}
#region IEnlistmentNotification Members
/// <summary>
/// Prepare the session for the transaction commit. Run
/// <see cref="ISessionImplementor.BeforeTransactionCompletion(ITransaction)"/> for the session and for
/// <see cref="ConnectionManager.DependentSessions"/> if any. <see cref="Lock"/> the context
/// before signaling it is done, or before rollback in case of failure.
/// </summary>
/// <param name="preparingEnlistment">The object for notifying the prepare phase outcome.</param>
public virtual void Prepare(PreparingEnlistment preparingEnlistment)
{
using (_session.BeginContext())
{
try
{
using (_session.ConnectionManager.BeginProcessingFromSystemTransaction(_useConnectionOnSystemTransactionPrepare))
{
if (_useConnectionOnSystemTransactionPrepare)
{
// Ensure any newly acquired connection gets enlisted in the transaction. When distributed,
// this code runs from another thread and we cannot rely on Transaction.Current.
using (var tx = new TransactionScope(EnlistedTransaction))
{
// Required when both connection auto-enlistment and session auto-enlistment are disabled.
_session.JoinTransaction();
_session.BeforeTransactionCompletion(null);
foreach (var dependentSession in _session.ConnectionManager.DependentSessions)
dependentSession.BeforeTransactionCompletion(null);
tx.Complete();
}
}
else
{
_session.BeforeTransactionCompletion(null);
foreach (var dependentSession in _session.ConnectionManager.DependentSessions)
dependentSession.BeforeTransactionCompletion(null);
}
}
// Lock the session to ensure second phase gets done before the session is used by code following
// the transaction scope disposal.
Lock();
_logger.Debug("Prepared for system transaction");
preparingEnlistment.Prepared();
}
catch (Exception exception)
{
_logger.Error(exception, "System transaction prepare phase failed");
try
{
CompleteTransaction(false);
}
finally
{
preparingEnlistment.ForceRollback(exception);
}
}
}
}
void IEnlistmentNotification.Commit(Enlistment enlistment)
=> ProcessSecondPhase(enlistment, true);
// May be called in case of scope disposal without being completed, on transaction timeout or other failure like
// deadlocks. Not called in case of ForceRollback from the prepare phase of this enlistment.
void IEnlistmentNotification.Rollback(Enlistment enlistment)
=> ProcessSecondPhase(enlistment, false);
void IEnlistmentNotification.InDoubt(Enlistment enlistment)
=> ProcessSecondPhase(enlistment, null);
/// <summary>
/// Handle the second phase callbacks. Has no actual work to do excepted signaling it is done.
/// </summary>
/// <param name="enlistment">The enlistment object for signaling to the transaction manager the notification has been handled.</param>
/// <param name="success"><see langword="true"/> if this is a commit callback, <see langword="false"/> if this is a rollback
/// callback, <see langword="null"/> if this is an in-doubt callback.</param>
protected virtual void ProcessSecondPhase(Enlistment enlistment, bool? success)
{
using (_session.BeginContext())
{
_logger.Debug(
success.HasValue
? success.Value
? "Committing system transaction"
: "Rolled back system transaction"
: "System transaction is in doubt");
try
{
CompleteTransaction(success ?? false);
}
finally
{
enlistment.Done();
}
}
}
#endregion
/// <summary>
/// Handle the transaction completion. Notify <see cref="ConnectionManager"/> of the end of the
/// transaction. Notify end of transaction to the session and to <see cref="ConnectionManager.DependentSessions"/>
/// if any. Close sessions requiring it then cleanup transaction contextes and then <see cref="Unlock"/> blocked
/// threads.
/// </summary>
/// <param name="isCommitted"><see langword="true"/> if the transaction is committed, <see langword="false"/>
/// otherwise.</param>
protected virtual void CompleteTransaction(bool isCommitted)
{
// Some implementations (Mono) may "re-complete" the transaction on the cloned transaction disposal:
// do an early exit here in such case.
if (!IsInActiveTransaction)
return;
try
{
// Allow transaction completed actions to run while others stay blocked.
_bypassLock.Value = true;
using (_session.BeginContext())
{
// Flag active as false before running actions, otherwise the session may not cleanup as much
// as possible.
IsInActiveTransaction = false;
// Never allows using connection on after transaction event. And tell the connection manager
// it is called from system transaction. Allows releasing of connection on next usage
// when release mode is on commit, allows un-enlisting the connection on next usage
// when release mode is on close. Without BeginsProcessingFromSystemTransaction(false),
// the connection manager would attempt those operations immediately, causing concurrency
// issues and crashes for some data providers.
using (_session.ConnectionManager.BeginProcessingFromSystemTransaction(false))
{
_session.ConnectionManager.AfterTransaction();
// Required for un-enlisting the connection manager when auto-join is false.
// Not done in AfterTransaction, because users may use NHibernate transactions
// within scopes, although mixing is not advised.
if (!ShouldCloseSessionOnSystemTransactionCompleted)
_session.ConnectionManager.EnlistIfRequired(null);
_session.AfterTransactionCompletion(isCommitted, null);
foreach (var dependentSession in _session.ConnectionManager.DependentSessions)
dependentSession.AfterTransactionCompletion(isCommitted, null);
Cleanup(_session);
}
}
}
catch (Exception ex)
{
// May be run in a dedicated thread. Log any error, otherwise they could stay unlogged.
_logger.Error(ex, "Failure at transaction completion");
throw;
}
finally
{
// Dispose releases blocked threads by the way.
Dispose();
}
}
private static void Cleanup(ISessionImplementor session)
{
foreach (var dependentSession in session.ConnectionManager.DependentSessions.ToList())
{
var dependentContext = dependentSession.TransactionContext;
// Do not nullify TransactionContext here, could create a race condition with
// would be await-er on session for disposal (test cases cleanup checks by example).
if (dependentContext == null)
continue;
// Race condition with session disposal is protected on session side by Wait.
if (dependentContext.ShouldCloseSessionOnSystemTransactionCompleted)
// This changes the enumerated collection.
dependentSession.CloseSessionFromSystemTransaction();
// Now we can (and even must) nullify it.
dependentSession.TransactionContext = null;
dependentContext.Dispose();
}
var context = session.TransactionContext;
// Do not nullify TransactionContext here, could create a race condition with
// would be await-er on session for disposal (test cases cleanup checks by example).
// Race condition with session disposal is protected on session side by Wait.
if (context.ShouldCloseSessionOnSystemTransactionCompleted)
{
// This closes the connection manager, which will release the connection.
// This can cause issues with the connection own second phase and concurrency issues
// when the transaction is distributed. In such case, user needs to disable
// UseConnectionOnSystemTransactionPrepare.
session.CloseSessionFromSystemTransaction();
}
// Now we can (and even must) nullify it.
session.TransactionContext = null;
// No context dispose, done later.
}
private bool _isDisposed;
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed)
// Avoid disposing twice.
return;
_isDisposed = true;
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of the context.
/// </summary>
/// <param name="disposing"><see langword="true" /> if called by <see cref="Dispose()"/>.
/// <see langword="false" /> otherwise. Do not access managed resources if it is
/// <c>false</c>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Unlock();
EnlistedTransaction.Dispose();
_lock.Dispose();
}
}
}
/// <summary>
/// Transaction context for enlisting a dependent session. Dependent sessions are not owning
/// their <see cref="ConnectionManager"/>. The session owning it will have a transaction context
/// handling all actions for dependent sessions.
/// </summary>
public class DependentContext : ITransactionContext
{
/// <inheritdoc />
public bool IsInActiveTransaction
=> MainTransactionContext.IsInActiveTransaction;
/// <inheritdoc />
public bool ShouldCloseSessionOnSystemTransactionCompleted { get; set; }
/// <inheritdoc />
public virtual bool CanFlushOnSystemTransactionCompleted
=> MainTransactionContext.CanFlushOnSystemTransactionCompleted;
/// <summary>
/// The transaction context of the session owning the <see cref="ConnectionManager"/>.
/// </summary>
protected ITransactionContext MainTransactionContext { get; }
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="mainTransactionContext">The transaction context of the session owning the
/// <see cref="ConnectionManager"/>.</param>
public DependentContext(ITransactionContext mainTransactionContext)
{
MainTransactionContext = mainTransactionContext ?? throw new ArgumentNullException(nameof(mainTransactionContext));
}
/// <inheritdoc />
public virtual void Wait() =>
MainTransactionContext.Wait();
private bool _isDisposed;
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed)
// Avoid disposing twice.
return;
_isDisposed = true;
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of the context.
/// </summary>
/// <param name="disposing"><see langword="true" /> if called by <see cref="Dispose()"/>.
/// <see langword="false" /> otherwise. Do not access managed resources if it is
/// <c>false</c>.</param>
protected virtual void Dispose(bool disposing)
{
}
}
}
}