package com.voxeo.tropo.app; import java.io.IOException; import java.io.Serializable; import java.lang.management.ThreadInfo; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import javax.script.Compilable; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.servlet.ServletContext; import org.apache.log4j.Logger; import com.voxeo.tropo.Configuration; import com.voxeo.tropo.ScriptSecurityManager; import com.voxeo.tropo.ServletContextConstants; import com.voxeo.tropo.util.ScriptThreadPoolExecutor; import com.voxeo.tropo.util.Utils; public abstract class AbstractLocalApplicationManager extends AbstractApplicationManager implements LocalApplicationManager, LocalApplicationMonitor, Runnable { private static final Logger LOG = Logger.getLogger(AbstractLocalApplicationManager.class); protected static ScriptEngineManager SCRIPT_MGR = null; protected static ScriptSecurityManager SECURITY_MGR = null; protected ScriptEnginePool _engPool; protected static Map<String, String> CANONICAL_TYPES = new HashMap<String, String>(); class TInfo { long _lastCpuTime = 0; long _lastTime = System.currentTimeMillis(); int _tags = 0; float _percentage = 0; @Override public String toString() { return "TInfo[" + _percentage + ", " + _tags + "]"; } } protected int _loop; protected Object _loopLock = new Object(); protected Map<Long, TInfo> _tinfos; protected Map<Long, Thread> _threads; protected ExecutorService _pool = new ScriptThreadPoolExecutor(Configuration.get().getThreadSize() / 4, Configuration .get().getThreadSize(), new ThreadFactory() { public Thread newThread(final Runnable r) { final Thread t = new Thread(new ThreadGroup("Scripts"), r, "Script", 0); if (t.isDaemon()) { t.setDaemon(true); } if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } _threads.put(t.getId(), t); return t; } }); static { CANONICAL_TYPES.put("javascript", "js"); CANONICAL_TYPES.put("Javascript", "js"); CANONICAL_TYPES.put("js", "js"); CANONICAL_TYPES.put("es", "js"); CANONICAL_TYPES.put("groovy", "groovy"); CANONICAL_TYPES.put("Groovy", "groovy"); CANONICAL_TYPES.put("Jython", "jython"); CANONICAL_TYPES.put("jython", "jython"); CANONICAL_TYPES.put("py", "jython"); CANONICAL_TYPES.put("Py", "jython"); CANONICAL_TYPES.put("Jy", "jython"); CANONICAL_TYPES.put("jy", "jython"); CANONICAL_TYPES.put("php", "php"); } public static String getCanonicalType(final String type) { return CANONICAL_TYPES.get(type); } public void dispose() { super.dispose(); setLoopDectionTime(-1); _pool.shutdown(); } public void init(final ServletContext context, final Map<String, String> paras) { super.init(context, paras); ScriptEngineManager engMgr = (ScriptEngineManager) context.getAttribute(ServletContextConstants.ENGINE_MANAGER); _engPool = new ScriptEnginePool(engMgr, Configuration.get().getEnginePoolSizes()); CANONICAL_TYPES.putAll(paras); _tinfos = new HashMap<Long, TInfo>(); _threads = new ConcurrentHashMap<Long, Thread>(); _loop = 2; new Thread(this, "ApplicationMonitor").start(); } public ScriptEnginePool getScriptEnginePool() { return _engPool; } public ScriptEngine getScriptEngine(final String type) { return _engPool.get(type); } public void putScriptEngine(final String type, ScriptEngine eng) { if (eng == null) { return; } _engPool.put(type, eng); } public void execute(final ApplicationInstance inst) { _pool.execute(inst); } public ApplicationURL createURL(final String url, final String method) throws MalformedURLException, IOException { return new JavaURL(new URL(url), method); } protected Application createApplication(final String name, final String type, final int account, final String app, final Properties params) throws MalformedURLException, IOException, InvalidApplicationException { final ApplicationURL url = createURL(name, "GET"); return new SimpleApplication(this, url, type, account, app, params); } protected Application createApplication(final String url, final int accountId, final String app, final Properties params) throws InvalidApplicationException { Utils.setLogContext(String.valueOf(accountId), "-1", "-1", "-1"); final String[] urlAndType = getURLandTypeBasedOnExtension(url); try { return createApplication(urlAndType[0], urlAndType[1], accountId, app, params); } catch (final Exception e) { LOG.error("Unable to create application: " + e.getMessage(), e); throw new InvalidApplicationException(e); } } // simple util method protected static String[] getURLandTypeBasedOnExtension(final String url) throws InvalidApplicationException { final String name = url.substring(url.lastIndexOf('/') + 1); final String[] splits = name.split("[.]"); if (splits.length < 2) { throw new InvalidApplicationException("Unable to find application type based extension: " + name); } final String type = getCanonicalType(splits[1]); if (type == null) { throw new InvalidApplicationException("Unable to support application type: " + splits[1]); } splits[0] = url; splits[1] = type; if (LOG.isDebugEnabled()) { LOG.debug("Trying to create application for URL=" + splits[0] + " and type=" + splits[1]); } return splits; } public static synchronized void initialization(final ServletContext ctx) throws InstantiationException, IllegalAccessException { if (SCRIPT_MGR == null) { Utils.pythonHome(); Utils.rubyHome(); Utils.getAppDir(); // initialize script engine manager SCRIPT_MGR = new ScriptEngineManager(); ctx.setAttribute(ServletContextConstants.ENGINE_MANAGER, SCRIPT_MGR); LOG.info("Initializing Script Engine Manager [" + SCRIPT_MGR + "] ..."); for (final ScriptEngineFactory f : SCRIPT_MGR.getEngineFactories()) { final ScriptEngine e = f.getScriptEngine(); final StringBuffer log = new StringBuffer("Engine=" + f.getEngineName() + "[" + f.getEngineVersion() + "], lang=" + f.getLanguageName() + "[" + f.getLanguageVersion() + "], alias=" + f.getNames()); log.append(e instanceof Compilable ? ", compilable=true" : ", compiable=false"); log.append(e instanceof Invocable ? ", invocable=true" : ", invocable=false"); log.append(e instanceof Serializable ? ", serializable=true" : ", serializable=false."); LOG.info(log.toString()); } } if (SECURITY_MGR == null && Configuration.get().isEnableSecurityManager()) { SECURITY_MGR = new ScriptSecurityManager(ctx); System.setSecurityManager(SECURITY_MGR); } AbstractApplicationManager.initialization(ctx); } public void run() { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); while (_tmx.isThreadCpuTimeEnabled()) { if (getLoopDectionTime() > 0) { try { loopDection(); } catch (final Throwable t) { LOG.error(t.toString(), t); } } synchronized (_loopLock) { try { final long time = getLoopDectionTime() * 60000L; if (time > 0) { _loopLock.wait(time); } else { _loopLock.wait(); } } catch (final InterruptedException e) { ; } } try { for (final Iterator<Long> i = _tinfos.keySet().iterator(); i.hasNext();) { final long id = i.next(); if (_tmx.getThreadInfo(id) == null) { i.remove(); } } } catch (final Throwable t) { LOG.error(t.toString(), t); } } } void loopDection() { LOG.info("Starts loop detection."); int active = 0; for (final long id : _tmx.getAllThreadIds()) { final ThreadInfo info = _tmx.getThreadInfo(id); if (info == null) { continue; } if (!"Script".equals(info.getThreadName())) { continue; } if (Thread.State.RUNNABLE != info.getThreadState()) { continue; } active++; TInfo ti = _tinfos.get(id); if (ti != null) { final long nowCpuTime = _tmx.getThreadCpuTime(id); if (nowCpuTime == -1) { _tinfos.remove(id); continue; } final long cpu = nowCpuTime - ti._lastCpuTime; final long now = System.nanoTime(); final long time = now - ti._lastTime; ti._lastCpuTime = nowCpuTime; ti._lastTime = now; ti._percentage = (float) cpu / (float) time; } else { ti = new TInfo(); ti._lastCpuTime = _tmx.getThreadCpuTime(id); if (ti._lastCpuTime == -1) { continue; } ti._lastTime = System.nanoTime(); _tinfos.put(id, ti); } } final float threshold = (float) (active > 50 ? 0.25 : active > 10 ? 0.5 : 0.75); for (final long id : _tinfos.keySet()) { final TInfo ti = _tinfos.get(id); final Thread t = _threads.get(id); if (t == null || ti == null) { continue; } LOG.info("Thread[" + id + "] cpu info :" + ti); if (ti._percentage > threshold) { ti._tags++; if (ti._tags > 2) { LOG.error("*WARNING* Thread[" + id + "] has taken most of CPU time out of " + active + " script threads and will be set with the lowest priority."); t.setPriority(Thread.MIN_PRIORITY); } } else { ti._tags = 0; t.setPriority(Thread.NORM_PRIORITY); } } LOG.info("Ends loop detection."); } public void setLoopDectionTime(final int x) { synchronized (_loopLock) { _loop = x; _loopLock.notifyAll(); } } public int getLoopDectionTime() { synchronized (_loopLock) { return _loop; } } }