-
Notifications
You must be signed in to change notification settings - Fork 934
Add support for query space synchronization #1392
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
Conversation
It may be 5.1 if it has no breaking changes, otherwise 6.0. Currently I am focusing on 5.0.1 and also 5.0 releases of some dependent projects like caches. |
5.1 would be great, since it's a performance drag. As far as breaking changes go, it adds new methods to public interfaces (ISQLQuery, ISessionFactory), but some of these could be hidden/internalized in a first release. |
src/NHibernate/Impl/SqlQueryImpl.cs
Outdated
@@ -170,10 +172,12 @@ public override IList<T> List<T>() | |||
|
|||
public NativeSQLQuerySpecification GenerateQuerySpecification(IDictionary<string, TypedValue> parameters) | |||
{ | |||
var allQuerySpaces=new HashSet<string>(querySpaces ?? new List<string>()); | |||
allQuerySpaces.UnionWith(GetSynchronizedQuerySpaces()); |
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.
NativeSQLQuerySpecification
already does deduplication, so we dont' need to do it here.
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.
it seems like the Init call should be removed.
Agreed. Init
does only do the cache clearing, which should be done after query execution.
I think CoordinateSharedCacheCleanup
calls it because it is a duplicate of the one in AbstractStatementExecutor
, for which it does maybe make sens to clear the cache before hand.
Or doing it before was maybe a crutch for avoiding returning stale data for concurrent sessions right after the SQL update before the end of ExecuteUpdate
had cleared the cache. But such a logic does not prevent a concurrent session to put again things in the cache before the sql update have done its job, so if this the reason, it was a bit moot. So anyway I think this Init
call should be removed in the case of the CoordinateSharedCacheCleanup
method of NativeSQLQueryPlan
.
As far as breaking changes go, it adds new methods to public interfaces (ISQLQuery, ISessionFactory), but some of these could be hidden/internalized in a first release.
It also add a new ancestor to an interface, which is also a breaking change, strictly speaking.
So either it is possible to change this PR in a way where nothing changes on existing interfaces (extension methods with likely hard cast to implementation in them, do nothing if not the expected implementation...), or we have to bump next version to 6.0 instead of 5.1 if merging this.
Java does things with its generics which I don't think is possible in C#
It looks like the return type covariance I had to deal with for session builders, see this class and interface. (In this case I could additionally "hide" the generic type (class/interface eight line above) but I do not think this can apply to query space case.)
@@ -42,7 +42,7 @@ public abstract partial class AbstractQueryImpl : IQuery | |||
private CacheMode? cacheMode; | |||
private CacheMode? sessionCacheMode; | |||
private string comment; | |||
|
|||
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.
Accidental white-space addition.
The The |
@fredericDelaporte Any suggestion on how to make this suitable for inclusion? |
@gliljas at least resolve the conflicts and made the build green. |
Fixed redundant cache clearing
Return task as cancelled
|
||
if (log.IsDebugEnabled()) | ||
{ | ||
foreach (var p in cacheGroup) |
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.
I would love to see only one line in logs in this case.
{ | ||
foreach (var p in cacheGroup) | ||
{ | ||
log.Debug("evicting second-level cache: " + p.EntityName); |
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.
I would love to see only one line in logs in this case.
src/NHibernate/Impl/SqlQueryImpl.cs
Outdated
|
||
public IReadOnlyCollection<string> GetSynchronizedQuerySpaces() | ||
{ | ||
return addedQuerySpaces.IsValueCreated ? addedQuerySpaces.Value : new HashSet<string>(); |
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.
The new HashSet<string>()
kills the purpose of the Lazy<>
here.
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.
I think the Lazy<>
is not worth it here. For having it useful, GetSynchronizedQuerySpaces()
would have to yield null
instead when it is not initialized. Would it be a private method, why not, but as an interface method, better yield an empty collection.
To be squashed.
To be squashed.
I ran the build locally on WSL, and I think I managed to fix it by updating NSubstitute to 3.1.0. |
The Travis build is fixed. I had thought about upgrading NSubstitue, but looking at the references of both versions, I was having the impression it will not change a thing. |
I have run an async regen just to verify the tests due to the async upgrade and configuration change on master, which may impact them. |
} | ||
} | ||
|
||
#endregion | ||
|
||
public virtual async Task InitAsync(CancellationToken cancellationToken) |
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.
"Since the class is not internal, this is a breaking change. It needs to be added back, obsoleted, in the non-generated partial."
@@ -34,19 +34,6 @@ public abstract partial class AbstractStatementExecutor : IStatementExecutor | |||
|
|||
public abstract Task<int> ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken); | |||
|
|||
protected virtual async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) |
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.
Yes, and same for this one.
src/NHibernate/ISQLQuery.cs
Outdated
/// <returns>The query.</returns> | ||
public static ISQLQuery AddSynchronizedQuerySpace(this ISQLQuery sqlQuery, string querySpace) | ||
{ | ||
if (!(sqlQuery is ISynchronizableSQLQuery ssq)) |
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.
@fredericDelaporte I think then it shall be CastOrThrow
|
||
// Since v5.2 | ||
[Obsolete("This method has no more actual async calls to do, use its sync version instead.")] | ||
protected virtual Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) |
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.
Need either async
keyword, or wrapping into try-catch.
|
||
namespace NHibernate | ||
{ | ||
public interface ISynchronizableQuery<out T> where T : ISynchronizableQuery<T> |
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.
Do we plan to have other types of ISynchronizableQuery<>
other than ISQLQuery
?
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.
Looking at Hibernate code, it seems not. They use it for SQLQuery
(obsoleted on their side) and NativeQuery
(replacement for SQLQuery
). So why not merging that in ISynchronizableSQLQuery
without the generic param trick.
To be squashed.
Query space invalidation is not available in 5.1.x, see nhibernate#1392. To be squashed
Ok, this was a bit of a Pandora's box...
Fixes #1381
The problem was that using an
ISQLQuery
andExecuteUpdate
causes the entire (sort of) 2nd level cache to be cleared. Not a problem with an in-memory cache provider, but with a distributed cache provider (Redis in our case), it's a massive problem. We ended up having to create ugly wrappers, with nativeSqlCommand
etc.I found this...
https://stackoverflow.com/questions/19054673/2nd-level-cache-invalidation-with-native-queries
...which indicated that Hibernate had a solution for this, but these methods were missing in NHibernate.
The first thing to do was a unit test, to verify the behavior. This is the first assert in
NativeSQLBulkOperationsWithCache.cs .
My expectation was that it would call the
ICache.Clear
method once (only one queryspace/table in the mapping), but found that it called the method 10 times (!). That's quite costly in a distributed scenario, especially if you have more than one mapped class. We do. :)The reasons for this are two.
CoordinateSharedCacheCleanup
, called fromPerformExecuteUpdate
, which creates aBulkOperationCleanupAction
and callsInit()
on it. This call causes a first wave ofICache.Clear
:s. I don't understand why a nativeExecuteUpdate
would need to clear the cache before it's run, but it does. The Hibernate version of this looks about the same, but never calls Init(), so the cache is never touched. I haven't touched this yet, since I wanted your input, but it seems like the Init call should be removed.BulkOperationCleanupAction
callsEvictEntity
for every queryspace it finds, but this may very well end up with the sameIPersister
many times, and the sameICache
just as many times.Since the unit test has a mapped base class, subclassed into 4, we ended up with 2*5 calls to the
Vehicle
cache.This was fixed by adding an overload of
EvictEntity
(and its siblings) which takes anIEnumerable<string>
(entity names), which can be handled as a batch, calling eachICache
only once. Async versions were also added. Please take a look at theTask.WhenAny(Task.WhenAll....)
construct, which is used to exit via cancellation. It may be worth discussing.The
AddSynchronizedQuerySpace
method was added via anISynchronizableQuery
interface, just as Hibernate uses SynchronizeableQuery (notice the spelling, which I think is wrong). Java does things with its generics which I don't think is possible in C#, but I replicated the behavior using a self referencing generic interface. It's somewhat elegant, but also a bit weird. Just using a plainISynchronizableQuery
interface would prevent method chaining, but an option could be to simply add the methods toISQLQuery
.I replicated the Hibernate unit test I found for this, which checks whether the session is flushed unnecessarily.
As I mentioned on the developer list, I've added NSubstitute to the project.
ICacheProvider
test it wasn't super elegant, but that's also an indication that things in the core code is a bit coupled.BulkOperationCleanupAction
tests were a much better fit, and such tests could be used a lot more.