/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.engine; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; import org.threeten.bp.ZonedDateTime; import com.codahale.metrics.MetricRegistry; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.analytics.env.AnalyticsEnvironment; import com.opengamma.core.position.PositionOrTrade; import com.opengamma.core.security.Security; import com.opengamma.id.VersionCorrection; import com.opengamma.service.ServiceContext; import com.opengamma.service.ThreadLocalServiceContext; import com.opengamma.service.VersionCorrectionProvider; import com.opengamma.sesame.Environment; import com.opengamma.sesame.cache.CacheInvalidator; import com.opengamma.sesame.cache.CacheProvider; import com.opengamma.sesame.cache.CachingProxyDecorator; import com.opengamma.sesame.cache.DefaultFunctionCache; import com.opengamma.sesame.cache.ExecutingMethodsThreadLocal; import com.opengamma.sesame.cache.FunctionCache; import com.opengamma.sesame.config.FunctionArguments; import com.opengamma.sesame.config.FunctionModelConfig; import com.opengamma.sesame.config.NonPortfolioOutput; import com.opengamma.sesame.config.ViewColumn; import com.opengamma.sesame.config.ViewConfig; import com.opengamma.sesame.function.AvailableImplementations; import com.opengamma.sesame.function.AvailableOutputs; import com.opengamma.sesame.function.InvalidInputFunction; import com.opengamma.sesame.function.InvokableFunction; import com.opengamma.sesame.function.PermissionDeniedFunction; import com.opengamma.sesame.function.scenarios.FilteredScenarioDefinition; import com.opengamma.sesame.function.scenarios.ScenarioDefinition; import com.opengamma.sesame.graph.CompositeNodeDecorator; import com.opengamma.sesame.graph.FunctionBuilder; import com.opengamma.sesame.graph.FunctionModel; import com.opengamma.sesame.graph.Graph; import com.opengamma.sesame.graph.GraphBuilder; import com.opengamma.sesame.graph.GraphModel; import com.opengamma.sesame.graph.NodeDecorator; import com.opengamma.sesame.marketdata.GatheringMarketDataBundle; import com.opengamma.sesame.marketdata.MarketDataEnvironment; import com.opengamma.sesame.marketdata.MarketDataRequirement; import com.opengamma.sesame.proxy.ExceptionWrappingProxy; import com.opengamma.sesame.proxy.MetricsProxy; import com.opengamma.sesame.trace.CallGraph; import com.opengamma.sesame.trace.Tracer; import com.opengamma.sesame.trace.TracingProxy; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.result.Failure; import com.opengamma.util.result.Result; /** * View is the main class for running calculations over a portfolio and producing results. * <p> * A view is created by a {@link ViewFactory}. It defines a set of calculations used to populate a * table of results when the view is run. The columns in the table are defined by a {@link ViewConfig} * and there is one row in the table of results for each item in the portfolio. * <p> * A view can also define a set of outputs which are calculations that are performed independently of * the portfolio. For example an output could be defined to return the curve used in the calculations. * <p> * A view is executed by calling one of the {@code run} or {@code runAsync} methods. A view can be run * repeatedly and can execute multiple runs concurrently. */ public class View { private static final Logger s_logger = LoggerFactory.getLogger(View.class); private final Graph _graph; private final ViewConfig _viewConfig; private final ListeningExecutorService _executor; private final FunctionModelConfig _systemDefaultConfig; private final List<String> _columnNames; private final GraphModel _graphModel; /** * Provider that supplies caches to this view. A cache is requested at the start of each calculation * cycle and used for the duration of the cycle. Normally the factory will return the same cache each * cycle. However if the cache is cleared the factory will create a new, empty cache which will be * returned at the start of the next calculation cycle. */ private final CacheProvider _cacheFactory; /** For building new, empty caches that are only used for a single calculation cycle. */ private final CacheBuilder<Object, Object> _cacheBuilder; private final Optional<MetricRegistry> _metricRegistry; private final ComponentMap _componentMap; private final CacheInvalidator _cacheInvalidator; /** * Thread local variable used to expose the cache for the current cycle to the caching proxy. * In order to ensure consistency in the calculations, the same cache must be used for the whole * of a calculation cycle. Therefore the caching proxy must have a way to get hold of the correct * cache when it is handling a method call. There is no way to communicate an ID for the cycle * between the view and the proxy, so the cache is stored in a thread local which is used by * the cache provider. */ private final ThreadLocal<Cache<Object, Object>> _cacheThreadLocal = new ThreadLocal<>(); /** Whether caching is enabled. */ private final boolean _cachingEnabled; View(ViewConfig viewConfig, ExecutorService executor, FunctionModelConfig systemDefaultConfig, FunctionBuilder functionBuilder, EnumSet<FunctionService> services, ComponentMap componentMap, Set<Class<?>> inputTypes, AvailableOutputs availableOutputs, AvailableImplementations availableImplementations, CacheProvider cacheFactory, CacheBuilder<Object, Object> cacheBuilder, CacheInvalidator cacheInvalidator, Optional<MetricRegistry> metricRegistry) { // Provider that supplies the cache to the caching decorators // the field is updated with a cache from _cacheFactory at the start of each cycle CacheProvider cacheProvider = new CacheProvider() { @Override public Cache<Object, Object> get() { return _cacheThreadLocal.get(); } }; FunctionCache cache = new DefaultFunctionCache(cacheProvider); _cacheBuilder = ArgumentChecker.notNull(cacheBuilder, "cacheBuilder"); _cachingEnabled = services.contains(FunctionService.CACHING); _cacheInvalidator = ArgumentChecker.notNull(cacheInvalidator, "cacheInvalidator"); _componentMap = ArgumentChecker.notNull(componentMap, "componentMap").with(FunctionCache.class, cache); _viewConfig = ArgumentChecker.notNull(viewConfig, "viewConfig"); _executor = MoreExecutors.listeningDecorator(executor); _systemDefaultConfig = ArgumentChecker.notNull(systemDefaultConfig, "systemDefaultConfig"); _cacheFactory = ArgumentChecker.notNull(cacheFactory, "cacheFactory"); _metricRegistry = ArgumentChecker.notNull(metricRegistry, "metricRegistry"); _columnNames = columnNames(_viewConfig); ExecutingMethodsThreadLocal executingMethods = new ExecutingMethodsThreadLocal(); NodeDecorator decorator = createNodeDecorator(services, cacheProvider, executingMethods); s_logger.debug("building graph model"); GraphBuilder graphBuilder = new GraphBuilder(availableOutputs, availableImplementations, _componentMap.getComponentTypes(), systemDefaultConfig, decorator); _graphModel = graphBuilder.build(viewConfig, inputTypes); s_logger.debug("graph model complete, building graph"); _graph = _graphModel.build(_componentMap, functionBuilder); s_logger.debug("graph complete"); } private NodeDecorator createNodeDecorator(EnumSet<FunctionService> services, CacheProvider cacheProvider, ExecutingMethodsThreadLocal executingMethods) { ImmutableList.Builder<NodeDecorator> decorators = new ImmutableList.Builder<>(); // Build up the proxies to be used from the outermost // to the innermost // Timing/tracing sits outside of caching so the actual // time taken for a request is reported. This can also // report on whether came from the cache or were // calculated (if cache there will be no child calls). // Only allow one tracing proxy but pick the most // comprehensive one if (services.contains(FunctionService.TRACING)) { decorators.add(TracingProxy.INSTANCE); } // Caching proxy memoizes requests as required so that // expensive calculations are not performed more // frequently than they need to be if (services.contains(FunctionService.CACHING)) { decorators.add(new CachingProxyDecorator(cacheProvider, executingMethods)); } // Metrics records time taken to execute each function. This // sits inside the caching layer as we're interested in how // long the actual calculation takes not how long it takes to // get from the cache if (services.contains(FunctionService.METRICS)) { if (_metricRegistry.isPresent()) { decorators.add(new MetricsProxy(_metricRegistry.get())); } else { // This should be prevented by the ViewFactoryComponentFactory but is // here in case of programmatic misconfiguration s_logger.warn("Unable to create metrics proxy as no metrics repository has been configured"); } } // Ensure we always have the exception wrapping behaviour so // methods returning Result<?> return Failure if an exception // is thrown internally. decorators.add(ExceptionWrappingProxy.INSTANCE); return CompositeNodeDecorator.compose(decorators.build()); } /** * Runs a single calculation cycle, blocking until the results are available. * * @param cycleArguments settings for running the calculations * @return the calculation results * @deprecated use {@link #run(CalculationArguments, MarketDataEnvironment)} */ @Deprecated public Results run(CycleArguments cycleArguments) { return run(cycleArguments, Collections.emptyList()); } /** * Runs a single calculation cycle, blocking until the results are available. * * @param cycleArguments settings for running the calculations * @param inputs the inputs to the calculation, e.g. trades, positions, securities * @return the calculation results * @deprecated use {@link #run(CalculationArguments, MarketDataEnvironment, List)} */ @Deprecated public Results run(CycleArguments cycleArguments, List<?> inputs) { try { CalculationArguments calculationArguments = CalculationArguments.builder() .valuationTime(cycleArguments.getValuationTime()) .captureInputs(cycleArguments.isCaptureInputs()) .configVersionCorrection(cycleArguments.getConfigVersionCorrection()) .traceCells(cycleArguments.getTraceCells()) .traceOutputs(cycleArguments.getTraceOutputs()) .build(); return runAsync(calculationArguments, cycleArguments.getMarketDataEnvironment(), inputs).get(); } catch (InterruptedException | ExecutionException e) { throw new OpenGammaRuntimeException("Failed to run view", e); } } /** * Runs a single calculation cycle, blocking until the results are available. * * @param cycleArguments settings for running the calculations * @return the calculation results */ public Results run(CalculationArguments cycleArguments, MarketDataEnvironment marketData) { return run(cycleArguments, marketData, Collections.emptyList()); } /** * Runs a single calculation cycle, blocking until the results are available. * * @param calculationArguments settings for running the calculations * @param inputs the inputs to the calculation, e.g. trades, positions, securities * @return the calculation results */ public Results run(CalculationArguments calculationArguments, MarketDataEnvironment marketData, List<?> inputs) { try { return runAsync(calculationArguments, marketData, inputs).get(); } catch (InterruptedException | ExecutionException e) { throw new OpenGammaRuntimeException("Failed to run view", e); } } /** * Runs a single calculation cycle asynchronously, returning a future representing the pending results. * * @param calculationArguments settings for running the calculations * @return a future representing the calculation results */ public ListenableFuture<Results> runAsync(CalculationArguments calculationArguments, MarketDataEnvironment marketData) { return runAsync(calculationArguments, marketData, Collections.emptyList()); } /** * Runs a single calculation cycle asynchronously, returning a future representing the pending results. * * @param calculationArguments settings for running the calculations * @param inputs the inputs to the calculation, e.g. trades, positions, securities * @return a future representing the calculation results * @throws IllegalStateException if ThreadLocalServiceContext not set */ public ListenableFuture<Results> runAsync(CalculationArguments calculationArguments, MarketDataEnvironment marketData, final List<?> inputs) { ArgumentChecker.notNull(calculationArguments, "calculationArguments"); final Instant start = Instant.now(); final long startInitialization = System.nanoTime(); final long startExecution; /* * Get a cache from the factory that will be used for the duration of this calculation cycle. * the same cache must be used for all calculations in a cycle to ensure consistency in the results. * * The cache is never cleared during a cycle, if the user requests a clean cache then an empty cache is * created and returned by the factory at the start of the next cycle. * * Therefore it is possible that two cycles can be executing concurrently in the same view using a different cache. * It is essential to ensure that the correct cache is used for each cycle even though the caching proxy * might receive invocations for different cycles interleaved with each other. * * To achieve this, a thread local is used to hold the cache. there is one ThreadLocalWrapper for each cycle * which contains the cache for that cycle. It also contains the thread local that will hold the cache to allow * the caching proxy to retrieve it. The tasks that perform the calculations set the thread local with * the cycle's cache before executing the calculations and clearing the thread local afterwards. */ Cache<Object, Object> cache = getCache(); VersionCorrectionProvider vcProvider = getVersionCorrectionProvider(calculationArguments); ServiceContext originalContext = getThreadLocalServiceContext(); ServiceContext context = originalContext.with(VersionCorrectionProvider.class, vcProvider); final CycleInitializer cycleInitializer = calculationArguments.isCaptureInputs() ? new CapturingCycleInitializer(context, _componentMap, calculationArguments, marketData, _graphModel, _viewConfig, _cacheBuilder, inputs) : new StandardCycleInitializer(context, _graph, cache); ServiceContext cycleContext = cycleInitializer.getServiceContext(); ThreadLocalWrapper threadLocalWrapper = new ThreadLocalWrapper(cycleContext, originalContext, cycleInitializer.getCache(), _cacheThreadLocal, AnalyticsEnvironment.getInstance(), false); ListenableFuture<List<TaskResult>> tasksFuture = runAsync(calculationArguments, marketData, cycleInitializer, threadLocalWrapper, inputs); startExecution = System.nanoTime(); return Futures.transform(tasksFuture, new Function<List<TaskResult>, Results>() { @Nullable @Override public Results apply(List<TaskResult> taskResults) { return buildResults(inputs, taskResults, start, startInitialization, startExecution, cycleInitializer); } }); } /** * Collects requirements for market data that must be provided for running the calculations in this view for * a portfolio. * <p> * If the data is present in {@code suppliedData} it won't be included in the returned requirements. * * @param suppliedData market data that is already available and therefore doesn't need to be built or provided * @param calculationArguments arguments specifying how the calculations should be performed * @param portfolio the portfolio for which the calculations are being performed * @return requirements for the market data needed to run the calculations in this view for the specified portfolio */ public Set<MarketDataRequirement> gatherRequirements(MarketDataEnvironment suppliedData, CalculationArguments calculationArguments, List<?> portfolio) { Cache<Object, Object> cache = getCache(); VersionCorrectionProvider vcProvider = getVersionCorrectionProvider(calculationArguments); ServiceContext originalContext = getThreadLocalServiceContext(); ServiceContext context = originalContext.with(VersionCorrectionProvider.class, vcProvider); CycleInitializer cycleInitializer = new StandardCycleInitializer(context, _graph, cache); ThreadLocalWrapper threadLocalWrapper = new ThreadLocalWrapper(context, originalContext, cycleInitializer.getCache(), _cacheThreadLocal, AnalyticsEnvironment.getInstance(), true); ZonedDateTime valuationTime = calculationArguments.getValuationTime(); GatheringMarketDataBundle gatheringBundle = GatheringMarketDataBundle.create(suppliedData.toBundle()); MarketDataEnvironment marketData = new GatheringMarketDataEnvironment(gatheringBundle, valuationTime); ListenableFuture<List<TaskResult>> tasksFuture = runAsync(calculationArguments, marketData, cycleInitializer, threadLocalWrapper, portfolio); try { tasksFuture.get(); } catch (InterruptedException | ExecutionException e) { // this will only happen if there is a bug, all exceptions should be caught and wrapped in a failure result throw new OpenGammaRuntimeException("Failed to gather requirements", e); } return gatheringBundle.getRequirements(); } /** * Asynchronously runs the calculations in this view, returning a future representing the results. * * @param calculationArguments arguments specifying how the calculations should be performed * @param marketData market data used by the calculations * @param cycleInitializer for setting up the calculation cycle * @param threadLocalWrapper for setting up and tearing down thread-local state in each task * @param portfolio the portfolio for which the calculations should be run * @return a future representing the results of the calculations */ private ListenableFuture<List<TaskResult>> runAsync(CalculationArguments calculationArguments, MarketDataEnvironment marketData, CycleInitializer cycleInitializer, ThreadLocalWrapper threadLocalWrapper, List<?> portfolio) { List<Task> tasks = new ArrayList<>(); Graph graph = cycleInitializer.getGraph(); ScenarioDefinition scenario = _viewConfig.getScenarioDefinition(); tasks.addAll(portfolioTasks(calculationArguments, marketData, portfolio, graph, scenario, threadLocalWrapper)); tasks.addAll(nonPortfolioTasks(calculationArguments, marketData, graph, scenario, threadLocalWrapper)); List<ListenableFuture<TaskResult>> resultFutures = invokeTasks(tasks); return Futures.allAsList(resultFutures); } /** * Creates a version correction provider based on the version correction in the arguments. * * @param calculationArguments arguments containing a version correction * @return provider for the version correction in the arguments */ private VersionCorrectionProvider getVersionCorrectionProvider(CalculationArguments calculationArguments) { VersionCorrection correction = calculationArguments.getConfigVersionCorrection(); return correction == null || correction.containsLatest() ? new FixedInstantVersionCorrectionProvider() : new FixedInstantVersionCorrectionProvider(correction.getVersionAsOf()); } /** * @return a cache, appropriate for the cache settings */ private Cache<Object, Object> getCache() { return _cachingEnabled ? _cacheFactory.get() : new NoOpCache(); } /** * @return the service context from {@link ThreadLocalServiceContext} * @throws IllegalStateException if there is no thread-local service context */ private static ServiceContext getThreadLocalServiceContext() { ServiceContext originalContext = ThreadLocalServiceContext.getInstance(); if (originalContext == null) { throw new IllegalStateException("ThreadLocalServiceContext not set"); } return originalContext; } /** * Builds a set of results from a list of futures representing the individual pending calculation results. * * @param portfolio the inputs to the trade calculations, e.g. trades, securities * @param taskResults the results of the individual calculations * @param start the start time of the calculation cycle * @param startInitialization the start time of the cycle (system nano time) * @param startExecution the start time of the calculations (system nano time) * @param cycleInitializer for post-processing the results * @return the results of the calculations */ private Results buildResults(List<?> portfolio, List<TaskResult> taskResults, Instant start, long startInitialization, long startExecution, CycleInitializer cycleInitializer) { ResultBuilder resultsBuilder = Results.builder(portfolio, _columnNames); for (TaskResult result : taskResults) { result.addToResults(resultsBuilder); } long startResultsBuild = System.nanoTime(); Results results = resultsBuilder.build(start, startExecution, startInitialization, startResultsBuild); return cycleInitializer.complete(results); } /** * Submits all the tasks to the executor and returns the futures. This only exists because the {@code invokeAll} * method of {@code ListeningExecutorService} returns {@code Future} and not {@code ListenableFuture}. * * @param tasks the tasks to execute * @return futures representing the pending results of the tasks */ private List<ListenableFuture<TaskResult>> invokeTasks(List<Task> tasks) { List<ListenableFuture<TaskResult>> results = new ArrayList<>(tasks.size()); for (Task task : tasks) { results.add(_executor.submit(task)); } return results; } /** * Returns the {@link FunctionModel} of the function used to calculate the value in a column. * @param columnName the name of the column * @param inputType type of input (i.e. the security, trade or position type) for the row * @return the function model or null if there isn't one for the specified input type * @throws IllegalArgumentException if the column name isn't found */ public FunctionModel getFunctionModel(String columnName, Class<?> inputType) { return _graphModel.getFunctionModel(columnName, inputType); } /** * Returns the {@link FunctionModel} of the function used to calculate a non-portfolio output. * @param outputName The name of the output * @return the function model * @throws IllegalArgumentException if the output name isn't found */ public FunctionModel getFunctionModel(String outputName) { return _graphModel.getFunctionModel(outputName); } private List<Task> portfolioTasks(CalculationArguments calculationArguments, MarketDataEnvironment marketDataEnvironment, List<?> inputs, Graph graph, ScenarioDefinition scenarioDefinition, ThreadLocalWrapper threadLocalWrapper) { // create tasks for the portfolio outputs int colIndex = 0; List<Task> portfolioTasks = Lists.newArrayList(); for (ViewColumn column : _viewConfig.getColumns()) { FilteredScenarioDefinition filteredDef = scenarioDefinition.filter(column.getName()); Environment env = new EngineEnvironment(valuationTime(calculationArguments, marketDataEnvironment), marketDataEnvironment.toBundle(), _cacheInvalidator); Environment columnEnv = env.withScenarioDefinition(filteredDef); Map<Class<?>, InvokableFunction> functions = graph.getFunctionsForColumn(column.getName()); int rowIndex = 0; for (Object input : inputs) { // the function that is determined from the input InvokableFunction function; // the input to the function that is determined, which can be a security when the input is a position or trade Object functionInput; // try the type of the input InvokableFunction inputFunction = functions.get(input.getClass()); if (inputFunction != null) { function = inputFunction; functionInput = input; } else if (input instanceof PositionOrTrade) { // extract the security from the position or trade try { Security security = ((PositionOrTrade) input).getSecurity(); if (security == null) { function = new InvalidInputFunction( "Position or trade does not contain a security, column: " + column + " type: " + input.getClass().getName()); functionInput = input; } else { function = functions.get(security.getClass()); if (function == null) { function = new InvalidInputFunction( "No function found for security, column: " + column + " type: " + input.getClass().getName()); } functionInput = security; } } catch (AuthorizationException ex) { function = new PermissionDeniedFunction(ex.getMessage()); functionInput = input; } } else { // input is not known by the configuration function = new InvalidInputFunction( "No function found for input, column: " + column + " type: " + input.getClass().getName()); functionInput = input; } Tracer tracer = Tracer.create(calculationArguments.traceType(rowIndex, colIndex)); FunctionModelConfig columnConfig = column.getFunctionConfig(functionInput.getClass()); FunctionModelConfig functionModelConfig = columnConfig.mergedWith(_viewConfig.getDefaultConfig(), _systemDefaultConfig); Class<?> implType = function.getUnderlyingReceiver().getClass(); Class<?> declaringType = function.getDeclaringClass(); Map<Class<?>, FunctionArguments> functionArguments = calculationArguments.getFunctionArguments(); FunctionArguments args = functionArguments(functionArguments, implType, declaringType, functionModelConfig); portfolioTasks.add(new PortfolioTask(columnEnv, functionInput, args, rowIndex++, colIndex, function, tracer, threadLocalWrapper)); } colIndex++; } return portfolioTasks; } // create tasks for the non-portfolio outputs private List<Task> nonPortfolioTasks(CalculationArguments calculationArguments, MarketDataEnvironment marketDataEnvironment, Graph graph, ScenarioDefinition scenarioDefinition, ThreadLocalWrapper cache) { List<Task> tasks = Lists.newArrayList(); for (NonPortfolioOutput output : _viewConfig.getNonPortfolioOutputs()) { InvokableFunction function = graph.getNonPortfolioFunction(output.getName()); Tracer tracer = Tracer.create(calculationArguments.traceType(output.getName())); FunctionModelConfig outputConfig = output.getOutput().getFunctionModelConfig(); FunctionModelConfig functionModelConfig = outputConfig.mergedWith(_viewConfig.getDefaultConfig(), _systemDefaultConfig); Class<?> implType = function.getUnderlyingReceiver().getClass(); Class<?> declaringType = function.getDeclaringClass(); Map<Class<?>, FunctionArguments> functionArguments = calculationArguments.getFunctionArguments(); FunctionArguments args = functionArguments(functionArguments, implType, declaringType, functionModelConfig); // create an environment with scenario arguments filtered for the output FilteredScenarioDefinition filteredDef = scenarioDefinition.filter(output.getName()); Environment env = new EngineEnvironment(valuationTime(calculationArguments, marketDataEnvironment), marketDataEnvironment.toBundle(), _cacheInvalidator); Environment outputEnv = env.withScenarioDefinition(filteredDef); tasks.add(new NonPortfolioTask(outputEnv, args, output.getName(), function, tracer, cache)); } return tasks; } private static List<String> columnNames(ViewConfig viewConfig) { List<String> columnNames = Lists.newArrayListWithCapacity(viewConfig.getColumns().size()); for (ViewColumn column : viewConfig.getColumns()) { String columnName = column.getName(); columnNames.add(columnName); } return columnNames; } /** * Returns the function arguments created by merging the arguments in the map with the arguments in the * function model configuration. Arguments are merged in the following order * <ol> * <li>Function implementation type arguments from the map</li> * <li>Function declaration type arguments from the map</li> * <li>Function implementation type arguments from the configuration</li> * <li>Function declaration type arguments from the configuration</li> * </ol> * * @param argMap function arguments keyed by function type * @param implType the type of the function implementation class * @param declType the type of the function interface * @param config the function configuration * @return the function arguments created by merging the arguments in the map with the arguments in the * function model configuration */ private static FunctionArguments functionArguments(Map<Class<?>, FunctionArguments> argMap, Class<?> implType, Class<?> declType, FunctionModelConfig config) { FunctionArguments implArgs = argMap.containsKey(implType) ? argMap.get(implType) : FunctionArguments.EMPTY; FunctionArguments declArgs = argMap.containsKey(declType) ? argMap.get(declType) : FunctionArguments.EMPTY; return implArgs.mergedWith(declArgs, config.getFunctionArguments(implType), config.getFunctionArguments(declType)); } private static ZonedDateTime valuationTime(CalculationArguments calcArgs, MarketDataEnvironment marketData) { if (calcArgs.getValuationTime() != null) { return calcArgs.getValuationTime(); } else { return marketData.getValuationTime(); } } //---------------------------------------------------------- private interface TaskResult { void addToResults(ResultBuilder resultBuilder); } //---------------------------------------------------------- private abstract static class Task implements Callable<TaskResult> { private final Environment _env; private final Object _input; private final InvokableFunction _invokableFunction; private final Tracer _tracer; private final FunctionArguments _args; private final ThreadLocalWrapper _threadLocalWrapper; private Task(Environment env, @Nullable Object input, FunctionArguments args, InvokableFunction invokableFunction, Tracer tracer, ThreadLocalWrapper threadLocalWrapper) { _env = env; _input = input; _args = args; _invokableFunction = invokableFunction; _tracer = tracer; _threadLocalWrapper = threadLocalWrapper; } @Override public TaskResult call() throws Exception { TracingProxy.start(_tracer); Result<?> result = invokeFunction(); CallGraph callGraph = TracingProxy.end(); return createResult(result, callGraph); } private Result<?> invokeFunction() { AnalyticsEnvironment previousEnvironment = AnalyticsEnvironment.getInstance(); //allow any exception in bindThread to be propagated since //this would indicate something very fundamental has gone //wrong in the environment. _threadLocalWrapper.bindToThread(); try { Object retVal = _invokableFunction.invoke(_env, _input, _args); return retVal instanceof Result ? (Result<?>) retVal : Result.success(retVal); } catch (RuntimeException e) { s_logger.warn("Failed to execute function", e); return Result.failure(e); } finally { //note: only restore thread if it was successfully bound above. //again, exceptions are allowed to propagate. _threadLocalWrapper.restoreThread(previousEnvironment); } } protected abstract TaskResult createResult(Result<?> result, CallGraph callGraph); } //---------------------------------------------------------- private static final class PortfolioTask extends Task { private final int _rowIndex; private final int _columnIndex; private PortfolioTask(Environment env, Object input, FunctionArguments args, int rowIndex, int columnIndex, InvokableFunction invokableFunction, Tracer tracer, ThreadLocalWrapper threadLocalWrapper) { super(env, input, args, invokableFunction, tracer, threadLocalWrapper); _rowIndex = rowIndex; _columnIndex = columnIndex; } @Override protected TaskResult createResult(final Result<?> result, final CallGraph callGraph) { return new TaskResult() { @Override public void addToResults(ResultBuilder resultBuilder) { resultBuilder.add(_rowIndex, _columnIndex, result, callGraph); } }; } } //---------------------------------------------------------- private static final class NonPortfolioTask extends Task { private final String _outputValueName; private NonPortfolioTask(Environment env, FunctionArguments args, String outputValueName, InvokableFunction invokableFunction, Tracer tracer, ThreadLocalWrapper threadLocalWrapper) { super(env, null, args, invokableFunction, tracer, threadLocalWrapper); _outputValueName = ArgumentChecker.notEmpty(outputValueName, "outputValueName"); } @Override protected TaskResult createResult(final Result<?> result, final CallGraph callGraph) { return new TaskResult() { @Override public void addToResults(ResultBuilder resultBuilder) { resultBuilder.add(_outputValueName, result, callGraph); } }; } } /** * Wrapper around state that needs to be bound to a thread before the calculations are performed * and cleared when the calculations are complete. * <p> * When {@link #bindToThread()} is called the values are bound to the current thread and when {@link #restoreThread()} * is called they are removed. * <p> * The values need to be bound to the thread which will execute the functions, which is likely to be a * thread from the pool and not the one that initializes the view. The intention is that * tasks which execute functions should bind the values in a try-finally block and execute the * function in the body of the block. */ private static final class ThreadLocalWrapper { private final ServiceContext _cycleServiceContext; private final ServiceContext _originalServiceContext; private final ThreadLocal<Cache<Object, Object>> _cacheThreadLocal; private final Cache<Object, Object> _cache; private final AnalyticsEnvironment _targetEnvironment; private final boolean _requirementsGathering; private ThreadLocalWrapper(ServiceContext cycleServiceContext, ServiceContext originalServiceContext, Cache<Object, Object> cache, ThreadLocal<Cache<Object, Object>> cacheThreadLocal, AnalyticsEnvironment analyticsEnvironment, boolean requirementsGathering) { _cycleServiceContext = cycleServiceContext; _originalServiceContext = originalServiceContext; _cache = cache; _cacheThreadLocal = cacheThreadLocal; _targetEnvironment = analyticsEnvironment; _requirementsGathering = requirementsGathering; } /** * Binds the thread local state to the current thread. * * @return this, so it can be used in a try-finally block */ private void bindToThread() { _cacheThreadLocal.set(_cache); ThreadLocalServiceContext.init(_cycleServiceContext); AnalyticsEnvironment.setInstance(_targetEnvironment); if (_requirementsGathering) { Failure.turnOffStackTraces(); } } /** * Removes the thread local bindings. */ public void restoreThread(AnalyticsEnvironment previousEnvironment) { _cacheThreadLocal.remove(); ThreadLocalServiceContext.init(_originalServiceContext); AnalyticsEnvironment.setInstance(previousEnvironment); if (_requirementsGathering) { Failure.turnOnStackTraces(); } } } }