/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.cache; import static com.opengamma.sesame.config.ConfigBuilder.argument; import static com.opengamma.sesame.config.ConfigBuilder.arguments; import static com.opengamma.sesame.config.ConfigBuilder.config; import static com.opengamma.sesame.config.ConfigBuilder.function; import static com.opengamma.sesame.config.ConfigBuilder.implementations; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.testng.annotations.Test; import org.threeten.bp.LocalDate; import org.threeten.bp.ZonedDateTime; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.opengamma.sesame.EngineTestUtils; import com.opengamma.sesame.Environment; import com.opengamma.sesame.SimpleEnvironment; import com.opengamma.sesame.config.EngineUtils; import com.opengamma.sesame.config.FunctionModelConfig; import com.opengamma.sesame.engine.ComponentMap; import com.opengamma.sesame.function.FunctionMetadata; import com.opengamma.sesame.function.Output; import com.opengamma.sesame.function.scenarios.AbstractScenarioArgument; import com.opengamma.sesame.function.scenarios.FilteredScenarioDefinition; import com.opengamma.sesame.function.scenarios.ScenarioFunction; import com.opengamma.sesame.graph.FunctionBuilder; import com.opengamma.sesame.graph.FunctionId; import com.opengamma.sesame.graph.FunctionIdProvider; import com.opengamma.sesame.graph.FunctionModel; import com.opengamma.sesame.marketdata.MarketDataBundle; import com.opengamma.sesame.marketdata.MarketDataId; import com.opengamma.timeseries.date.DateTimeSeries; import com.opengamma.util.result.Result; import com.opengamma.util.test.TestGroup; import com.opengamma.util.time.LocalDateRange; @SuppressWarnings("unchecked") @Test(groups = TestGroup.UNIT) public class CachingProxyDecoratorTest { private static final Set<Class<?>> NO_COMPONENTS = ComponentMap.EMPTY.getComponentTypes(); private final CacheProvider _cacheProvider = EngineTestUtils.createCacheProvider(); /** check the cache contains the item returns from the function */ @Test public void oneLookup() throws Exception { FunctionModelConfig config = config(implementations(TestFn.class, Impl.class), arguments(function(Impl.class, argument("s", "s")))); FunctionBuilder functionBuilder = new FunctionBuilder(); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionMetadata metadata = EngineUtils.createMetadata(TestFn.class, "foo"); FunctionModel functionModel = FunctionModel.forFunction(metadata, config, NO_COMPONENTS, cachingDecorator); TestFn fn = (TestFn) functionModel.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); Method foo = EngineUtils.getMethod(TestFn.class, "foo"); CachingProxyDecorator.Handler invocationHandler = (CachingProxyDecorator.Handler) Proxy.getInvocationHandler(fn); Impl delegate = (Impl) invocationHandler.getDelegate(); FunctionId functionId = functionBuilder.getFunctionId(delegate); MethodInvocationKey key = new MethodInvocationKey(functionId, foo, new Object[]{"bar"}); Object results = fn.foo("bar"); Object value = _cacheProvider.get().getIfPresent(key); assertNotNull(value); assertSame(value, results); } /** check that multiple instances of the same function return the cached value when invoked with the same args */ @Test public void multipleFunctions() { FunctionModelConfig config = config(implementations(TestFn.class, Impl.class), arguments(function(Impl.class, argument("s", "s")))); FunctionBuilder functionBuilder = new FunctionBuilder(); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionMetadata metadata = EngineUtils.createMetadata(TestFn.class, "foo"); FunctionModel functionModel1 = FunctionModel.forFunction(metadata, config, NO_COMPONENTS, cachingDecorator); TestFn fn1 = (TestFn) functionModel1.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); FunctionModel functionModel2 = FunctionModel.forFunction(metadata, config, NO_COMPONENTS, cachingDecorator); TestFn fn2 = (TestFn) functionModel2.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); assertSame(fn1.foo("bar"), fn2.foo("bar")); } /** * check that multiple identical calls produce the same value even if the underlying function doesn't. * this isn't how functions are supposed to work but it demonstrates a point for testing */ @Test public void multipleCalls() { FunctionModelConfig config = config(implementations(TestFn.class, Impl.class), arguments(function(Impl.class, argument("s", "s")))); FunctionBuilder functionBuilder = new FunctionBuilder(); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionMetadata metadata = EngineUtils.createMetadata(TestFn.class, "foo"); FunctionModel functionModel = FunctionModel.forFunction(metadata, config, NO_COMPONENTS, cachingDecorator); TestFn fn = (TestFn) functionModel.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); assertSame(fn.foo("bar"), fn.foo("bar")); } @Test public void sameFunctionDifferentConstructorArgs() { FunctionModelConfig config1 = config(implementations(TestFn.class, Impl.class), arguments(function(Impl.class, argument("s", "a string")))); FunctionModelConfig config2 = config(implementations(TestFn.class, Impl.class), arguments(function(Impl.class, argument("s", "a different string")))); FunctionMetadata metadata = EngineUtils.createMetadata(TestFn.class, "foo"); FunctionBuilder functionBuilder = new FunctionBuilder(); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionModel functionModel1 = FunctionModel.forFunction(metadata, config1, NO_COMPONENTS, cachingDecorator); TestFn fn1 = (TestFn) functionModel1.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); FunctionModel functionModel2 = FunctionModel.forFunction(metadata, config2, NO_COMPONENTS, cachingDecorator); TestFn fn2 = (TestFn) functionModel2.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); Object val1 = fn1.foo("bar"); Object val2 = fn2.foo("bar"); assertTrue(val1 != val2); } interface TestFn { @Cacheable @Output("Foo") Object foo(String arg); } public static class Impl implements TestFn { private final String _s; public Impl(String s) { _s = s; } @Override public Object foo(String arg) { return _s + new Object(); } } /* package */ interface TopLevelFn { @Output("topLevel") Object fn(); } public static class TopLevel implements TopLevelFn { private final DelegateFn _delegateFn; public TopLevel(DelegateFn delegateFn) { _delegateFn = delegateFn; } @Override @Cacheable public Object fn() { return _delegateFn.fn(); } } /* package */ interface DelegateFn { Object fn(); } public static class Delegate1 implements DelegateFn { private final String _s; public Delegate1(String s) { _s = s; } @Override public Object fn() { return _s + new Object(); } } public static class Delegate2 implements DelegateFn { private final String _s; public Delegate2(String s) { _s = s; } @Override public Object fn() { return _s + new Object(); } } /** * 2 functions where the top level function is the same and the dependency functions are the same implementation * type but have different constructor args. */ @Test public void sameFunctionDifferentDependencyInstances() { FunctionModelConfig config1 = config(implementations(TopLevelFn.class, TopLevel.class, DelegateFn.class, Delegate1.class), arguments(function(Delegate1.class, argument("s", "a string")))); FunctionModelConfig config2 = config(implementations(TopLevelFn.class, TopLevel.class, DelegateFn.class, Delegate1.class), arguments(function(Delegate1.class, argument("s", "a different string")))); FunctionMetadata metadata = EngineUtils.createMetadata(TopLevelFn.class, "fn"); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionBuilder functionBuilder = new FunctionBuilder(); FunctionModel functionModel1 = FunctionModel.forFunction(metadata, config1, NO_COMPONENTS, cachingDecorator); TopLevelFn fn1 = (TopLevelFn) functionModel1.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); FunctionModel functionModel2 = FunctionModel.forFunction(metadata, config2, NO_COMPONENTS, cachingDecorator); TopLevelFn fn2 = (TopLevelFn) functionModel2.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); Object val1 = fn1.fn(); Object val2 = fn2.fn(); assertTrue(val1 != val2); } /** * 2 functions where the top level function is the same and the dependency functions implement the same interface * but are instances of different classes. */ @Test public void sameFunctionDifferentDependencyTypes() { FunctionModelConfig config1 = config(implementations(TopLevelFn.class, TopLevel.class, DelegateFn.class, Delegate1.class), arguments(function(Delegate1.class, argument("s", "a string")))); FunctionModelConfig config2 = config(implementations(TopLevelFn.class, TopLevel.class, DelegateFn.class, Delegate2.class), arguments(function(Delegate2.class, argument("s", "a string")))); FunctionMetadata metadata = EngineUtils.createMetadata(TopLevelFn.class, "fn"); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionBuilder functionBuilder = new FunctionBuilder(); FunctionModel functionModel1 = FunctionModel.forFunction(metadata, config1, NO_COMPONENTS, cachingDecorator); TopLevelFn fn1 = (TopLevelFn) functionModel1.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); FunctionModel functionModel2 = FunctionModel.forFunction(metadata, config2, NO_COMPONENTS, cachingDecorator); TopLevelFn fn2 = (TopLevelFn) functionModel2.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); Object val1 = fn1.fn(); Object val2 = fn2.fn(); assertTrue(val1 != val2); } /** check caching works when the class method is annotated and the interface isn't */ @Test public void annotationOnClass() throws Exception { FunctionModelConfig config = config(implementations(TestFn2.class, Impl2.class)); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionMetadata metadata = EngineUtils.createMetadata(TestFn2.class, "foo"); FunctionModel functionModel = FunctionModel.forFunction(metadata, config, NO_COMPONENTS, cachingDecorator); FunctionBuilder functionBuilder = new FunctionBuilder(); TestFn2 fn = (TestFn2) functionModel.build(functionBuilder, ComponentMap.EMPTY).getReceiver(); Method foo = EngineUtils.getMethod(TestFn2.class, "foo"); CachingProxyDecorator.Handler invocationHandler = (CachingProxyDecorator.Handler) Proxy.getInvocationHandler(fn); Impl2 delegate = (Impl2) invocationHandler.getDelegate(); MethodInvocationKey key = new MethodInvocationKey(functionBuilder.getFunctionId(delegate), foo, new Object[]{"bar"}); Object results = fn.foo("bar"); Object value = _cacheProvider.get().getIfPresent(key); assertNotNull(value); assertSame(value, results); } interface TestFn2 { @Output("Foo") Object foo(String arg); } public static class Impl2 implements TestFn2 { @Cacheable @Override public Object foo(String arg) { return new Object(); } } /** Check the expected cache keys are pushed onto a thread local stack while a cacheable method executes. */ @Test public void executingMethods() { FunctionBuilder functionBuilder = new FunctionBuilder(); FunctionModelConfig config = config(implementations(ExecutingMethodsI1.class, ExecutingMethodsC1.class, ExecutingMethodsI2.class, ExecutingMethodsC2.class)); ExecutingMethodsThreadLocal executingMethods = new ExecutingMethodsThreadLocal(); ImmutableMap<Class<?>, Object> components = ImmutableMap.of(ExecutingMethodsThreadLocal.class, executingMethods, FunctionIdProvider.class, functionBuilder); ComponentMap componentMap = ComponentMap.of(components); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider, executingMethods); ExecutingMethodsI1 i1 = FunctionModel.build(ExecutingMethodsI1.class, config, componentMap, functionBuilder, cachingDecorator); i1.fn("s", 1); } interface ExecutingMethodsI1 { @Output("abc") @Cacheable Object fn(String s, int i); } public static class ExecutingMethodsC1 implements ExecutingMethodsI1 { private final ExecutingMethodsI2 _i2; private final FunctionIdProvider _functionIdProvider; private final ExecutingMethodsThreadLocal _executingMethods; public ExecutingMethodsC1(ExecutingMethodsI2 i2, ExecutingMethodsThreadLocal executingMethods, FunctionIdProvider functionIdProvider) { _i2 = i2; _functionIdProvider = functionIdProvider; // this is a bit grubby but necessary so the method keys can be checked CachingProxyDecorator.Handler handler = (CachingProxyDecorator.Handler) Proxy.getInvocationHandler(i2); ExecutingMethodsC2 c2 = (ExecutingMethodsC2) handler.getDelegate(); c2._c1 = this; _executingMethods = executingMethods; } @Override public Object fn(String s, int i) { Method fn = EngineUtils.getMethod(ExecutingMethodsI1.class, "fn"); FunctionId functionId = _functionIdProvider.getFunctionId(this); MethodInvocationKey key = new MethodInvocationKey(functionId, fn, new Object[]{s, i}); LinkedList<MethodInvocationKey> expected = Lists.newLinkedList(); expected.add(key); assertEquals(expected, _executingMethods.get()); Object retVal = _i2.fn(s, i, s + i); assertEquals(expected, _executingMethods.get()); return retVal; } } interface ExecutingMethodsI2 { @Cacheable Object fn(String s, int i, String s2); } public static class ExecutingMethodsC2 implements ExecutingMethodsI2 { private final ExecutingMethodsThreadLocal _executingMethods; private final FunctionIdProvider _functionIdProvider; private ExecutingMethodsC1 _c1; public ExecutingMethodsC2(ExecutingMethodsThreadLocal executingMethods, FunctionIdProvider functionIdProvider) { _executingMethods = executingMethods; _functionIdProvider = functionIdProvider; } @Override public Object fn(String s, int i, String s2) { Method fn1 = EngineUtils.getMethod(ExecutingMethodsI1.class, "fn"); FunctionId id1 = _functionIdProvider.getFunctionId(_c1); MethodInvocationKey key1 = new MethodInvocationKey(id1, fn1, new Object[]{s, i}); Method fn2 = EngineUtils.getMethod(ExecutingMethodsI2.class, "fn"); FunctionId thisId = _functionIdProvider.getFunctionId(this); MethodInvocationKey key2 = new MethodInvocationKey(thisId, fn2, new Object[]{s, i, s2}); LinkedList<MethodInvocationKey> expected = Lists.newLinkedList(); expected.add(key2); expected.add(key1); assertEquals(expected, _executingMethods.get()); return "not used"; } } /** * check that scenario arguments in the environment are only included the cache key for functions whose return value * can be affected by the scenario. */ @Test public void pruneScenarioArguments() throws ExecutionException, InterruptedException { FunctionModelConfig config = config(implementations(Fn1.class, ScenarioImpl1.class, Fn2.class, ScenarioImpl2.class)); CachingProxyDecorator cachingDecorator = new CachingProxyDecorator(_cacheProvider); FunctionBuilder functionBuilder = new FunctionBuilder(); Fn1 i1 = FunctionModel.build(Fn1.class, config, ComponentMap.EMPTY, functionBuilder, cachingDecorator); ZonedDateTime valuationTime = ZonedDateTime.now(); MarketDataBundle marketDataBundle = new MarketDataBundle() { @Override public <T, I extends MarketDataId<T>> Result<T> get(I id, Class<T> dataType) { throw new UnsupportedOperationException("get not implemented"); } @Override public <T, I extends MarketDataId<T>> Result<DateTimeSeries<LocalDate, T>> get( I id, Class<T> dataType, LocalDateRange dateRange) { throw new UnsupportedOperationException("get not implemented"); } @Override public MarketDataBundle withTime(ZonedDateTime time) { throw new UnsupportedOperationException("withTime not implemented"); } @Override public MarketDataBundle withDate(LocalDate date) { throw new UnsupportedOperationException("withDate not implemented"); } }; // for calls to ScenarioArgumentsC1 the args for ScenarioArgumentsC2 will be included in the key // because ScenarioArgumentsC1 calls ScenarioArgumentsC2 FilteredScenarioDefinition scenarioDef1 = new FilteredScenarioDefinition(new Args1(), new Args2()); // for calls to ScenarioArgumentsC2 the args for ScenarioArgumentsC1 will be filtered out because // ScenarioArgumentsC2 doesn't call ScenarioArgumentsC1 and therefore its scenario arguments can't affect // any values calculated by ScenarioArgumentsC2 FilteredScenarioDefinition scenarioDef2 = new FilteredScenarioDefinition(new Args2()); // env1 is passed to the functions. it contains scenario arguments for all classes SimpleEnvironment env1 = new SimpleEnvironment(valuationTime, marketDataBundle, scenarioDef1); // env2 is the environment that should be passed to ScenarioArgumentsC2 - its scenario arguments have been // filtered to only include the ones applicable to ScenarioArgumentsC2 and its dependencies SimpleEnvironment env2 = new SimpleEnvironment(valuationTime, marketDataBundle, scenarioDef2); i1.fn(env1, "s1", 1); i1.fn(env1, "s2", 2); ScenarioImpl1 c1 = (ScenarioImpl1) EngineUtils.getProxiedObject(i1); ScenarioImpl2 c2 = (ScenarioImpl2) EngineUtils.getProxiedObject(c1._fn2); FunctionId id1 = functionBuilder.getFunctionId(c1); FunctionId id2 = functionBuilder.getFunctionId(c2); Method method1 = EngineUtils.getMethod(Fn1.class, "fn"); Method method2 = EngineUtils.getMethod(Fn2.class, "fn"); checkValueIsInCache(env1, "s1", 1, id1, method1, "S1 1"); checkValueIsInCache(env1, "s2", 2, id1, method1, "S2 2"); checkValueIsInCache(env2, "s1", 1, id2, method2, "s1 1"); checkValueIsInCache(env2, "s2", 2, id2, method2, "s2 2"); } /** * Checks a value is in the cache after a call to a cacheable method. * <p> * The arguments, the receiver and the method are used to build a key which is used to look up the cached value. * * @param env the environment argument to the method * @param stringArg the string argument to the method * @param intArg the int argument to the method * @param functionId the ID of the function receiving the method call * @param method the method called * @param expectedValue the value that should be in the cache */ private void checkValueIsInCache(Environment env, String stringArg, int intArg, FunctionId functionId, Method method, String expectedValue) throws InterruptedException, ExecutionException { MethodInvocationKey key = new MethodInvocationKey(functionId, method, new Object[]{env, stringArg, intArg}); Object value = _cacheProvider.get().getIfPresent(key); assertNotNull(value); assertEquals(expectedValue, value); } interface Fn1 { @Cacheable Object fn(Environment env, String s, Integer i); } public static class ScenarioImpl1 implements Fn1, ScenarioFunction<Args1, ScenarioImpl1> { private final Fn2 _fn2; public ScenarioImpl1(Fn2 fn2) { _fn2 = fn2; } @Override public Object fn(Environment env, String s, Integer i) { return _fn2.fn(env, s, i).toUpperCase(); } @Nullable @Override public Class<Args1> getArgumentType() { return Args1.class; } } interface Fn2 { @Cacheable String fn(Environment env, String s, Integer i); } public static class ScenarioImpl2 implements Fn2, ScenarioFunction<Args2, ScenarioImpl2> { @Override public String fn(Environment env, String s, Integer i) { return s + " " + i; } @Nullable @Override public Class<Args2> getArgumentType() { return Args2.class; } } public static class Args1 extends AbstractScenarioArgument<Args1, ScenarioImpl1> { private Args1() { super(ScenarioImpl1.class); } @Override public boolean equals(Object obj) { return obj instanceof Args1; } @Override public int hashCode() { return 1; } } public static class Args2 extends AbstractScenarioArgument<Args2, ScenarioImpl2> { private Args2() { super(ScenarioImpl2.class); } @Override public boolean equals(Object obj) { return obj instanceof Args2; } @Override public int hashCode() { return 2; } } }