/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * Created: Feb 23, 2007 * By: Robin Salkeld * --!> */ package org.openquark.cal.services; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.openquark.cal.compiler.CompilerMessage; import org.openquark.cal.compiler.CompilerMessageLogger; import org.openquark.cal.compiler.MessageLogger; import org.openquark.cal.compiler.ModuleName; import org.openquark.cal.compiler.io.EntryPoint; import org.openquark.cal.compiler.io.EntryPointSpec; import org.openquark.cal.compiler.io.InputPolicy; import org.openquark.cal.compiler.io.OutputPolicy; import org.openquark.cal.machine.CALExecutor; import org.openquark.cal.runtime.CALExecutorException; import org.openquark.cal.runtime.ExecutionContext; import org.openquark.util.General; /** * This is a helper class that manages a cache of {@link EntryPoint}s for a * particular workspace. It provides runFunction() methods that accept an * {@link EntryPointSpec} along with a set of arguments and execute the * application function of the specified function to those arguments, using the * {@link InputPolicy InputPolicies} and {@link OutputPolicy OutputPolicies} * specified. This is implemented by dynamically obtaining and recreating * {@code EntryPoints} as they are needed and holding them in a cache for future * function calls to use. This will often be more efficient than * BasicCalServices.runFunction() since repeated calls to the same functions * with different arguments is a common usage pattern. * * @author Robin Salkeld, Magnus Byne */ final class EntryPointCache { /** The program model manager this class caches entry points for. */ final ProgramModelManager programModelManager; /** this is the size of the module cache - it limits the number of entry * points that will be cached for any one module. */ private final int numEntryPointsToCachePerModule; /** * This is used to map from ModuleName to a module caches. * A LRU cache is used, so that we only cache entry points in a fixed * number of modules. Once this number is exceeded, all the entry points for * the first module referenced are removed. */ final private LRUCache<ModuleName, ModuleCache> cache; /** * this lock is used to protect the cache */ final private ReadWriteLock cacheLock = new ReentrantReadWriteLock(); /** * this is used for the caching - * the least recently added items are removed from the cache * when the max size is exceeded * * @author Magnus Byne */ private static class LRUCache<K,V> extends java.util.LinkedHashMap<K,V> { private static final long serialVersionUID = -2300095914974540766L; /** this is the maximum number of items the map will hold*/ protected int cacheSize; /** * Create the cache * @param cacheSize the number of items to store */ LRUCache(int cacheSize) { this.cacheSize = cacheSize; } @Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return size() >= cacheSize; } } /** * This class is used to cache entry points for a single module * @author mbyne */ private final class ModuleCache { /** the cache of entrypointspec -> entrypoint*/ final private LRUCache<EntryPointSpec, EntryPoint> cache; /** the name of the module that entry points are cached for*/ final private ModuleName moduleName; /**lock used to gate access to the cache*/ final private ReadWriteLock cacheLock = new ReentrantReadWriteLock(); ModuleCache(ModuleName name, int size) { moduleName = name; cache = new LRUCache<EntryPointSpec, EntryPoint>(size); } /** * runs a function. If the entryPointSpec is not already in the cache it is * first added. * @param entryPointSpec describes the function to run * @param arguments the arguments for running the function * @param executionContext the execution context to use * @return the result of running the function * @throws CALExecutorException * @throws GemCompilationException */ Object runFunction(EntryPointSpec entryPointSpec, Object[] arguments, ExecutionContext executionContext) throws CALExecutorException, GemCompilationException { cacheLock.readLock().lock(); try { EntryPoint ep = cache.get(entryPointSpec); if (ep == null) { addEntryPoints(Collections.singletonList(entryPointSpec), true); ep = cache.get(entryPointSpec); } CALExecutor executor = programModelManager.makeExecutor(executionContext); return executor.exec(ep, arguments); } finally { cacheLock.readLock().unlock(); } } /** * Add a list of entrypointspecs to the module cache * assumes that a read lock is already held on the cache * @param entryPointSpecs */ void addEntryPoints(Collection<EntryPointSpec> entryPointSpecs) throws GemCompilationException { addEntryPoints(entryPointSpecs, false); } /** * Add a list of entrypointspecs to the module cache * assumes that a read lock is already held on the cache * @param entryPointSpecs * @param haveRead - this param indicates if we already have a read lock on the cache */ private void addEntryPoints(Collection<EntryPointSpec> entryPointSpecs, boolean haveRead) throws GemCompilationException { //upgrade to a write lock if (haveRead) { cacheLock.readLock().unlock(); } cacheLock.writeLock().lock(); try { HashSet<EntryPointSpec> required = new HashSet<EntryPointSpec>(); required.addAll(cache.keySet()); required.addAll(entryPointSpecs); CompilerMessageLogger compilerLogger = new MessageLogger(); List<EntryPointSpec> requiredList = new ArrayList<EntryPointSpec>(required); //compile the entry points List<EntryPoint> entryPoints = programModelManager.getCompiler().getEntryPoints(requiredList, moduleName, compilerLogger); if (compilerLogger.getMaxSeverity().compareTo(CompilerMessage.Severity.ERROR) >= 0) { StringBuffer sb = new StringBuffer(); sb.append("Failed to generate entry points for module '"); sb.append(moduleName); sb.append(General.SYSTEM_EOL); for(CompilerMessage msg : compilerLogger.getCompilerMessages()) { sb.append(msg); sb.append(General.SYSTEM_EOL); } //all of the cached entry points are invalid now. cache.clear(); throw new GemCompilationException(sb.toString()); } //update the cache with the new entry points // entry point spec for (int i = 0, n = entryPoints.size(); i < n; i++) { EntryPointSpec entryPointSpec = requiredList.get(i); EntryPoint entryPoint = entryPoints.get(i); if (entryPoint != null) { cache.put(entryPointSpec, entryPoint); } else { cache.remove(entryPointSpec); } } } finally { if (haveRead) { cacheLock.readLock().lock(); } cacheLock.writeLock().unlock(); } } } Object runFunction(EntryPointSpec entryPointSpec, Object[] arguments, ExecutionContext executionContext) throws CALExecutorException, GemCompilationException { cacheLock.readLock().lock(); try { ModuleName moduleName = entryPointSpec.getFunctionalAgentName().getModuleName(); ModuleCache moduleCache = getModuleCache(moduleName); return moduleCache.runFunction(entryPointSpec, arguments, executionContext); } finally { cacheLock.readLock().unlock(); } } /** * gets the module cache for the specified module. It assumes there is already * a read lock on the module cache. If there is no entry for the module * it is created. * @param moduleName * @return the module cache */ private ModuleCache getModuleCache(ModuleName moduleName) { ModuleCache moduleCache = cache.get(moduleName); if (moduleCache != null) { return moduleCache; } cacheLock.readLock().unlock(); cacheLock.writeLock().lock(); try { //make sure it hasn't been inserted by someone while the lock was being upgraded moduleCache = cache.get(moduleName); if (moduleCache != null) { return moduleCache; } moduleCache = new ModuleCache(moduleName, numEntryPointsToCachePerModule); cache.put(moduleName, moduleCache); return moduleCache; } finally { cacheLock.readLock().lock(); cacheLock.writeLock().unlock(); } } /** * Pre-caches entry points - can be used to cut down initial time taken to run functions. * @param entryPointSpecs * @throws GemCompilationException */ void cacheEntryPoints(Collection<EntryPointSpec> entryPointSpecs) throws GemCompilationException { //split the entry points into sets of each module HashMap<ModuleName, HashSet<EntryPointSpec> > specs = new HashMap<ModuleName, HashSet<EntryPointSpec> >(); for(EntryPointSpec spec : entryPointSpecs) { ModuleName module = spec.getFunctionalAgentName().getModuleName(); Set<EntryPointSpec> moduleSet = specs.get(module); if (moduleSet == null) { HashSet<EntryPointSpec> set = new HashSet<EntryPointSpec>(); set.add(spec); specs.put(module, set); } else { moduleSet.add(spec); } } //update each of the module caches cacheLock.readLock().lock(); try { for(ModuleName moduleName : specs.keySet()) { ModuleCache moduleCache = getModuleCache(moduleName); moduleCache.addEntryPoints(specs.get(moduleName)); } } finally { cacheLock.readLock().unlock(); } } /** * EntryPointCache constructor. * @param programModelManager */ EntryPointCache(ProgramModelManager programModelManager, int numEntryPointsToCachePerModule, int numModulesToCache) { this.programModelManager = programModelManager; this.numEntryPointsToCachePerModule = numEntryPointsToCachePerModule; cache = new LRUCache<ModuleName, ModuleCache>(numModulesToCache); } /** lock the module cache */ void lockCache() { cacheLock.writeLock().lock(); } /** unlock the module cache */ void unlockCache() { cacheLock.writeLock().unlock(); } /** * remove everything from the cache * must do this whenever any modules are recompiled, as they could affect any other module. * Must have acquired the module lock first. */ void flush() { cache.clear(); } }