From 5ca11e7b44ba18bea4794645a6e9b8042f73fa03 Mon Sep 17 00:00:00 2001 From: "Keith R. Gustafson" Date: Tue, 29 Sep 2020 12:15:30 -0700 Subject: [PATCH] Make CacheMessageManager support the EntityStore method value cache Previously, using the @Indexed annotation on a data entity method would only work within a single instance. With this change, CacheMessageManager will update the method value cache as needed. Note: As commented in several places in this commit, every entity that uses @Indexed must have its EntityGroup "distribute" attribute set to true for this to work. --- .../com/techempower/cache/EntityStore.java | 64 +++++++++++++------ .../techempower/cache/annotation/Indexed.java | 16 +++-- .../com/techempower/data/EntityGroup.java | 14 ++++ .../cluster/jms/CacheMessageManager.java | 6 ++ 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/gemini/src/main/java/com/techempower/cache/EntityStore.java b/gemini/src/main/java/com/techempower/cache/EntityStore.java index 8230a51f..e30b2089 100755 --- a/gemini/src/main/java/com/techempower/cache/EntityStore.java +++ b/gemini/src/main/java/com/techempower/cache/EntityStore.java @@ -280,6 +280,45 @@ protected boolean isIndexed(Class type, String metho } } + /** + * Whether the provided class uses the @Indexed annotation and therefore uses + * the method value cache. + */ + public boolean usesMethodValueCache(Class type) + { + return methodValueCaches.get(type) != null; + } + + /** + * Update the specified method value cache. + */ + public void methodValueCacheUpdate(Class type, long... ids) + { + final MethodValueCache methodValueCache = methodValueCaches.get(type); + if (methodValueCache != null) + { + for (long id : ids) + { + methodValueCache.update(id); + } + } + } + + /** + * Delete from the specified method value cache. + */ + public void methodValueCacheDelete(Class type, long... ids) + { + final MethodValueCache methodValueCache = methodValueCaches.get(type); + if (methodValueCache != null) + { + for (long id : ids) + { + methodValueCache.delete(id); + } + } + } + /** * Reset all entity groups controlled by this controller. */ @@ -1028,14 +1067,9 @@ public T get(Class type, String methodName, Object v public void refresh(Class type, long... ids) { getGroupSafe(type).refresh(ids); - - final MethodValueCache methodValueCache = methodValueCaches.get(type); - if (methodValueCache != null) - { - for (long id : ids) { - methodValueCache.update(id); - } - } + + // Update index/methodValueCache. + methodValueCacheUpdate(type, ids); // Notify the listeners. notifyListenersCacheObjectExpired(true, type, ids); @@ -1085,11 +1119,7 @@ public void put(T entity) if (!useAffectedRows || rowsUpdated > 0) { // Update method value caches. - final MethodValueCache methodValueCache = methodValueCaches.get(entity.getClass()); - if (methodValueCache != null) - { - methodValueCache.update(entity.getId()); - } + methodValueCacheUpdate(entity.getClass(), entity.getId()); // Notify the listeners. final CacheListener[] toNotify = listeners; @@ -1133,12 +1163,8 @@ public void remove(T entity) } // Update method value cache. - final MethodValueCache methodValueCache = methodValueCaches.get(entity.getClass()); - if (methodValueCache != null) - { - methodValueCache.delete(entity.getId()); - } - + methodValueCacheDelete(entity.getClass(), entity.getId()); + // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) diff --git a/gemini/src/main/java/com/techempower/cache/annotation/Indexed.java b/gemini/src/main/java/com/techempower/cache/annotation/Indexed.java index 3ad81c83..f36161d3 100755 --- a/gemini/src/main/java/com/techempower/cache/annotation/Indexed.java +++ b/gemini/src/main/java/com/techempower/cache/annotation/Indexed.java @@ -29,10 +29,18 @@ import java.lang.annotation.*; /** - * This annotation is used to mark entity classes or methods as indexed. - * Classes or methods marked as @Indexed should always cache method values, - * even if method value caching is turned off. + * This annotation is used to mark entity classes or methods as indexed. Classes + * or methods marked as @Indexed should always cache method values, even if + * method value caching is turned off. + *

+ * IMPORTANT: If you use this annotation in an application that uses the + * CacheMessageManager to distribute cache updates to other instances, it is + * your responsibility to ensure that your corresponding EntityGroup + * ("distribute" defaulting to false) or CacheGroup ("distribute" defaulting to + * true) has the "distribute" flag set to true. Otherwise each instance will + * risk having a stale method value cache and you'll get wrong answers from + * EntityStore.get() and list() and honestly it won't be very fun. */ -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Indexed { } diff --git a/gemini/src/main/java/com/techempower/data/EntityGroup.java b/gemini/src/main/java/com/techempower/data/EntityGroup.java index 4d7628ef..2914d419 100755 --- a/gemini/src/main/java/com/techempower/data/EntityGroup.java +++ b/gemini/src/main/java/com/techempower/data/EntityGroup.java @@ -2269,6 +2269,13 @@ public static class Builder * need to notify DistributionListeners. However, if some instances use a * CacheGroup for this entity, then it may be useful to set this to true so * those instances can update their cache. + *

+ * IMPORTANT: If you use the @Indexed annotation on this entity in an + * application that uses the CacheMessageManager to distribute cache updates to + * other instances, it is your responsibility to ensure that "distribute" is set + * to true. Otherwise each instance will risk having a stale method value cache + * and you'll get wrong answers from EntityStore.get() and list() and it + * honestly won't be very fun. */ protected boolean distribute = false; @@ -2364,6 +2371,13 @@ public Builder readOnly() /** * Specifies updates to the resulting EntityGroup should be passed to * DistributionListeners. + *

+ * IMPORTANT: If you use the @Indexed annotation on this entity in an + * application that uses the CacheMessageManager to distribute cache updates to + * other instances, it is your responsibility to ensure that "distribute" is set + * to true. Otherwise each instance will risk having a stale method value cache + * and you'll get wrong answers from EntityStore.get() and list() and it + * honestly won't be very fun. */ public Builder distribute(boolean distribute) { diff --git a/gemini/src/main/java/com/techempower/gemini/cluster/jms/CacheMessageManager.java b/gemini/src/main/java/com/techempower/gemini/cluster/jms/CacheMessageManager.java index 4cc70921..fa0ec719 100755 --- a/gemini/src/main/java/com/techempower/gemini/cluster/jms/CacheMessageManager.java +++ b/gemini/src/main/java/com/techempower/gemini/cluster/jms/CacheMessageManager.java @@ -489,6 +489,9 @@ else if (group instanceof EntityGroup) log.info("Receiving 'cache object expired' but group id is invalid:{}, group: {}", cacheMessage, group); } + // Now that the object is updated in the cache, update the method value cache if + // needed. + store.methodValueCacheUpdate(group.getType(), cacheMessage.getObjectId()); break; } @@ -514,6 +517,9 @@ else if (group instanceof EntityGroup) "invalid: {}, group: {}, cacheMessage: {}", cacheMessage.getGroupId(), group, cacheMessage); } + // Now that the object is deleted from the cache, also delete from the method + // value cache if needed. + store.methodValueCacheDelete(group.getType(), cacheMessage.getObjectId()); break; } case (CacheMessage.ACTION_GROUP_RESET):