Skip to content

Commit

Permalink
Issue #2198 - tenant-aware caching
Browse files Browse the repository at this point in the history
Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>
  • Loading branch information
JohnTimm committed Apr 15, 2021
1 parent 0171bf0 commit e635bdc
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

import com.ibm.fhir.core.util.CacheKey;
import com.ibm.fhir.core.util.CachingProxy.KeyGenerator;

@Retention(RUNTIME)
@Target(METHOD)
public @interface Cacheable {
int maximumSize() default 128;
int duration() default 1;
TimeUnit unit() default TimeUnit.HOURS;
Class<? extends CacheKey.Generator> keyGeneratorClass() default CacheKey.Generator.class;
Class<? extends KeyGenerator> keyGeneratorClass() default KeyGenerator.class;
}
11 changes: 0 additions & 11 deletions fhir-core/src/main/java/com/ibm/fhir/core/util/CacheKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

package com.ibm.fhir.core.util;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;

Expand Down Expand Up @@ -42,14 +41,4 @@ public boolean equals(Object obj) {
public static CacheKey key(Object... values) {
return new CacheKey(values);
}

public interface Generator {
public static final Generator DEFAULT = new Generator() {
@Override
public CacheKey generate(Object target, Method method, Object[] args) {
return key(method, args);
}
};
CacheKey generate(Object target, Method method, Object[] args);
}
}
95 changes: 72 additions & 23 deletions fhir-core/src/main/java/com/ibm/fhir/core/util/CacheManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,97 @@

package com.ibm.fhir.core.util;

import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.Caffeine;

public final class CacheManager {
private static final CacheManager INSTANCE = new CacheManager();
private static final Map<String, Cache<?, ?>> CACHE_MAP = new ConcurrentHashMap<>();

private final Map<String, Cache<?, ?>> cacheMap;
private CacheManager() { }

private CacheManager() {
cacheMap = new ConcurrentHashMap<>();
public static <K, V> Cache<K, V> createCache(String cacheName, int maximumSize) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.recordStats()
.build();
addCache(cacheName, cache);
return cache;
}

public static CacheManager getInstance() {
return INSTANCE;
public static <K, V> Map<K, V> createCacheAsMap(String cacheName, int maximumSize) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.recordStats()
.build();
addCache(cacheName, cache);
return cache.asMap();
}

public void addCache(String name, Cache<?, ?> cache) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(cache, "cache");
cacheMap.put(name, cache);
public static <K, V> Cache<K, V> createCache(String cacheName, Duration duration) {
Cache<K, V> cache = Caffeine.newBuilder()
.expireAfterWrite(duration)
.recordStats()
.build();
addCache(cacheName, cache);
return cache;
}

public static <K, V> Map<K, V> createCacheAsMap(String cacheName, Duration duration) {
Cache<K, V> cache = Caffeine.newBuilder()
.expireAfterWrite(duration)
.recordStats()
.build();
addCache(cacheName, cache);
return cache.asMap();
}

public Cache<?, ?> getCache(String name) {
Objects.requireNonNull(name, "name");
return cacheMap.get(name);
public static <K, V> Cache<K, V> createCache(String cacheName, int maximumSize, Duration duration) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration)
.recordStats()
.build();
addCache(cacheName, cache);
return cache;
}

public void removeCache(String name) {
Objects.requireNonNull(name, "name");
cacheMap.remove(name);
public static <K, V> Map<K, V> createCacheAsMap(String cacheName, int maximumSize, Duration duration) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration)
.recordStats()
.build();
addCache(cacheName, cache);
return cache.asMap();
}

public CacheStats getCacheStats(String name) {
Objects.requireNonNull(name, "name");
Cache<?, ?> cache = cacheMap.get(name);
if (cache != null) {
return cache.stats();
public static void addCache(String cacheName, Cache<?, ?> cache) {
Objects.requireNonNull(cacheName, "cacheName");
Objects.requireNonNull(cache, "cache");
if (isManaged(cacheName)) {
throw new IllegalArgumentException("Cache with cacheName '" + cacheName + "' is already managed");
}
return null;
CACHE_MAP.put(cacheName, cache);
}

@SuppressWarnings("unchecked")
public static <K, V> Cache<K, V> getCache(String cacheName) {
Objects.requireNonNull(cacheName, "cacheName");
return (Cache<K, V>) CACHE_MAP.get(cacheName);
}

public static void removeCache(String cacheName) {
Objects.requireNonNull(cacheName, "cacheName");
CACHE_MAP.remove(cacheName);
}

public static boolean isManaged(String cacheName) {
Objects.requireNonNull(cacheName, "cacheName");
return CACHE_MAP.containsKey(cacheName);
}
}
56 changes: 19 additions & 37 deletions fhir-core/src/main/java/com/ibm/fhir/core/util/CacheSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,46 @@

package com.ibm.fhir.core.util;

import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ibm.fhir.core.annotation.Cacheable;

public class CacheSupport {
public static <K, V> Map<K, V> createCache(int maximumSize) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
public final class CacheSupport {
private CacheSupport() { }

public static <K, V> Cache<K, V> createCache(Duration duration) {
return Caffeine.newBuilder()
.expireAfterWrite(duration)
.build();
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(String name, int maximumSize) {
Cache<K, V> cache = Caffeine.newBuilder()
public static <K, V> Cache<K, V> createCache(int maximumSize) {
return Caffeine.newBuilder()
.maximumSize(maximumSize)
.build();
CacheManager.getInstance().addCache(name, cache);
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(int duration, TimeUnit unit) {
Cache<K, V> cache = Caffeine.newBuilder()
.expireAfterWrite(duration, unit)
public static <K, V> Cache<K, V> createCache(int maximumSize, Duration duration) {
return Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration)
.build();
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(String name, int duration, TimeUnit unit) {
Cache<K, V> cache = Caffeine.newBuilder()
.expireAfterWrite(duration, unit)
.build();
CacheManager.getInstance().addCache(name, cache);
public static <K, V> Map<K, V> createCacheAsMap(Duration duration) {
Cache<K, V> cache = createCache(duration);
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(int maximumSize, int duration, TimeUnit unit) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration, unit)
.build();
public static <K, V> Map<K, V> createCacheAsMap(int maximumSize) {
Cache<K,V> cache = createCache(maximumSize);
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(String name, int maximumSize, int duration, TimeUnit unit) {
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration, unit)
.build();
CacheManager.getInstance().addCache(name, cache);
public static <K, V> Map<K, V> createCacheAsMap(int maximumSize, Duration duration) {
Cache<K, V> cache = createCache(maximumSize, duration);
return cache.asMap();
}

public static <K, V> Map<K, V> createCache(Cacheable cacheable) {
Objects.requireNonNull(cacheable, "cacheable");
return createCache(cacheable.maximumSize(), cacheable.duration(), cacheable.unit());
}
}
34 changes: 24 additions & 10 deletions fhir-core/src/main/java/com/ibm/fhir/core/util/CachingProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@

package com.ibm.fhir.core.util;

import static com.ibm.fhir.core.util.CacheSupport.createCache;
import static com.ibm.fhir.core.util.CacheKey.key;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import com.ibm.fhir.core.annotation.Cacheable;
import com.ibm.fhir.core.util.CacheKey.Generator;

public class CachingProxy {
private static final Logger log = Logger.getLogger(CachingProxy.class.getName());
Expand All @@ -42,6 +42,16 @@ public static boolean hasCacheableMethod(Class<?> targetClass) {
return false;
}

public interface KeyGenerator {
public static final KeyGenerator DEFAULT = new KeyGenerator() {
@Override
public CacheKey generate(Object target, Method method, Object[] args) {
return key(method, args);
}
};
CacheKey generate(Object target, Method method, Object[] args);
}

private static boolean isCacheable(Method method) {
return method.isAnnotationPresent(Cacheable.class);
}
Expand All @@ -52,7 +62,7 @@ private static class CachingInvocationHandler implements InvocationHandler {
private final Object target;
private final Class<?> targetClass;
private final Map<Method, Method> targetMethodCache = new ConcurrentHashMap<>();
private final Map<Class<? extends CacheKey.Generator>, CacheKey.Generator> keyGeneratorCache = new ConcurrentHashMap<>();
private final Map<Class<? extends KeyGenerator>, KeyGenerator> keyGeneratorCache = new ConcurrentHashMap<>();
private final Map<Method, Map<CacheKey, Object>> resultCacheMap = new ConcurrentHashMap<>();

public CachingInvocationHandler(Object target) {
Expand All @@ -66,12 +76,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
Method targetMethod = targetMethodCache.computeIfAbsent(method, k -> computeTargetMethod(method));
if (isCacheable(targetMethod)) {
Cacheable cacheable = targetMethod.getAnnotation(Cacheable.class);
Class<? extends CacheKey.Generator> keyGeneratorClass = cacheable.keyGeneratorClass();
Class<? extends KeyGenerator> keyGeneratorClass = cacheable.keyGeneratorClass();

CacheKey.Generator keyGenerator = getKeyGenerator(keyGeneratorClass, target, targetMethod, args);
KeyGenerator keyGenerator = getKeyGenerator(keyGeneratorClass, target, targetMethod, args);
CacheKey key = keyGenerator.generate(target, targetMethod, args);

Map<CacheKey, Object> resultCache = resultCacheMap.computeIfAbsent(targetMethod, k -> createCache(cacheable));
Map<CacheKey, Object> resultCache = resultCacheMap.computeIfAbsent(targetMethod, k -> createCacheAsMap(cacheable));
Object result = resultCache.computeIfAbsent(key, k -> computeResult(targetMethod, args));

return (result != NULL) ? result : null;
Expand Down Expand Up @@ -99,21 +109,25 @@ private Method computeTargetMethod(Method method) {
}
}

private CacheKey.Generator getKeyGenerator(Class<? extends CacheKey.Generator> keyGeneratorClass, Object target, Method targetMethod, Object[] args) {
if (CacheKey.Generator.class.equals(keyGeneratorClass)) {
return CacheKey.Generator.DEFAULT;
private KeyGenerator getKeyGenerator(Class<? extends KeyGenerator> keyGeneratorClass, Object target, Method targetMethod, Object[] args) {
if (KeyGenerator.class.equals(keyGeneratorClass)) {
return KeyGenerator.DEFAULT;
}
return keyGeneratorCache.computeIfAbsent(keyGeneratorClass, k -> computeKeyGenerator(keyGeneratorClass));
}

private CacheKey.Generator computeKeyGenerator(Class<? extends Generator> keyGeneratorClass) {
private KeyGenerator computeKeyGenerator(Class<? extends KeyGenerator> keyGeneratorClass) {
try {
return keyGeneratorClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw wrap(e);
}
}

private <K, V> Map<K, V> createCacheAsMap(Cacheable cacheable) {
return CacheSupport.createCacheAsMap(cacheable.maximumSize(), Duration.of(cacheable.duration(), cacheable.unit().toChronoUnit()));
}

private Object computeResult(Method targetMethod, Object[] args) {
try {
log.finest(() -> "Result cache miss for target method: " + targetMethod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package com.ibm.fhir.core.util;

import static com.ibm.fhir.core.util.CacheSupport.createCache;
import static com.ibm.fhir.core.util.CacheSupport.createCacheAsMap;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
Expand All @@ -24,7 +24,7 @@
* A utility class for working with URLs
*/
public class URLSupport {
private static final Map<String, URL> URL_CACHE = createCache(128);
private static final Map<String, URL> URL_CACHE = createCacheAsMap(128);

private URLSupport() { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package com.ibm.fhir.path.evaluator;

import static com.ibm.fhir.core.util.CacheSupport.createCache;
import static com.ibm.fhir.core.util.CacheSupport.createCacheAsMap;
import static com.ibm.fhir.path.FHIRPathDateTimeValue.dateTimeValue;
import static com.ibm.fhir.path.FHIRPathDateValue.dateValue;
import static com.ibm.fhir.path.FHIRPathDecimalValue.decimalValue;
Expand Down Expand Up @@ -97,7 +97,7 @@ public class FHIRPathEvaluator {
public static final Collection<FHIRPathNode> SINGLETON_FALSE = singleton(FHIRPathBooleanValue.FALSE);

private static final int EXPRESSION_CONTEXT_CACHE_MAX_ENTRIES = 512;
private static final Map<String, ExpressionContext> EXPRESSION_CONTEXT_CACHE = createCache(EXPRESSION_CONTEXT_CACHE_MAX_ENTRIES);
private static final Map<String, ExpressionContext> EXPRESSION_CONTEXT_CACHE = createCacheAsMap(EXPRESSION_CONTEXT_CACHE_MAX_ENTRIES);

private final EvaluatingVisitor visitor = new EvaluatingVisitor();

Expand Down Expand Up @@ -283,10 +283,10 @@ public static class EvaluatingVisitor extends FHIRPathBaseVisitor<Collection<FHI
private static final String SYSTEM_NAMESPACE = "System";

private static final int IDENTIFIER_CACHE_MAX_ENTRIES = 2048;
private static final Map<String, Collection<FHIRPathNode>> IDENTIFIER_CACHE = createCache(IDENTIFIER_CACHE_MAX_ENTRIES);
private static final Map<String, Collection<FHIRPathNode>> IDENTIFIER_CACHE = createCacheAsMap(IDENTIFIER_CACHE_MAX_ENTRIES);

private static final int LITERAL_CACHE_MAX_ENTRIES = 128;
private static final Map<String, Collection<FHIRPathNode>> LITERAL_CACHE = createCache(LITERAL_CACHE_MAX_ENTRIES);
private static final Map<String, Collection<FHIRPathNode>> LITERAL_CACHE = createCacheAsMap(LITERAL_CACHE_MAX_ENTRIES);

private EvaluationContext evaluationContext;
private final Stack<Collection<FHIRPathNode>> contextStack = new Stack<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ public void contextInitialized(ServletContextEvent event) {
if (remoteTermServiceProvidersArray != null) {
for (Object remoteTermServiceProviderObject : remoteTermServiceProvidersArray) {
PropertyGroup remoteTermServiceProviderPropertyGroup = (PropertyGroup) remoteTermServiceProviderObject;
Boolean enabled = remoteTermServiceProviderPropertyGroup.getBooleanProperty("enabled", Boolean.FALSE);
if (!enabled) {
continue;
}
try {
Configuration.Builder builder = Configuration.builder();

Expand Down
Loading

0 comments on commit e635bdc

Please sign in to comment.