-
Notifications
You must be signed in to change notification settings - Fork 934
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
base: 5.3.x
Are you sure you want to change the base?
Conversation
|
||
/// <summary> | ||
/// Gets the cache region name. | ||
/// </summary> | ||
public string RegionName | ||
{ | ||
get { return Cache.RegionName; } | ||
get { return Cache?.RegionName; } |
There was a problem hiding this comment.
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.
if (_cache == null || _isDestroyed) | ||
throw new InvalidOperationException(_isDestroyed ? "The cache has already been destroyed" : "The concrete cache is not defined"); |
There was a problem hiding this comment.
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.
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:
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 - |
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? |
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).
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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
Would it be better if |
Another thought -- can |
Yes I dislike too this bloat code for checking the object state everywhere. But there is also the I will use your suggestion for the two other caches at least. About incorporating |
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();
...
} |
I thought you wanted me to put something in the Maybe we should dodge that by adding an It would also avoid adding a possible breaking change in a minor by starting to throw on read of those interface properties for |
There were 2 suggestions actually. One is about CacheBase class and another one about properties (both properties in fact). |
Can you explain why you need additional property |
As commented before doing it:
|
I have added a commit to remove |
I have not had anything against this property. I was just wondering why it is needed. The explanation and reasoning was fine. |
This reverts commit e2889f7.
Reverted. |
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. |
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. |
I lean on agreeing with @bahusoid on the issue. We probably should release the 5.3.4 without this change. |
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? |
This PR just changes one exception with another. It doesn't fix anything. So it won't help you
So if you can prove that it's reproducible with still open session factory (check ISessionFactory.IsClosed) we should investigate it more closely. |
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.