Skip to content

If DbTransaction.Dispose throws an exception, the AdoTransaction is left in an inconsistent state #2871

Closed
@jjeffery

Description

@jjeffery

As part of #2144, _isAlreadyDisposed is set to true earlier in the AdoTransaction.Dispose method than before.

One of the implications of this is that if the DbTransaction throws an exception in its Dispose method, then _isAlreadyDisposed is true, but begun might also still be true. Any attempt to dispose of the associated session will throw an InvalidOperationException with the text Disconnect cannot be called while an explicit transaction is in progress.

The end result is a session with an active transaction, neither of which can be disposed. This in turn causes other transactions to block for long periods of time.

As to why the DbTransaction is throwing an exception, I'll leave that for another issue. (Short summary: the MySQL connector can sometimes throw an exception in Dispose if an async query was cancelled). For now I thought it best to just focus on the fact that an AdoTransaction object enters an inconsistent state if the Dispose method throws an exception for any reason at all.

Having read #2144, my understanding is that the _isAlreadyDisposed field is set earlier to avoid recursive calls to the Dispose method. Another way to achieve the same thing might be to define another field:

AdoTransaction.cs:335

/// <summary>
/// A flage to indicate if <c>Dispose</c> is currently being called.
/// </summary>
private bool _isInDispose;

AdoTransaction.cs:364

protected virtual void Dispose(bool isDisposing)
{
	using (SessionIdLoggingContext.CreateOrNull(sessionId))
	{
		if (_isAlreadyDisposed || _isInDispose)
		{
			// don't dispose of multiple times.
			return;
		}

		try
		{
			_isInDispose = true;

			// free managed resources that are being managed by the AdoTransaction if we
			// know this call came through Dispose()
			if (isDisposing)
			{
				if (trans != null)
				{
					trans.Dispose();
					trans = null;
					log.Debug("DbTransaction disposed.");
				}

				if (IsActive && session != null)
				{
					// Assume we are rolled back
					AfterTransactionCompletion(false);
				}
				// nothing for Finalizer to do - so tell the GC to ignore it
				GC.SuppressFinalize(this);
			}
			// free unmanaged resources here
			_isAlreadyDisposed = true;
		}
		finally
		{
			_isInDispose = false;
		}
	}
}

If this is acceptable, I'm happy to submit a PR. Alternatively, if there is a better way to achieve this, I'm happy to change the approach.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions