Skip to content

Avoid NRE in cache strategies #2554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 5.3.x
Choose a base branch
from

Conversation

fredericDelaporte
Copy link
Member

@fredericDelaporte fredericDelaporte commented Sep 20, 2020

This fixes a regression from #1480, where destroying the externally supplied and shared cache has been replaced by setting it to null.

Related: #2530.

But this change causes further usages of the cache to cause null reference exceptions, instead of meaningful errors.

This PR is in my opinion the most correct minimal fix.

Another option could be to just remove the line setting the cache to null, effectively causing Destroy to be no-op. But this would mean letting the user use "destroyed" caches, which seems a bit wrong. And in the case of read-write caches, other failures would occur due to some internal object being disposed.


/// <summary>
/// Gets the cache region name.
/// </summary>
public string RegionName
{
get { return Cache.RegionName; }
get { return Cache?.RegionName; }
Copy link
Member Author

@fredericDelaporte fredericDelaporte Sep 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use InternalCache here instead, but this seems to me good enough.

Comment on lines 280 to 281
if (_cache == null || _isDestroyed)
throw new InvalidOperationException(_isDestroyed ? "The cache has already been destroyed" : "The concrete cache is not defined");
Copy link
Member Author

@fredericDelaporte fredericDelaporte Sep 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These strategies classes are instantiated with a null underlying cache, and a later assignment on their Cache property is supplying their underlying cache. Nothing prevents this underlying cache of staying null or being set to null, without the cache strategy having been destroyed.

So we cannot just "guess" that the strategy has been destroyed if its underlying cache is null, it may be wrong.

I do not see any valid reason for the build pattern actually used by theses classes. It should likely be reworked for taking the underlying cache as a constructor parameter, mandatory, and setters should be obsoleted and removed in next major.
But that would be out of the scope of this PR.

@bahusoid
Copy link
Member

TBH I don't like this change. All these checks just to throw not NRE but different kind of exception feels like not useful change that unnecessarily clutters the code.

So I would either go with:

just remove the line setting the cache to null, effectively causing Destroy to be no-op

Or maybe introduce some kind of "Null Object" cache and assign it instead. Which either simply makes no op on calls (it seems we already have one - FakeCache) or throws InvalidOperationException.

@fredericDelaporte
Copy link
Member Author

And what about the read-write case, which will fail on its disposed lock? It is no better than a NRE and we should also handle this case more gracefully, don't you think?

@bahusoid
Copy link
Member

bahusoid commented Sep 20, 2020

And what about the read-write case

Sorry I don't know this part of code - so have no idea what would fail on its disposed lock and in which case (Empty Destroy method or with "Null Value Object" cache or in both cases).
So implementing FakeCache like class that throws user friendly exception and use it instead of null won't work? Ok.. Then I have no better suggestions..

It is no better than a NRE and we should also handle this case more gracefully, don't you think?

I am for graceful behavior if it's possible to be implemented in one place. I don't like scattering such checks across all implementations. If it's the only choice - I think it's not worth it for the rare cases when object is used for disposed session factory.

So that's just my thoughts I'm not requesting you to make any changes (sorry forgot to nit :) ). Let it be reviewed by someone else.

@@ -96,6 +98,7 @@ private int NextLockId()
/// </remarks>
public object Get(CacheKey key, long txTimestamp)
{
CheckCache();
using (_asyncReaderWriterLock.ReadLock())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the (strict) read-write strategy case, most cache operations acquire this lock. This class Destroy does dispose it, so most operations, in case of continued usage, would likely throw a disposed object exception from within the lock.

Somehow it is actually better than a NRE, but still not good: the strategies classes are "destroyed", so throwing a disposed object exception can be considered suitable. Excepted that it should be the strategy class throwing it directly, instead of some internal object it holds. Letting this been thrown by some internal object can puzzle end users as whether it is a bug inside NHibernate or not, like that strategy class having a bug causing usage of a disposed object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it is protected by acquiring the cache prior to accessing the lock, with the cache acquisition throwing if the strategy is already destroyed.
This is a bit brittle pattern since we could acquire the cache later...

@hazzik
Copy link
Member

hazzik commented Sep 22, 2020

Would it be better if CheckCache is encapsulated into Cache property and the usage of _cache field is updated with the usage of property?

@hazzik
Copy link
Member

hazzik commented Sep 22, 2020

Another thought -- can CheckCache be incorporated into CacheBase?

@fredericDelaporte
Copy link
Member Author

Yes I dislike too this bloat code for checking the object state everywhere.

But there is also the _asyncReaderWriterLock which can be disposed and so needs to be protected by those checks. Maybe we can use your first suggestion and dodge the lock trouble by nullifying it and using ?. every where on it to dodge the issue and let the later Cache access raise the exception, but that would be a code smell I think, those ?. on the lock.

I will use your suggestion for the two other caches at least.

About incorporating CheckCache in CacheBase, I do not get it right now, I will have another look.

@hazzik
Copy link
Member

hazzik commented Sep 23, 2020

incorporating CheckCache in CacheBase, I do not get it right now, I will have another look.

Now most of our calls look like this:

// prop definition simplified
CacheBase Cache => _cache;

void SomeMethod() {
    CheckCache();
    _cache.DoSomething();
...
}

What I'm proposing is doing this:

// prop definition simplified
CacheBase Cache { get { CheckCache(); return _cache; } }

void SomeMethod() {
    Cache.DoSomething();
...
}

@fredericDelaporte
Copy link
Member Author

I thought you wanted me to put something in the CacheBase class, but instead you refer to the CacheBase typed property versus the ICache one.
One trouble is, currently, the CacheBase one is an explicit interface implementation and so is unpractical to directly call.
The ICache one is on the path to removal.

Maybe we should dodge that by adding an InternalCache property, with a todo 6.0 move code into CacheBase Cache and remove property.

It would also avoid adding a possible breaking change in a minor by starting to throw on read of those interface properties for null caches.

@hazzik
Copy link
Member

hazzik commented Sep 24, 2020

I thought you wanted me to put something in the CacheBase class, but instead you refer to the CacheBase typed property versus the ICache one.

There were 2 suggestions actually. One is about CacheBase class and another one about properties (both properties in fact).

@hazzik
Copy link
Member

hazzik commented Sep 24, 2020

Can you explain why you need additional property InternalCache?

@fredericDelaporte
Copy link
Member Author

As commented before doing it:

  • To avoid a possible breaking change in a SemVer patch release. Changing one of the existing property for throwing when the cache is null, instead of just yielding it, is a possible breaking change since theses properties are public.
  • Doing it in the CacheBase typed property would require some cumbersome, bloat code everywhere to ensure we call it, since it is an explicit implementation. Something like IBatchableCacheConcurrencyStrategy strategy = this; var cache = strategy.Cache;.

@fredericDelaporte
Copy link
Member Author

I have added a commit to remove InternalCache property. But as explained, it adds some other kind of bloat code, in order to be able to call the explicitly implemented CacheBase property. The ICache property is not suitable because ICache does not have all the required methods. By example we cannot call GetMany on an ICache typed property.

@hazzik
Copy link
Member

hazzik commented Oct 5, 2020

I have added a commit to remove InternalCache property.

I have not had anything against this property. I was just wondering why it is needed. The explanation and reasoning was fine.

@fredericDelaporte
Copy link
Member Author

Reverted.

@fredericDelaporte
Copy link
Member Author

This PR is holding the release of 5.3.4 since ten days (latest merge done on the 5.3.x branch). It should be validated or rejected, and then I would prepare the release of the 5.3.4 fixes.

@bahusoid
Copy link
Member

It should be validated or rejected

I still think the issue is not worth changes made in this PR. And it shouldn't hold the release - this PR can wait till next round as NRE or other exception it's about user invalid usage case scenario.

@hazzik
Copy link
Member

hazzik commented Oct 26, 2020

I lean on agreeing with @bahusoid on the issue. We probably should release the 5.3.4 without this change.

@fredericDelaporte fredericDelaporte removed this from the 5.3.4 milestone Oct 28, 2020
@mazharqayyum
Copy link
Contributor

It should be validated or rejected

I still think the issue is not worth changes made in this PR. And it shouldn't hold the release - this PR can wait till next round as NRE or other exception it's about user invalid usage case scenario.

Can you please expand on invalid usage scenario? ISQL DDL query causes this exception, I have the L2 SysCache configured. Its a known behavior of Nhibernate that any direct update to database will wipe the L2 cache. It seems like a good deal for really rare scenario which in my case is. So what is the recommended way in 5.3.x?

@bahusoid
Copy link
Member

This PR just changes one exception with another. It doesn't fix anything. So it won't help you

Can you please expand on invalid usage scenario

#2530 (comment)

So if you can prove that it's reproducible with still open session factory (check ISessionFactory.IsClosed) we should investigate it more closely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants