Value cannot be null. Parameter name: toCache

Posts   
 
    
nweinit
User
Posts: 21
Joined: 16-Nov-2016
# Posted on: 28-Feb-2022 17:47:08   

I've recently reviewed our logs and noticed the following exception (with stack, from the LLBLGen level):

System.ArgumentNullException: Value cannot be null.
Parameter name: toCache
   at SD.LLBLGen.Pro.ORMSupportClasses.CacheController.CacheResultset(CacheKey key, String connectionString, TimeSpan duration, CachedResultset toCache, Boolean overwriteIfPresent, String tag)
   at SD.LLBLGen.Pro.ORMSupportClasses.RetrievalQuery.ReadComplete()
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityMaterializerBase.Materialize(Func`4 valueReadErrorHandler, String& failureErrorText)
   at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.ExecuteMultiRowRetrievalQuery(IRetrievalQuery queryToExecute, IEntityFactory2 entityFactory, IEntityCollection2 collectionToFill, IFieldPersistenceInfo[] fieldsPersistenceInfo, Boolean allowDuplicates, IEntityFields2 fieldsUsedForQuery)
   at <internal namespace>.DataEntities.DatabaseSpecific.DataAccessAdapter.ExecuteMultiRowRetrievalQuery(IRetrievalQuery queryToExecute, IEntityFactory2 entityFactory, IEntityCollection2 collectionToFill, IFieldPersistenceInfo[] fieldsPersistenceInfo, Boolean allowDuplicates, IEntityFields2 fieldsUsedForQuery) in D:\TeamCity\BuildAgent\work\8e9b704778219e91\Source\Powerex.TxMS.DataEntities\DatabaseSpecific\DataAccessAdapter.Custom.cs:line 165
   at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.FetchEntityCollectionInternal(QueryParameters parameters)
   at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.FetchEntityCollection(QueryParameters parameters)

I don't know how to reproduce it but I believe that since the error is inside your code, there's not much I can do to fix it anyways. We use LLBLGen 5.6.0.0 with Oracle 12c/19c, LLBLGen Pro Runtime Framework, .NET 4.8, SD.Presets.Adapter.General. I'm fine if you tell me this issue is fixed in later versions (which version?), if not, then... hopefully you can fix it simple_smile

Walaa avatar
Walaa
Support Team
Posts: 14824
Joined: 21-Aug-2005
# Posted on: 28-Feb-2022 23:13:27   

If you are using an old version of ODP.NET, could you please try the latest one?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 38963
Joined: 17-Aug-2003
# Posted on: 01-Mar-2022 10:02:44   

Hmm, looking at the code I don't think this is related to ODP.NET. The object that's null is the resultset, but there's a check before the call if the resultset is null, it'll be replaced by an empty one. That only fails if the reader is null which won't happen in this scenario.

So please provide a repro case with reproducible code that shows the problem. You posted a stacktrace with no context/code what's going on, and that's not enough to investigate the problem. Please keep in mind that e.g. adapter isn't thread safe. Don't share query objects etc. among threads.

Frans Bouma | Lead developer LLBLGen Pro
nweinit
User
Posts: 21
Joined: 16-Nov-2016
# Posted on: 01-Mar-2022 22:45:12   

This is the rest of the stack, i've removed anything that is internal to us as it won't mean anything to you anyway:

at SD.LLBLGen.Pro.ORMSupportClasses.CacheController.CacheResultset(CacheKey key, String connectionString, TimeSpan duration, CachedResultset toCache, Boolean overwriteIfPresent, String tag)
at SD.LLBLGen.Pro.ORMSupportClasses.RetrievalQuery.ReadComplete()
at SD.LLBLGen.Pro.ORMSupportClasses.EntityMaterializerBase.Materialize(Func`4 valueReadErrorHandler, String& failureErrorText)
at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.ExecuteMultiRowRetrievalQuery(IRetrievalQuery queryToExecute, IEntityFactory2 entityFactory, IEntityCollection2 collectionToFill, IFieldPersistenceInfo[] fieldsPersistenceInfo, Boolean allowDuplicates, IEntityFields2 fieldsUsedForQuery)
at <internal namespace>.DataEntities.DatabaseSpecific.DataAccessAdapter.ExecuteMultiRowRetrievalQuery(IRetrievalQuery queryToExecute, IEntityFactory2 entityFactory, IEntityCollection2 collectionToFill, IFieldPersistenceInfo[] fieldsPersistenceInfo, Boolean allowDuplicates, IEntityFields2 fieldsUsedForQuery) 
at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.FetchEntityCollectionInternal(QueryParameters parameters)
at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterCore.FetchEntityCollection(QueryParameters parameters)
...
...
...
at System.Threading.Tasks.Parallel.<>c__DisplayClass17_0`1.<ForWorker>b__1()
at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.Wait()
at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body, Action`2 bodyWithState, Func`4 bodyWithLocal, Func`1 localInit, Action`1 localFinally)
at System.Threading.Tasks.Parallel.For(Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body)
...
... 
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()

I cannot reproduce this; As mentioned I've just noticed it in the logs and it's not happening frequently at all. But shouldn't your code handle a null CachedResultset? Seeing that the object is passed internally in your code, I don't understand how anything that I can do would cause it to happen. However, since we are running this in parallel this might explain some things, especially since you've mentioned that the adapter is not thread safe; nonetheless, even though we run multiple queries in parallel, each thread creates its own adapter and it is not shared across the threads. Each thread receives an object with simple parameters which are then being converted to predicates and fetched.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 38963
Joined: 17-Aug-2003
# Posted on: 02-Mar-2022 09:48:45   

Code in RetrievalQuery:

ReadComplete:

public void ReadComplete()
{
    if(!this.CacheResultset || _resultsPulledFromCache)
    {
        return;
    }

    if(_cachedResultset is null)
    {
        // initialize the cached resultset. Apparently the resultset is empty, so we have to create an empty one
        InitCachedResultset();
    }
    CacheController.CacheResultset(_cacheKey, this.Connection.ConnectionString, this.CacheDuration, _cachedResultset, this.OverwriteIfPresent, this.CacheTag);
}

As the _cachedResultset variable passed to CacheResultset is null, the null check catches it. This can happen when the resultset is empty. InitCachedResultset takes care of that:

private void InitCachedResultset()
{
    if(!this.CacheResultset || _activeReader is null)
    {
        return;
    }
    var columnNames = new List<string>(_activeReader.FieldCount);
    var typePerOrdinal = new List<Type>(_activeReader.FieldCount);
    for(int i = 0; i < _activeReader.FieldCount; i++)
    {
        columnNames.Add(_activeReader.GetName(i));
        typePerOrdinal.Add(_activeReader.GetFieldType(i));
    }

    _cachedResultset = new CachedResultset(columnNames, typePerOrdinal, _activeReader.GetSchemaTable());
}

So this basically builds a new empty resultset to cache. This is part of the RetrievalQuery, an object that's executed as the query and shouldn't leave a thread's scope if you don't share an adapter instance among threads. In the case of multithreaded access to this object, then it might fail, but as said, it's not thread safe.

I don't know your code so I can't comment on that. This is the code that's in the runtime, and has been since 5.6.

Frans Bouma | Lead developer LLBLGen Pro
nweinit
User
Posts: 21
Joined: 16-Nov-2016
# Posted on: 02-Mar-2022 21:51:37   

Thanks for the code. I don't know the rest of your codebase, but based on the snippet you've shared, is it possible that the _activeReader could be null somehow? In that scenario (in the InitCachedResultset) it'll return and the _cachedResultset would remain null. Not trying to debug your code, just something i've noticed.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 38963
Joined: 17-Aug-2003
# Posted on: 03-Mar-2022 10:04:12   

After looking into it more deeply, we concluded it can only be null if the query threw an exception when the execute of the actual query happened and the query should be cached as it'll then in theory land in the Finally clause which will call the method which crashes. We have to verify this with a test tho.

See the Materialize method which is part of the call stack of the exception you ran into:

internal bool Materialize(Func<IDataReader, object[], Exception, bool> valueReadErrorHandler, out string failureErrorText)
{
    IDataReader dataSource = null;
    failureErrorText = string.Empty;
    try
    {
        dataSource = _queryToExecute.Execute(CommandBehavior.Default);
        if(dataSource == null)
        {
            failureErrorText = "datareader is null.";
            return false;
        }
        if(dataSource.IsClosed)
        {
            failureErrorText = "datareader is closed.";
            return false;
        }
        var counters = new FetchCounters() { AmountObjectsRead = 0, AmountObjectsSeen = 0, AmountToSkip = _queryToExecute.ManualRowsToSkip };
        object[] valuesOfRow = new object[_numberOfFields];
        while(dataSource.Read())
        {
            try
            {
                dataSource.GetValues(valuesOfRow);
            }
            catch(Exception ex)
            {
                bool handled = false;
                if(valueReadErrorHandler != null)
                {
                    handled = valueReadErrorHandler(dataSource, valuesOfRow, ex);
                }
                if(!handled)
                {
                    throw;
                }
            }
            var breakLoop = HandleRowData(valuesOfRow, counters);
            if(breakLoop)
            {
                break;
            }
        }
        return true;
    }
    finally
    {
        _queryToExecute.ReadComplete();
        PersistenceCore.CleanupDataReader(dataSource, _queryToExecute);
    }
}

When the Execute at the top fails with an exception, the reader inside _queryToExecute is null as it's not set (because there's none), but the _queryToExecute.ReadComplete will run into that when the query is to be cached.

We'll try to verify this with a test using a crashing query and see if it reproduces this stacktrace (instead of the real one).

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 38963
Joined: 17-Aug-2003
# Posted on: 03-Mar-2022 10:16:52   

Reproduced

[Test]
public void CrashingQueryWithCachingShouldNotCauseNRE()
{
    using(var adapter = new DataAccessAdapter())
    {
        var qf = new QueryFactory();
        var q = qf.Customer.Where(OrderFields.OrderId.Source("Foo").Equal("Bar"))
                .CacheResultset(10);
        // will crash but shouldn't crash with NRE
        Assert.Throws(typeof(ORMQueryExecutionException), ()=>adapter.FetchQuery(q));
    }
}
Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 38963
Joined: 17-Aug-2003
# Posted on: 03-Mar-2022 11:51:22   

Fixed in v5.8.5 hotfix and v5.9.1 hotfix, now available. Thanks for continuing the investigation, we otherwise wouldn't have found it simple_smile

If you want to stay on v5.6, which is out of support, it's an easy fix, you have to manually compile the sourcecode of the runtime (available in the sourcecode archive available under My Account -> Downloads -> v5.6 -> Extras section on our website after logging in), and in RetrievalQuery.ReadComplete, replace the first line which is an if statement with:

// Additionally to the checks for skipping the cache logic, if there's no reader, we also can't continue. This is the case when the query threw an
// exception at execution time. We can't produce an empty resultset from that either so it ends here.
if(!this.CacheResultset || _resultsPulledFromCache || _activeReader==null)
Frans Bouma | Lead developer LLBLGen Pro
nweinit
User
Posts: 21
Joined: 16-Nov-2016
# Posted on: 03-Mar-2022 15:55:53   

Great news! This was never really a major issue for us, especially since there was an exception in the fetch anyway so data wouldn't have been returned in the first place, it was more of an info for you and I'm glad to see it was addressed. We will upgrade to a newer version sometimes in the future so it's nice to know it'll be fixed there simple_smile

Thanks for persisting and finding the root cause.