Skip to content
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

Implement multiple get and put for query cache and query batch #1788

Merged

Conversation

fredericDelaporte
Copy link
Member

This is a follow up to @maca88 #1633 (batching 2nd level cache operations) and @bahusoid #1718 (query batch). It aims at combining both: enabling batching of 2nd level cache operations for the query batch.

/// from an <see cref="IEntityPersister"/> or <see cref="ICollectionPersister"/>.
/// When a different persister or a different operation is added to the batch, the current batch will be executed.
/// </summary>
internal partial class CacheBatcher
public sealed partial class CacheBatcher
Copy link
Member Author

Choose a reason for hiding this comment

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

For enabling batching of puts across batched queries, I had to transmit a CacheBatcher with a public interface method, so I had to switch this type to public.

@@ -34,7 +30,7 @@ public CacheBatcher(ISessionImplementor session)
/// </summary>
/// <param name="persister">The entity persister.</param>
/// <param name="data">The data to put in the cache.</param>
public void AddToBatch(IEntityPersister persister, CachePutData data)
internal void AddToBatch(IEntityPersister persister, CachePutData data)
Copy link
Member Author

Choose a reason for hiding this comment

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

CachePutDatabeing internal, the method has to be internal too since the declaring class is no more internal but public. For consistency and has users should not need to use directly the class, but just have to pass it around, I have switched all the methods to internal.

{
private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof (StandardQueryCache));
private readonly ICache _queryCache;
private readonly IBatchableReadOnlyCache _batchableReadOnlyCache;
private readonly IBatchableCache _batchableCache;
Copy link
Member Author

Choose a reason for hiding this comment

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

#1777 (CacheBase) would allow a simpler implementation here.

if (queryParameters.IsReadOnlyInitialized)
persistenceContext.DefaultReadOnly = queryParameters.ReadOnly;
else
queryParameters.ReadOnly = persistenceContext.DefaultReadOnly;
Copy link
Member Author

Choose a reason for hiding this comment

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

Here is the reason for the need of QueryParameters: handling the readonly flag for loading entities. With single gets, it can be done on the caller side.
But with multiple gets, it has to be done on the callee side. Unless gets are sorted out according to their readonly status matching or not the one of the persistence context (which is not trivial to handle) and then called as separated multiple gets. I considered it would complicate the code a bit to much and so choose to move this handling in the query cache.
(For the record, multi-query/criteria were not handling the readonly flag of each query they were containing.)

bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session);
// Since 5.2
[Obsolete("Have the query cache implement IBatchableQueryCache, and use IBatchableQueryCache.Get")]
IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet<string> spaces, ISessionImplementor session);
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 order to properly handle the multi-get cases, I had to transmit the QueryParameters. So for consistency, I have made the same change to the single Get and to Put methods.

@@ -44,18 +47,17 @@ public void Execute()
{
Init();

if (!Session.Factory.ConnectionProvider.Driver.SupportsMultipleQueries)
using (Session.BeginProcess())

This comment was marked as off-topic.

using (Session.BeginProcess())
{
DoExecute();
ExecuteBatched();

This comment was marked as off-topic.

{
// The auto-flush must be handled before obtaining the commands, because an auto-flush
// may have to invalidate cached data which otherwise would cause a command to be skipped.
Session.AutoFlushIfRequired(querySpaces);

This comment was marked as off-topic.

stopWatch.Start();
}
var resultSetsCommand = Session.Factory.ConnectionProvider.Driver.GetResultSetsCommand(Session);
CombineQueries(resultSetsCommand, GetCachedResults());

This comment was marked as outdated.

if (index == _queryInfos.Count - 1)
{
InitializeEntitiesAndCollections(reader, hydratedObjects);
}

This comment was marked as off-topic.

@bahusoid

This comment has been minimized.

@fredericDelaporte

This comment has been minimized.

@bahusoid

This comment has been minimized.

@fredericDelaporte

This comment has been minimized.

@bahusoid

This comment has been minimized.

// the UnresolvableObjectException could occur while resolving
// associations, leaving the PC in an inconsistent state
Log.Debug(ex, "could not reassemble cached result set");
// TODO: add and handle a RemoveMany?
Copy link
Contributor

Choose a reason for hiding this comment

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

I forgot about RemoveMany when doing #1633 as it was not needed there, but it is already implemented in nhibernate/NHibernate-Caches#45, so why not add it if it can be used here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have put a question mark because this case is almost an error case, and it does not look likely to me it would benefit from batching. It should not happen frequently, and even less frequently would it benefit from a Many handling, since the first encountered one short-circuit the result-set processing: it could be batched only in multi-queries case, if many queries in it encounter a "no more resolvable entity reference" case.

So in fact I should change that comment, I do not think it would be worth it to handle a many case here.

IEnumerable<ISqlCommand> GetCommands();
/// <param name="cachedResults">The cached results.</param>
/// <returns>The commands for obtaining the results not already cached.</returns>
IEnumerable<ISqlCommand> GetCommands(IDictionary<ICachingInformation, IList> cachedResults);

This comment was marked as resolved.

This comment was marked as resolved.

fredericDelaporte added a commit to fredericDelaporte/nhibernate-core that referenced this pull request Jul 10, 2018
Additional cleanup/doc originating from nhibernate#1788 but which should be here
@fredericDelaporte
Copy link
Member Author

Squashed and rebased for resolving conflicts with #1789.

Copy link
Member

@hazzik hazzik left a comment

Choose a reason for hiding this comment

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

Some code stlyles

/// </summary>
public partial interface IQueryCache
{
/// <summary>
/// The underlying <see cref="ICache"/>.
/// </summary>
ICache Cache { get; }

This comment was marked as resolved.

void Clear();
// Since 5.2
Copy link
Member

Choose a reason for hiding this comment

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

Need a new line before

bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session);
// Since 5.2
Copy link
Member

Choose a reason for hiding this comment

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

Need a new line before

IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet<string> spaces, ISessionImplementor session);
/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

A new line before please

{
get
{
ThrowIfNotInitialized();
Copy link
Member Author

Choose a reason for hiding this comment

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

This one would have been better moved in #1789, but I forgot it.

@@ -243,8 +294,15 @@ private void InitializeEntitiesAndCollections(DbDataReader reader, List<object>[
if (queryInfo.IsResultFromCache)
continue;
queryInfo.Loader.InitializeEntitiesAndCollections(
hydratedObjects[i], reader, Session, Session.PersistenceContext.DefaultReadOnly);
hydratedObjects[i], reader, Session, queryInfo.Parameters.IsReadOnly(Session),
Copy link
Member Author

Choose a reason for hiding this comment

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

This change would also have been better moved in #1789. The old code was from multi-query/criteria, which was not knowing for which queries they were initializing and so could not handle their readonly flag.


private void CombineQueries(IResultSetsCommand resultSetsCommand)
{
foreach (var multiSource in _queries)
Copy link
Member

Choose a reason for hiding this comment

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

This multiSource name is leftover of initial interface namings. Maybe it's better to rename it simply to query (if you agree - please note that this name is used mulitple times in this file)

@@ -208,16 +258,17 @@ public void ExecuteNonBatched()

protected List<T> GetTypedResults<T>()
{
if (_loaderResults == null)
ThrowIfNotInitialized();
if (_queryInfos.Any(qi => qi.Result == null))

This comment was marked as resolved.

internal IList GetResultFromQueryCache(ISessionImplementor session, QueryParameters queryParameters,
ISet<string> querySpaces, IQueryCache queryCache,
QueryKey key)
internal bool CanGetFromCache(ISessionImplementor session, QueryParameters queryParameters)
Copy link
Member

@bahusoid bahusoid Jul 12, 2018

Choose a reason for hiding this comment

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

This method doesn't handle per query SetCacheMode calls. Which actually now is used in tests (as no op but might lead to confusion that it actually can be used in Future):

.SetCacheMode(CacheMode.Normal)
.Future<EntitySimpleChild>()

Maybe this query cache mode needs to be moved to QueryParameters and handled similarly as QueryParameters.ReadOnly flag?

So we should move this code to QueryParameters.CanGetFromCache(session) method. And for parity introduce QueryParameters.CanPutToCache(session)

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, the queries individual cache mode has never been handled for future and multi-query/criteria, but we could. I do not think it should be done here, but better as another PR.

Copy link
Member

Choose a reason for hiding this comment

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

Yes I agree - per query cacheMode support should be done as another PR.

But to minimize "change noise" I think it's better to introduce here QueryParameters.CanGetFromCache(session) and use it instead of this method (and also introduce QueryParameters.CanPutToCache(session) with simple session check). Again I see no difference why this usage should be different from QueryParameters.IsReadOnly(session) - so those checks should be implemented and used the same way.

And dedicated "per query cacheMode" PR will simiply fill those 2 methods with proper implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it will not work, so better stick to YAGNI, not doing this in advance.

CacheMode needs to be temporarily switched on the session instead, because many cache interactions triggered deeply in the loading implementation have no access to query parameters.

Copy link
Member

@bahusoid bahusoid Jul 12, 2018

Choose a reason for hiding this comment

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

For this PR I'm just saying that it's better to have consistent way for readonly check for query and cache mode check for query. So from this point - what will not work?
We use queryParameters.IsReadOnly(session) for readonly check. So it looks logical to me to use queryParameters.CanGetFromCache(session) for cache mode check. So why introduce new logically similair method in Loader instead? It can be still marked internal here if you think this "cache mode per query" won't work.

Copy link
Member Author

Choose a reason for hiding this comment

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

These cases are not comparable in my opinion:

  • IsReadonly is a bit messy, originating from times where nullable were not avaliable. It is also there to hide the complexity of replacing nullability by an additional boolean.
  • CacheMode is not a boolean but a flag enumeration value. It conveys more state than a boolean. CanGetFromCache is just a small part of it, because by example it tells nothing about what should be done with entities assembled from their id cached in query result, whether they should themselves be put into the cache or not if they were missing in it when re-assembled.
    So it would be just a partial part of the information we actually need to handle properly cache mode, and we will have to put more somehow somewhere. It then looks likely to me that CanGetFromCache will just be redundant and not needed.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not talking about how data needs to be stored. But rather how it should be used in code.
Calling queryParameters.IsReadOnly(session) for checking query read-only flag looks pretty clean to me. How messy its implementation is irrelevant.
And as we need now method CanGetFromCache I think place and synatx should be the same. But let's not spend more time on it. If you are not convinced - let's keep it as is for time being.

Copy link
Member Author

Choose a reason for hiding this comment

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

You were right with the need for CanGetFromCache and CanPutToCache, but I still think it was better to do it in a dedicated PR.

src/NHibernate/Multi/QueryBatch.cs Show resolved Hide resolved
/// <summary>
/// The query result.
/// </summary>
IList Result { get; }
Copy link
Member

Choose a reason for hiding this comment

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

May be it should be exposed as GetResultsToCache or some other name that suggests that implementor should return values that's going to be put to cache.

Copy link
Member Author

Choose a reason for hiding this comment

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

If doing so, it should then hold neither results retrieved from cache nor results after cacheable transformation which should not be put (the put must be done on the untransformed result).

Copy link
Member

@bahusoid bahusoid left a comment

Choose a reason for hiding this comment

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

No more complaints from my part. And thanks for fixing the mess I made :)

@fredericDelaporte
Copy link
Member Author

I validated it, so it was also my responsibility.

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