I've started using the Query Cache more in some apps and I have noticed a
problem with the way it uses memory.


Cayenne has two versions of the Query Cache - Local and Shared. The shared
cache data is available to all ObjectContexts. The Local cache data is
available only to the same ObjectContext that loaded it.


The Shared and Local query caches actually use the same global cache
instance, BUT they are kept separate by name spacing the keys. This has a
few surprising consequences:

1) Data written to the Local query cache will survive the lifetime of the
ObjectContext. If the cache group it is written to does not have an
explicit expiration time, then it will stay in the cache forever until you
run out of memory.

2) Removing a Local query cache group explicitly to invalidate the cache
(and reclaim that memory) is not possible because NestedQueryCache (which
wraps the actual cache in order to provide the name spacing feature) is
only able to remove entries from the Shared query cache.

3) Entries written to the Local query cache will keep strong references the
entire state of the ObjectContext in memory since the whole DataObjects are
stored along with the reference to their current ObjectContext. This is in
contrast to the Shared query cache which only stores DataRows. This means
that if you load a lot of data into a context that you don't want to cache
along with a tiny amount of Local query cache data, all of it will be
resident in memory well past the lifetime of the ObjectContext, possibly
forever unless you have a specific expiration time set on that cache group.


The best short term workaround is to define short expiration times for all
of your local cache groups, especially the default group named
"cayenne.default.cachegroup". Reads from the local cache are already
specific to each ObjectContext, so the expiration really just is there to
protect you from having every locally cached query leak memory.


#3 would be really nice to change so that only DataRows are stored in the
Local cache just like with the Shared cache. Since the Shared cache already
works this way I would expect this to be fairly simple.

For #2 I would recommend an API change to Query Cache that allow specifying
the QueryCacheStrategy along with the cache group name when removing a
cache group, i.e. :

    public void removeGroup(QueryCacheStrategy strategy, String groupKey);

Then the existing one-arg removeGroup method should be deprecated.

    public void removeGroup(String groupKey);

For #1 I would recommend adding a finalizer to BaseContext that will purge
all the Local query cache entries from the global cache instance when the
ObjectContext is garbage collected. This is perhaps a little trickier since
the name spacing that defines the Local query cache is only available from
NestedQueryCache, which is supposed to be mostly invisible and accessed
only through the QueryCache API which doesn't know about name spacing. An
alternative to name spacing would be to actually create separate caches for
each ObjectContext, but this might be expensive from a performance

Let me know your thoughts.


Reply via email to