Skip to content

Commit cff5d98

Browse files
Handle the case of concurrent session disposal
1 parent 5a1f3f4 commit cff5d98

File tree

3 files changed

+45
-18
lines changed

3 files changed

+45
-18
lines changed

src/NHibernate/Impl/AbstractSessionImpl.cs

+34-12
Original file line numberDiff line numberDiff line change
@@ -410,16 +410,31 @@ public bool IsClosed
410410
public IDisposable BeginProcess()
411411
{
412412
return _processHelper.BeginProcess(this);
413-
}
414-
415-
/// <summary>
416-
/// If not nested in a call to <c>BeginProcess</c> on this session, set its session id in context.
417-
/// </summary>
418-
/// <returns>
419-
/// If not already processing, an object to dispose for restoring the previous session id.
420-
/// Otherwise, <see langword="null" />.
421-
/// </returns>
422-
public IDisposable BeginContext()
413+
}
414+
415+
/// <summary>
416+
/// If not nested in another call to <c>BeginProcess</c> on this session, optionnaly check
417+
/// and update the session status, then set its session id in context and flag it as processing.
418+
/// </summary>
419+
/// <param name="noCheckAndUpdate"><see langword="true" /> to initiate a processing without
420+
/// checking and updating the session.</param>
421+
/// <returns>
422+
/// If not already processing, an object to dispose for signaling the end of the process.
423+
/// Otherwise, <see langword="null" />.
424+
/// </returns>
425+
protected IDisposable BeginProcess(bool noCheckAndUpdate)
426+
{
427+
return _processHelper.BeginProcess(this, noCheckAndUpdate);
428+
}
429+
430+
/// <summary>
431+
/// If not nested in a call to <c>BeginProcess</c> on this session, set its session id in context.
432+
/// </summary>
433+
/// <returns>
434+
/// If not already processing, an object to dispose for restoring the previous session id.
435+
/// Otherwise, <see langword="null" />.
436+
/// </returns>
437+
public IDisposable BeginContext()
423438
{
424439
return _processHelper.Processing ? null : SessionIdLoggingContext.CreateOrNull(SessionId);
425440
}
@@ -442,15 +457,22 @@ public ProcessHelper()
442457

443458
public bool Processing { get => _processing; }
444459

445-
public IDisposable BeginProcess(AbstractSessionImpl session)
460+
public IDisposable BeginProcess(AbstractSessionImpl session, bool noCheckAndUpdate = false)
446461
{
447462
if (_processing)
448463
return null;
449464

450465
try
451466
{
452467
_context = SessionIdLoggingContext.CreateOrNull(session.SessionId);
453-
session.CheckAndUpdateSessionStatus();
468+
if (noCheckAndUpdate)
469+
{
470+
session.TransactionContext?.Wait();
471+
}
472+
else
473+
{
474+
session.CheckAndUpdateSessionStatus();
475+
}
454476
_processing = true;
455477
}
456478
catch

src/NHibernate/Impl/SessionImpl.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -1488,13 +1488,12 @@ public void Reconnect(DbConnection conn)
14881488
/// </summary>
14891489
public void Dispose()
14901490
{
1491-
using (BeginContext())
1491+
// Ensure we are not disposing concurrently to transaction completion, which would
1492+
// remove the transaction context.
1493+
using (BeginProcess(true))
14921494
{
14931495
log.Debug("[session-id={0}] running ISession.Dispose()", SessionId);
1494-
// Ensure we are not disposing concurrently to transaction completion, which would
1495-
// remove the context. (Do not store it into a local variable before the Wait.)
1496-
TransactionContext?.Wait();
1497-
// If the synchronization above is bugged and lets a race condition remaining, we may
1496+
// If the BeginProcess synchronization is bugged and lets a race condition remaining, we may
14981497
// blow here with a null ref exception after the null check. We could introduce
14991498
// a local variable for avoiding it, but that would turn a failure causing an exception
15001499
// into a failure causing a session and connection leak. So do not do it, better blow away

src/NHibernate/Transaction/AdoNetWithSystemTransactionFactory.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,13 @@ protected virtual void CompleteTransaction(bool isCommitted)
485485
if (!IsInActiveTransaction)
486486
return;
487487
Lock();
488-
var isSessionProcessing = _session.IsProcessing();
488+
// In case of a rollback due to a timeout, we may have the session disposal running concurrently
489+
// to the transaction completion in a way our current locking mechanism cannot fully protect: the
490+
// session disposal "BeginProcess" can go through the Wait before it is locked but flag the
491+
// session as procesisng after the transaction completion has read it as not processing. To dodge
492+
// that case, we consider the session as still processing initially regardless of its actual
493+
// status in case of rollback.
494+
var isSessionProcessing = !isCommitted || _session.IsProcessing();
489495
try
490496
{
491497
// Allow transaction completed actions to run while others stay blocked.

0 commit comments

Comments
 (0)