You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There's a risk DbDataReader won't be disposed of in GetResultSetAsync method within the src\NHibernate\Async\Loader\Loader.cs class. When a cancellation token is canceled after the DbDataReader is successfully obtained from session.Batcher.ExecuteReaderAsync() but during the AdvanceAsync method call, the reader is not closed.
The issue occurs due to the catch block for OperationCanceledException that throws without closing the reader:
I manually changed the NH source code to force the error to happen. Adding the following right after the DataReader initially gets set:
rs=await(session.Batcher.ExecuteReaderAsync(st,cancellationToken)).ConfigureAwait(false);// Added code on row 1064 to force OperationCanceledException:CancellationTokenSourcects=newCancellationTokenSource();cts.Cancel();cts.Token.ThrowIfCancellationRequested();
Then I added this test to src\NHibernate.Test\Async\Legacy\SQLLoaderTest.cs:
[Test]publicvoidExpiredCancellationTokenThrowsOperationCanceledException(){if(DialectisOracle8iDialect){return;}Assert.ThrowsAsync<OperationCanceledException>(()=>InnerExpiredCancellationTokenThrowsOperationCanceledException());}publicasyncTaskInnerExpiredCancellationTokenThrowsOperationCanceledException(){usingISessionsession=OpenSession();usingITransactiontxn=session.BeginTransaction();IQueryq=session.CreateSQLQuery(@" SELECT TOP 100000 a.*, b.* FROM sys.objects a CROSS JOIN sys.objects b ORDER BY a.name");await(q.ListAsync());}
This leads to an unexpected InvalidOperationException while rolling back the transaction rather than the expected OperationCanceledException.
Message:
Expected: <System.OperationCanceledException>
But was: <System.InvalidOperationException: There is already an open DataReader associated with this Command which must be closed first.
at System.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
at System.Data.SqlClient.SqlInternalTransaction.Rollback()
at System.Data.SqlClient.SqlInternalTransaction.Dispose(Boolean disposing)
at System.Data.SqlClient.SqlInternalTransaction.Dispose()
at System.Data.SqlClient.SqlTransaction.Dispose(Boolean disposing)
at NHibernate.Transaction.AdoTransaction.Dispose(Boolean isDisposing) in C:\dev\nhibernate-core\src\NHibernate\Transaction\AdoTransaction.cs:line 383
at NHibernate.Transaction.AdoTransaction.Dispose() in C:\dev\nhibernate-core\src\NHibernate\Transaction\AdoTransaction.cs:line 352
at NHibernate.Test.Legacy.SQLLoaderTestAsync.InnerExpiredCancellationTokenThrowsOperationCanceledException() in C:\dev\nhibernate-core\src\NHibernate.Test\Async\Legacy\SQLLoaderTest.cs:line 92
at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.BlockUntilCompleted()
at NUnit.Framework.Internal.MessagePumpStrategy.NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter)
at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
at NUnit.Framework.Assert.ThrowsAsync(IResolveConstraint expression, AsyncTestDelegate code, String message, Object[] args)>
Reproducing without manually canceling the token
It is not consistent, but I was able to reproduce with this test after 1-25 iterations:
publicasyncTaskInnerExpiredCancellationTokenThrowsOperationCanceledException(){usingvarcts=newCancellationTokenSource(100);usingISessionsession=OpenSession();usingITransactiontxn=session.BeginTransaction();IQueryq=session.CreateSQLQuery(@" SELECT TOP 100000 a.*, b.* FROM sys.objects a CROSS JOIN sys.objects b ORDER BY a.name");await(q.ListAsync(cts.Token));}
Possible Resolution
This seems to be generated code, but the goal would be to somehow make sure at least session.Batcher.CloseReader(rs); gets called in case of OperationCanceledException.
It might be that session.Batcher.CloseCommand(st, rs); should be used, as in the generic catch block? Calling any of those would ensure the DbDataReader gets closed which would allow a transaction to rollback without encountering InvalidOperationException that hides the original exception.
Description
There's a risk
DbDataReader
won't be disposed of inGetResultSetAsync
method within thesrc\NHibernate\Async\Loader\Loader.cs
class. When a cancellation token is canceled after theDbDataReader
is successfully obtained fromsession.Batcher.ExecuteReaderAsync()
but during theAdvanceAsync
method call, the reader is not closed.The issue occurs due to the catch block for
OperationCanceledException
that throws without closing the reader:Steps to Reproduce
I manually changed the NH source code to force the error to happen. Adding the following right after the DataReader initially gets set:
Then I added this test to
src\NHibernate.Test\Async\Legacy\SQLLoaderTest.cs
:This leads to an unexpected
InvalidOperationException
while rolling back the transaction rather than the expectedOperationCanceledException
.Reproducing without manually canceling the token
It is not consistent, but I was able to reproduce with this test after 1-25 iterations:
Possible Resolution
This seems to be generated code, but the goal would be to somehow make sure at least
session.Batcher.CloseReader(rs);
gets called in case ofOperationCanceledException
.It might be that
session.Batcher.CloseCommand(st, rs);
should be used, as in the generic catch block? Calling any of those would ensure theDbDataReader
gets closed which would allow a transaction to rollback without encounteringInvalidOperationException
that hides the original exception.Related issues
maca88/AsyncGenerator#90
dotnet/SqlClient#26 (Makes this less likely to happen)
The text was updated successfully, but these errors were encountered: