/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.service.routines; import com.foundationdb.ais.model.AkibanInformationSchema; import com.foundationdb.ais.model.Routine; import com.foundationdb.ais.model.TableName; import com.foundationdb.server.error.ExternalRoutineInvocationException; import com.foundationdb.server.error.NoSuchRoutineException; import com.foundationdb.server.service.dxl.DXLService; import com.foundationdb.server.service.session.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.script.*; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.Map; public class ScriptCache { private final DXLService dxlService; private final Map<TableName, CacheEntry> cache = new HashMap<>(); // Script engine discovery can be fairly expensive, so it is deferred. private final ScriptEngineManagerProvider engineProvider; private final static Logger logger = LoggerFactory.getLogger(ScriptCache.class); public ScriptCache(DXLService dxlService, ScriptEngineManagerProvider engineProvider) { this.dxlService = dxlService; this.engineProvider = engineProvider; } public synchronized void clear() { cache.clear(); } public synchronized void checkRemoveRoutine(TableName routineName, long currentVersion) { CacheEntry entry = cache.remove(routineName); if ((entry != null) && (entry.version == currentVersion)) { cache.put(routineName, entry); // Was valid after all. } } public boolean isScriptLanguage(Session session, String language) { return (getManager(session).getEngineByName(language) != null); } public ScriptPool<ScriptEvaluator> getScriptEvaluator(Session session, TableName routineName, long[] ret_aisGeneration) { return getEntry(session, routineName, ret_aisGeneration).getScriptEvaluator(); } public ScriptPool<ScriptLibrary> getScriptLibrary(Session session, TableName routineName, long[] ret_aisGeneration) { return getEntry(session, routineName, ret_aisGeneration).getScriptLibrary(); } public ScriptPool<ScriptInvoker> getScriptInvoker(Session session, TableName routineName, long[] ret_aisGeneration) { return getEntry(session, routineName, ret_aisGeneration).getScriptInvoker(this, session); } protected ScriptEngineManager getManager(Session session) { return engineProvider.getManager(); } protected synchronized CacheEntry getEntry(Session session, TableName routineName, long[] ret_aisGeneration) { AkibanInformationSchema ais = dxlService.ddlFunctions().getAIS(session); Routine routine = ais.getRoutine(routineName); if (null == routine) throw new NoSuchRoutineException(routineName); if (ret_aisGeneration != null) ret_aisGeneration[0] = ais.getGeneration(); long currentVersion = routine.getVersion(); CacheEntry entry = cache.get(routineName); if ((entry != null) && (entry.version == currentVersion)) return entry; ClassLoader origCL = getContextClassLoader(); if (!routine.isSystemRoutine()) { setContextClassLoader(engineProvider.getSafeClassLoader()); } try { ScriptEngine engine = getManager(session).getEngineByName(routine.getLanguage()); if (engine == null) throw new ExternalRoutineInvocationException(routineName, "Cannot find " + routine.getLanguage() + " script engine"); entry = new CacheEntry(routine, engine); cache.put(routineName, entry); } finally { if (!routine.isSystemRoutine()) { setContextClassLoader(origCL); } } return entry; } private ClassLoader getContextClassLoader() { return AccessController.doPrivileged( new PrivilegedAction<ClassLoader>() { public ClassLoader run() { return Thread.currentThread().getContextClassLoader(); } } ); } private void setContextClassLoader(final ClassLoader cl) { AccessController.doPrivileged( new PrivilegedAction<Void>() { public Void run() { Thread.currentThread().setContextClassLoader(cl); return null; } } ); } class CacheEntry { private TableName routineName; private long version; private String script; private TableName libraryName; private String function; private ScriptEngineFactory factory; private String threading; private boolean invocable, compilable; private ScriptPool<ScriptEvaluator> sharedEvaluatorPool; private ScriptPool<ScriptLibrary> sharedLibraryPool; private ScriptEngine spareEngine; public CacheEntry(Routine routine, ScriptEngine engine) { routineName = routine.getName(); version = routine.getVersion(); script = routine.getDefinition(); libraryName = routineName; // TODO: Until qualified EXTERNAL NAME supported. function = routine.getMethodName(); factory = engine.getFactory(); threading = (String)factory.getParameter("THREADING"); invocable = (engine instanceof Invocable); compilable = (engine instanceof Compilable); spareEngine = engine; } public ScriptPool<ScriptEvaluator> getScriptEvaluator() { // The only case where anything more than the factory is cached is // for THREAD-ISOLATED / STATELESS threading + Compilable, // which means that just one CompiledScript will work for // everyone. if (compilable && ("THREAD-ISOLATED".equals(threading) || "STATELESS".equals(threading))) { synchronized (this) { if (sharedEvaluatorPool == null) { ScriptEngine engine = spareEngine; if (engine != null) spareEngine = null; else engine = factory.getScriptEngine(); CompiledEvaluator compiled = new CompiledEvaluator(routineName, engine, script, true); sharedEvaluatorPool = new SharedPool<ScriptEvaluator>(compiled); } return sharedEvaluatorPool; } } // Otherwise, every caller gets a new pool which only has // the scope of the prepared statement, etc. ScriptEngine engine; synchronized (this) { engine = spareEngine; if (engine != null) spareEngine = null; } if (compilable) { CompiledEvaluator compiled = null; if (engine != null) compiled = new CompiledEvaluator(routineName, engine, script, false); return new CompiledEvaluatorPool(routineName, factory, script, compiled); } else { EngineEvaluator evaluator = null; if (engine != null) evaluator = new EngineEvaluator(routineName, engine, script); return new EngineEvaluatorPool(routineName, factory, script, evaluator); } } public ScriptPool<ScriptLibrary> getScriptLibrary() { assert invocable; // Can share if at multi-threaded (or stronger), since we // are invoking the function. if ("MULTITHREADED".equals(threading) || "THREAD-ISOLATED".equals(threading) || "STATELESS".equals(threading)) { synchronized (this) { if (sharedLibraryPool == null) { ScriptEngine engine = spareEngine; if (engine != null) spareEngine = null; else engine = factory.getScriptEngine(); ScriptLibrary library = new Library(routineName, engine, script); sharedLibraryPool = new SharedPool<>(library); } return sharedLibraryPool; } } // Otherwise, every caller gets a new pool which only has // the scope of the prepared statement, etc. ScriptEngine engine; synchronized (this) { engine = spareEngine; if (engine != null) spareEngine = null; } Library library = null; if (engine != null) library = new Library(routineName, engine, script); return new LibraryPool(routineName, factory, script, library); } public ScriptPool<ScriptInvoker> getScriptInvoker(ScriptCache cache, Session session) { assert invocable && (function != null); ScriptPool<ScriptLibrary> libraryPool; if (routineName.equals(libraryName)) { libraryPool = getScriptLibrary(); } else { synchronized (this) { spareEngine = null; } libraryPool = cache.getScriptLibrary(session, libraryName, null); } return new InvokerPool(libraryPool, function); } } static class SharedPool<T> implements ScriptPool<T> { private final T instance; public SharedPool(T instance) { this.instance = instance; } @Override public T get() { return instance; } @Override public void put(T elem, boolean success) { } } // Relatively conservative, since these could be big. static final int FIXED_SIZE = 8; static abstract class FixedPool<T> implements ScriptPool<T> { private final Deque<T> pool = new ArrayDeque<>(FIXED_SIZE); public FixedPool(T initial) { if (initial != null) pool.addLast(initial); } protected abstract T create(); @Override public T get() { T elem; synchronized (pool) { elem = pool.pollFirst(); } if (elem != null) return elem; else return create(); } @Override public void put(T elem, boolean success) { if (success) { synchronized (pool) { pool.offerLast(elem); } } } } static abstract class BasePool<T> extends FixedPool<T> { protected final TableName routineName; protected final ScriptEngineFactory factory; protected final String script; public BasePool(TableName routineName, ScriptEngineFactory factory, String script, T initial) { super(initial); this.routineName = routineName; this.factory = factory; this.script = script; } } static class EngineEvaluatorPool extends BasePool<ScriptEvaluator> { public EngineEvaluatorPool(TableName routineName, ScriptEngineFactory factory, String script, EngineEvaluator initial) { super(routineName, factory, script, initial); } @Override protected EngineEvaluator create() { return new EngineEvaluator(routineName, factory.getScriptEngine(), script); } } static class CompiledEvaluatorPool extends BasePool<ScriptEvaluator> { public CompiledEvaluatorPool(TableName routineName, ScriptEngineFactory factory, String script, CompiledEvaluator initial) { super(routineName, factory, script, initial); } @Override protected CompiledEvaluator create() { return new CompiledEvaluator(routineName, factory.getScriptEngine(), script, false); } } static class LibraryPool extends BasePool<ScriptLibrary> { public LibraryPool(TableName routineName, ScriptEngineFactory factory, String script, Library initial) { super(routineName, factory, script, initial); } @Override protected Library create() { return new Library(routineName, factory.getScriptEngine(), script); } } protected static void setScriptName(TableName routineName, ScriptEngine engine) { engine.getContext().setAttribute(ScriptEngine.FILENAME, routineName.toString(), ScriptContext.ENGINE_SCOPE); } static class EngineEvaluator implements ScriptEvaluator { private final TableName routineName; private final ScriptEngine engine; private final String script; public EngineEvaluator(TableName routineName, ScriptEngine engine, String script) { this.routineName = routineName; this.engine = engine; this.script = script; setScriptName(routineName, engine); } @Override public String getEngineName() { return engine.getFactory().getEngineName(); } @Override public boolean isCompiled() { return false; } @Override public boolean isShared() { return false; } @Override public Bindings getBindings() { return engine.getBindings(ScriptContext.ENGINE_SCOPE); } @Override public Object eval(Bindings bindings) { logger.debug("Evaluating {}", routineName); try { return engine.eval(script); // Bindings came from engine. } catch (ScriptException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } } } static class CompiledEvaluator implements ScriptEvaluator { private final TableName routineName; private final CompiledScript compiled; private final boolean shared; public CompiledEvaluator(TableName routineName, ScriptEngine engine, String script, boolean shared) { this.routineName = routineName; setScriptName(routineName, engine); logger.debug("Compiling {}", routineName); try { compiled = ((Compilable) engine).compile(script); } catch (ScriptException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } this.shared = shared; } @Override public String getEngineName() { return compiled.getEngine().getFactory().getEngineName(); } @Override public boolean isCompiled() { return true; } @Override public boolean isShared() { return shared; } @Override public Bindings getBindings() { // Prefer to use the Bindings already in the engine // instead of a fresh one, since some engines (Jython, for // instance) do not do well with a dynamic set. if (shared) return compiled.getEngine().createBindings(); else return compiled.getEngine().getBindings(ScriptContext.ENGINE_SCOPE); } @Override public Object eval(Bindings bindings) { logger.debug("Loading compiled {}", routineName); try { if (shared) return compiled.eval(bindings); else return compiled.eval(); } catch (ScriptException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } } } static class Library implements ScriptLibrary { private final TableName routineName; private final Invocable invocable; public Library(TableName routineName, ScriptEngine engine, String script) { this.routineName = routineName; setScriptName(routineName, engine); try { if (engine instanceof Compilable) { logger.debug("Compiling and loading {}", routineName); ((Compilable) engine).compile(script).eval(); } else { logger.debug("Evaluating {}", routineName); engine.eval(script); } } catch (ScriptException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } invocable = (Invocable) engine; } @Override public String getEngineName() { return ((ScriptEngine) invocable).getFactory().getEngineName(); } @Override public boolean isCompiled() { return (invocable instanceof Compilable); } @Override public Object invoke(String function, Object[] args) { logger.debug("Calling {} in {}", function, routineName); try { return invocable.invokeFunction(function, args); } catch (ScriptException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } catch (NoSuchMethodException ex) { throw new ExternalRoutineInvocationException(routineName, ex); } } } static class InvokerPool implements ScriptPool<ScriptInvoker> { private final ScriptPool<ScriptLibrary> libraryPool; private final String function; public InvokerPool(ScriptPool<ScriptLibrary> libraryPool, String function) { this.libraryPool = libraryPool; this.function = function; } @Override public ScriptInvoker get() { return new Invoker(libraryPool.get(), function); } @Override public void put(ScriptInvoker elem, boolean success) { libraryPool.put(elem.getLibrary(), success); } } static class Invoker implements ScriptInvoker { private final ScriptLibrary library; private final String function; public Invoker(ScriptLibrary library, String function) { this.library = library; this.function = function; } @Override public ScriptLibrary getLibrary() { return library; } @Override public String getFunctionName() { return function; } @Override public Object invoke(Object[] args) { return library.invoke(function, args); } } }