/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.cache; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Sets; import com.opengamma.sesame.Environment; import com.opengamma.sesame.config.EngineUtils; import com.opengamma.sesame.function.scenarios.FilteredScenarioDefinition; import com.opengamma.sesame.graph.ClassNode; import com.opengamma.sesame.graph.FunctionId; import com.opengamma.sesame.graph.FunctionIdProvider; import com.opengamma.sesame.graph.FunctionModelNode; import com.opengamma.sesame.graph.InterfaceNode; import com.opengamma.sesame.graph.NodeDecorator; import com.opengamma.sesame.graph.ProxyNode; import com.opengamma.sesame.proxy.AbstractProxyInvocationHandler; import com.opengamma.sesame.proxy.InvocationHandlerFactory; import com.opengamma.sesame.proxy.ProxyInvocationHandler; import com.opengamma.util.ArgumentChecker; /** * Decorates a node in the graph with a proxy which performs memoization using a cache. */ public class CachingProxyDecorator extends NodeDecorator { private static final Logger s_logger = LoggerFactory.getLogger(CachingProxyDecorator.class); private final ExecutingMethodsThreadLocal _executingMethods; private final CacheProvider _cacheProvider; /** * Constructs an instance for throwaway uses where the cache doesn't need to be invalidated (e.g. tools) * * @param cacheProvider provider of a cache used to store the calculated values */ public CachingProxyDecorator(CacheProvider cacheProvider) { this(cacheProvider, new ExecutingMethodsThreadLocal()); } /** * @param cacheProvider provider of a cache used to store the calculated values * @param executingMethods records the currently executing methods and allows cache entries to be removed when */ public CachingProxyDecorator(CacheProvider cacheProvider, ExecutingMethodsThreadLocal executingMethods) { _cacheProvider = cacheProvider; _executingMethods = ArgumentChecker.notNull(executingMethods, "executingMethods"); } @Override public FunctionModelNode decorateNode(FunctionModelNode node) { if (!(node instanceof ProxyNode) && !(node instanceof InterfaceNode)) { return node; } Class<?> interfaceType; Class<?> implementationType; if (node instanceof InterfaceNode) { implementationType = ((InterfaceNode) node).getImplementationType(); interfaceType = ((InterfaceNode) node).getType(); } else { implementationType = ((ProxyNode) node).getImplementationType(); interfaceType = ((ProxyNode) node).getType(); } if (EngineUtils.hasMethodAnnotation(interfaceType, Cacheable.class) || EngineUtils.hasMethodAnnotation(implementationType, Cacheable.class)) { Set<Class<?>> subtreeTypes = subtreeImplementationTypes(node); CachingHandlerFactory handlerFactory = new CachingHandlerFactory(implementationType, interfaceType, _cacheProvider, _executingMethods, subtreeTypes); return createProxyNode(node, interfaceType, implementationType, handlerFactory); } return node; } /** * Returns the types built by all nodes in the node's subtree. * * @param node a node * @return the set of all node types in the node's subtree */ private static Set<Class<?>> subtreeImplementationTypes(FunctionModelNode node) { Set<Class<?>> types = new HashSet<>(); populateSubtreeImplementationTypes(node, types); return types; } private static void populateSubtreeImplementationTypes(FunctionModelNode node, Set<Class<?>> accumulator) { // we only want the types for real function nodes, not proxies FunctionModelNode concreteNode = node.getConcreteNode(); if (concreteNode instanceof ClassNode) { accumulator.add(((ClassNode) concreteNode).getImplementationType()); } for (FunctionModelNode childNode : node.getDependencies()) { populateSubtreeImplementationTypes(childNode, accumulator); } } /** * Creates an instance of {@link Handler} when the graph is built. * The handler is invoked when a cacheable method is called and takes care of returning a cached result * or calculating one and putting it in the cache. */ private static final class CachingHandlerFactory implements InvocationHandlerFactory { private final Class<?> _interfaceType; private final Class<?> _implementationType; private final ExecutingMethodsThreadLocal _executingMethods; private final Set<Class<?>> _subtreeTypes; private final CacheProvider _cacheProvider; private CachingHandlerFactory(Class<?> implementationType, Class<?> interfaceType, CacheProvider cacheProvider, ExecutingMethodsThreadLocal executingMethods, Set<Class<?>> subtreeTypes) { _cacheProvider = ArgumentChecker.notNull(cacheProvider, "cacheProvider"); _executingMethods = ArgumentChecker.notNull(executingMethods, "executingMethods"); _subtreeTypes = ArgumentChecker.notNull(subtreeTypes, "subtreeTypes"); _implementationType = ArgumentChecker.notNull(implementationType, "implementationType"); _interfaceType = ArgumentChecker.notNull(interfaceType, "interfaceType"); } @Override public ProxyInvocationHandler create(Object delegate, ProxyNode node, FunctionIdProvider functionIdProvider) { Set<Method> cachedMethods = Sets.newHashSet(); for (Method method : _interfaceType.getMethods()) { if (method.getAnnotation(Cacheable.class) != null) { cachedMethods.add(method); } } for (Method method : _implementationType.getMethods()) { if (method.getAnnotation(Cacheable.class) != null) { // the proxy will always see the interface method. no point caching the instance method // need to go up the inheritance hierarchy and find all interface methods implemented by this method // and cache those for (Class<?> iface : EngineUtils.getInterfaces(_implementationType)) { try { Method ifaceMethod = iface.getMethod(method.getName(), method.getParameterTypes()); cachedMethods.add(ifaceMethod); } catch (NoSuchMethodException e) { // expected } } } } return new Handler(delegate, cachedMethods, _cacheProvider, _executingMethods, _subtreeTypes, functionIdProvider); } @Override public int hashCode() { return Objects.hash(_interfaceType, _implementationType); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final CachingHandlerFactory other = (CachingHandlerFactory) obj; return Objects.equals(this._interfaceType, other._interfaceType) && Objects.equals(this._implementationType, other._implementationType); } } /** * Handles method invocations and possibly returns a cached result instead of calling the underlying object. * If the method doesn't have a {@link Cacheable} annotation the underlying object is called. * If the cache contains an element that corresponds to the method and arguments it's returned and the underlying * object isn't called. * If the cache doesn't contain an element the underlying object is called and the cache is populated. * The values in the cache are futures. This allows multiple threads to request the same value and for all of * them to block while the first thread calculates it. * This is package scoped for testing. */ /* package */ static final class Handler extends AbstractProxyInvocationHandler { private final Object _delegate; private final Set<Method> _cachedMethods; private final CacheProvider _cacheProvider; private final ExecutingMethodsThreadLocal _executingMethods; private final Set<Class<?>> _subtreeTypes; private final FunctionId _functionId; private Handler(Object delegate, Set<Method> cachedMethods, CacheProvider cacheProvider, ExecutingMethodsThreadLocal executingMethods, Set<Class<?>> subtreeTypes, FunctionIdProvider functionIdProvider) { super(delegate); _subtreeTypes = ArgumentChecker.notNull(subtreeTypes, "subtreeTypes"); _cacheProvider = ArgumentChecker.notNull(cacheProvider, "cache"); _executingMethods = ArgumentChecker.notNull(executingMethods, "executingMethods"); _delegate = ArgumentChecker.notNull(delegate, "delegate"); _cachedMethods = ArgumentChecker.notNull(cachedMethods, "cachedMethods"); Object proxiedObject = EngineUtils.getProxiedObject(delegate); _functionId = functionIdProvider.getFunctionId(proxiedObject); } /** * Handles a method invocation, returning a cached value if available, otherwise calling the underlying * method to produce the value. * <p> * If the proxied method is not annotated with {@link Cacheable} it is always invoked. If the * method is annotated a key is created representing the method, the receiver and all the arguments. * This key is used to query the cache. * * @param proxy the proxy on which the method was invoked * @param method the method which was invoked * @param args the method arguments * @return the return value of the underlying method or a previously cached value * @throws Throwable if the underlying method throws an exception */ @Override public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { // check if the method is annotated with @Cacheable. if (_cachedMethods.contains(method)) { // the method is @Cacheable Object[] keyArgs = getArgumentsForCacheKey(args); // create a key representing the method call - the receiver's ID, the method and its arguments MethodInvocationKey key = new MethodInvocationKey(_functionId, method, keyArgs); // create a task to calculate the value if it's not in the cache - calls the underlying method CallableMethod calculationTask = new CallableMethod(key, method, args); // get the value from the cache - if it's not already present it's calculated return _cacheProvider.get().get(key, calculationTask); } else { // the method isn't annotated with @Cacheable, call it try { s_logger.debug("Calculating non-cacheable result by invoking method {}", method); return method.invoke(_delegate, args); } catch (InvocationTargetException e) { throw e.getCause(); } } } /** * <p>Returns the method call arguments that should be used in the cache key for the call's return value. * If the input arguments don't have an {@link Environment} as their first element they are returned. * If the input arguments have an environment as their first element a new set of arguments is returned * that is a copy of the input arguments but containing a different environment.</p> * * <p>The new environment is copied from the environment in the input but uses a different set of scenario * arguments. The new arguments only include the arguments for the functions below this function * in the graph.</p> * * <p>Scenarios above the current function in the graph can't affect the function's return value. If they * were included in the cache key they could cause a cache miss even though there is no way they can * invalidate the cache entry. By removing those arguments from the key we ensure that the only arguments * in the key are the ones that can change this function's return value.</p> * * @param args the arguments to a method call * @return the arguments that should be used in the cache key */ private Object[] getArgumentsForCacheKey(Object[] args) { if (args == null || args.length == 0 || !(args[0] instanceof Environment)) { return args; } Environment env = (Environment) args[0]; if (env.getScenarioDefinition().isEmpty()) { return args; } FilteredScenarioDefinition scenarioDef = env.getScenarioDefinition().forFunctions(_subtreeTypes); Environment newEnv = env.withScenarioDefinition(scenarioDef); Object[] keyArgs = args.clone(); keyArgs[0] = newEnv; return keyArgs; } /** Visible for testing */ /* package */ Object getDelegate() { return _delegate; } private class CallableMethod implements Callable<Object> { private final MethodInvocationKey _key; private final Method _method; private final Object[] _args; public CallableMethod(MethodInvocationKey key, Method method, Object[] args) { _key = key; _method = method; _args = args; } @Override public Object call() throws Exception { try { _executingMethods.push(_key); return _method.invoke(_delegate, _args); } catch (IllegalAccessException | InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof Error) { throw ((Error) cause); } else { throw ((Exception) cause); } } finally { _executingMethods.pop(); } } } } }