/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.monitor.script;
import org.helios.apmrouter.collections.ConcurrentLongSlidingWindow;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.apmrouter.util.SystemClock;
import org.helios.apmrouter.util.URLHelper;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;
import javax.script.*;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* <p>Title: ScriptContainer</p>
* <p>Description: Container and invoker for a compiled script </p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.monitor.script.ScriptContainer</code></p>
*/
public class ScriptContainer extends NotificationBroadcasterSupport implements ScriptContainerMBean {
/** The source URL */
protected final URL scriptUrl;
/** The source name */
protected final String name;
/** The script source */
protected volatile String source;
/** The compiled script */
protected CompiledScript compiledScript;
/** The timestamp highwater mark of the compiled script */
protected long sourceTimestamp;
/** Custom invocation frequency determined by a var called <b><code>customFrequency</code></b> that contains the frequency in ms. */
protected long customFrequency = -1L;
/** This script container's script engine/compiler */
protected final ScriptEngine scriptEngine;
/** This script container's script engine factory */
protected final ScriptEngineFactory scriptEngineFactory;
/** The script monitor supplied script bindings */
protected final Bindings globalBindings;
/** The local script bindings */
protected final Bindings localBindings;
/** Indicates if the source URL is writable and modified source can be saved */
protected final boolean writable;
/** Indicates the script is disabled after n consecutive errors */
protected boolean disabled = false;
/** The JMX ObjectName for this container */
protected final ObjectName objectName;
/** The context for this script */
protected final ScriptContext ctx = new SimpleScriptContext();
/** The overriden timestamp of the last change to the source */
protected final AtomicLong lastChangeOverride = new AtomicLong(-1L);
// ========================
// Counters
// ========================
/** The number of consecutive times the script has executed with errors */
protected final AtomicLong consecutiveErrors = new AtomicLong(0L);
/** The total number of times the script has executed with errors */
protected final AtomicLong totalErrors = new AtomicLong(0L);
/** The total number of times the script has executed */
protected final AtomicLong totalInvocations = new AtomicLong(0L);
/** The local collection sweep, starting at 0 and incrementing each period */
protected long localCollectionSweep = 0;
/** The script invocation time recorder */
protected final ConcurrentLongSlidingWindow invocationTimes = new ConcurrentLongSlidingWindow(100);
/** The script compilation time recorder */
protected final ConcurrentLongSlidingWindow compilationTimes = new ConcurrentLongSlidingWindow(20);
/** JMX script helper */
protected static final JMXScriptHelper jmx = new JMXScriptHelper();
/** Source prepended to the read file */
public static final String SRC_HEADER = "if(!inited) pout.println('\\n\\t[<< %s >>] Initializing');\n";
/** Source appended to the read file */
public static final String SRC_FOOTER = "\nif(!inited) { pout.println('\\n\\t[<< %s >>] Monitor OK'); inited = true;}";
/** The binding name for the collection sweep */
public static final String COLLECTION_SWEEP = "localsweep";
/**
* Creates a new ScriptContainer
* @param se This script container's script engine/compiler
* @param globalBindings The monitor supplied script bindings
* @param url The source URL
* @throws ScriptException thrown if the script cannot be compiled
*/
public ScriptContainer(ScriptEngine se, Bindings globalBindings, URL url) throws ScriptException {
scriptEngine = se;
scriptEngineFactory = se.getFactory();
localBindings = se.createBindings();
//localBindings.put("jmx", jmx);
this.globalBindings = globalBindings;
ctx.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);
ctx.setBindings(globalBindings, ScriptContext.GLOBAL_SCOPE);
this.scriptUrl = url;
try {
String cleanedUrl = this.scriptUrl.toURI().toString();
cleanedUrl = cleanedUrl.substring(cleanedUrl.indexOf(":")+1);
cleanedUrl = cleanedUrl.substring(cleanedUrl.indexOf(":")+1);
cleanedUrl = cleanedUrl.replace(':', ';');
System.out.println("Creating ObjectName [" + String.format(OBJECT_NAME_TEMPLATE, cleanedUrl, scriptEngineFactory.getLanguageName()) + "]");
objectName = JMXHelper.objectName(String.format(OBJECT_NAME_TEMPLATE, cleanedUrl, scriptEngineFactory.getLanguageName()));
JMXHelper.getHeliosMBeanServer().registerMBean(this, objectName);
} catch (Exception ex) {
throw new RuntimeException("Failed to register JMX interface for [" + this.scriptUrl + "]", ex);
}
writable = URLHelper.isWritable(this.scriptUrl);
name = urlToName(url);
sourceTimestamp = URLHelper.getLastModified(scriptUrl);
acquireScript();
compileScript();
}
/**
* Generates the script's logical name for the passed URL
* @param url The script's URL
* @return the script's logical name
*/
public static String urlToName(URL url) {
String urlName = url.getFile();
if(urlName.lastIndexOf(".")!=-1) {
return urlName.substring(0, urlName.lastIndexOf("."));
}
return urlName;
}
/**
* Acquires the script source and compiles it.
*/
protected void acquireScript() {
source = URLHelper.getTextFromURL(scriptUrl);
}
/**
* Compiles the acquired source
*/
protected void compileScript() {
try {
SystemClock.startTimer();
compiledScript = ((Compilable)scriptEngine).compile(String.format(SRC_HEADER, name) + source + String.format(SRC_FOOTER, name));
if(localBindings.containsKey("customFrequency")) {
try {
Long f = ((Number)localBindings.get("customFrequency")).longValue();
customFrequency = f;
} catch (Exception e) { /* No Op */ }
}
localBindings.put("inited", false);
localCollectionSweep = 0L;
} catch (Exception e) {
System.err.println("Failed to compile script [" + scriptUrl + "]. Will be ignored until modified.");
e.printStackTrace(System.err);
disabled = true;
} finally {
compilationTimes.insert(SystemClock.endTimer().elapsedMs);
}
}
/**
* Increments the consecutive error count
* @return The new error count
*/
public long incrementErrors() {
return consecutiveErrors.incrementAndGet();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#invoke()
*/
@Override
public Object invoke() throws ScriptException {
checkForUpdate();
localCollectionSweep++;
if(localCollectionSweep==Long.MAX_VALUE) {
localCollectionSweep=0L;
}
if(disabled) return null;
boolean completed = false;
SystemClock.startTimer();
try {
localBindings.put(COLLECTION_SWEEP, localCollectionSweep);
Object response = compiledScript.eval(ctx);
totalInvocations.incrementAndGet();
consecutiveErrors.set(0);
invocationTimes.insert(SystemClock.endTimer().elapsedMs);
completed=true;
return response;
} catch (Exception ex) {
consecutiveErrors.incrementAndGet();
totalErrors.incrementAndGet();
if(ex instanceof ScriptException) {
ScriptException se = (ScriptException)ex;
throw new ScriptException(String.format("Script execution exception on [%s]\n\tFileName:%s\n\tErrorMessage:%s\n\tLineNumber:%s\n\tColNumber:%s", name, se.getMessage(), se.getFileName(), se.getLineNumber(), se.getColumnNumber()));
}
throw new RuntimeException("Script execution exception on [" + name + "]", ex);
} finally {
if(!completed) SystemClock.endTimer();
}
}
// /**
// * {@inheritDoc}
// * @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#invoke()
// */
// @Override
// public void invoke() {
// try {
// invoke(globalBindings);
// } catch (ScriptException e) {
// throw new RuntimeException("Script Exception on [" + name + "]", e);
// }
// }
/**
* Invokes the script its own bindings
* @param args The arguments passed to the script
* @return the return value of the script execution
* @throws ScriptException thrown on any error during invocation
*/
public Object invoke(Object...args) throws ScriptException {
localBindings.put(ScriptEngine.ARGV, args);
return invoke(ctx);
}
/**
* Checks to see if the script has been updated and recompiles if it has
* @throws ScriptException thrown if the script cannot be recompiled
*/
protected void checkForUpdate() throws ScriptException {
boolean changed = false;
final long inMemTs = lastChangeOverride.get();
if(inMemTs>sourceTimestamp) {
changed = true;
sourceTimestamp = inMemTs;
} else {
// this means we can reload the script from the URL source
if(URLHelper.getLastModified(scriptUrl)>sourceTimestamp) {
sourceTimestamp = URLHelper.getLastModified(scriptUrl);
acquireScript();
changed = true;
}
}
if(changed) {
try {
compileScript();
if(disabled) {
disabled = false;
System.out.println("Reloaded and re-enabled [" + scriptUrl + "]");
} else {
System.out.println("Reloaded Script [" + scriptUrl + "]");
}
} catch (Exception se) {
disabled = true;
System.err.println("Failed to compile Script [" + scriptUrl + "]. Script has been disabled.");
System.err.println("\n\t=================================================\n" + source + "\n\t=================================================\n");
se.printStackTrace(System.err);
}
localBindings.put("inited", false);
}
}
/**
* {@inheritDoc}
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ScriptContainer [scriptUrl=\n");
builder.append(scriptUrl);
builder.append("\nsourceTimestamp=");
builder.append(new Date(sourceTimestamp));
builder.append("]");
return builder.toString();
}
/**
* {@inheritDoc}
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((scriptUrl == null) ? 0 : scriptUrl.hashCode());
return result;
}
/**
* {@inheritDoc}
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ScriptContainer other = (ScriptContainer) obj;
if (scriptUrl == null) {
if (other.scriptUrl != null) {
return false;
}
} else if (!scriptUrl.equals(other.scriptUrl)) {
return false;
}
return true;
}
/**
* Returns the source file's file name
* @return the source file's file name
*/
@Override
public String getName() {
return name;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#isEnabled()
*/
@Override
public boolean isEnabled() {
return !disabled;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#setEnabled(boolean)
*/
@Override
public void setEnabled(boolean enabled) {
disabled = !enabled;
}
/**
* Sets the disabled state of this script
* @param disabled true to disable, false to enable
*/
public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getSource()
*/
@Override
public String getSource() {
return source;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#setSource(java.lang.String)
*/
@Override
public void setSource(String src) {
source = src;
if(isWritable()) {
URLHelper.writeToURL(scriptUrl, source.getBytes(), false);
} else {
lastChangeOverride.set(SystemClock.time());
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getScriptURL()
*/
@Override
public URL getScriptURL() {
return scriptUrl;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getConsecutiveErrors()
*/
@Override
public long getConsecutiveErrors() {
return consecutiveErrors.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getTotalErrors()
*/
@Override
public long getTotalErrors() {
return totalErrors.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getTotalInvocations()
*/
@Override
public long getTotalInvocations() {
return totalInvocations.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getLastElapsedTimeMs()
*/
@Override
public long getLastElapsedTimeMs() {
return invocationTimes.isEmpty() ? -1L : invocationTimes.get(0);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getRollingElapsedTimeMs()
*/
@Override
public long getRollingElapsedTimeMs() {
return invocationTimes.avg();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getLocalBindings()
*/
@Override
public Map<String, String> getLocalBindings() {
Map<String, String> b = new HashMap<String, String>(localBindings.size());
for(Map.Entry<String, Object> e: localBindings.entrySet()) {
Object val = e.getValue();
b.put(e.getKey(), val==null ? "" : val.toString());
}
return b;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getGlobalBindings()
*/
@Override
public Map<String, String> getGlobalBindings() {
Map<String, String> b = new HashMap<String, String>(globalBindings.size());
for(Map.Entry<String, Object> e: globalBindings.entrySet()) {
Object val = e.getValue();
b.put(e.getKey(), val==null ? "" : val.toString());
}
return b;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getSourceTimestamp()
*/
@Override
public long getSourceTimestamp() {
if(lastChangeOverride.get()<1) {
return URLHelper.getLastModified(scriptUrl);
}
return lastChangeOverride.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getSourceDate()
*/
@Override
public Date getSourceDate() {
return new Date(getSourceTimestamp());
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#reset()
*/
@Override
public void reset() {
localCollectionSweep = 0L;
invocationTimes.clear();
consecutiveErrors.set(0);
totalErrors.set(0);
totalInvocations.set(0);
acquireScript();
compileScript();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getEngineName()
*/
@Override
public String getEngineName() {
return scriptEngineFactory.getEngineName();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getEngineVersion()
*/
@Override
public String getEngineVersion() {
return scriptEngineFactory.getEngineVersion();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getExtensions()
*/
@Override
public List<String> getExtensions() {
return scriptEngineFactory.getExtensions();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getMimeTypes()
*/
@Override
public List<String> getMimeTypes() {
return scriptEngineFactory.getMimeTypes();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getNames()
*/
@Override
public List<String> getNames() {
return scriptEngineFactory.getNames();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getLanguageName()
*/
@Override
public String getLanguageName() {
return scriptEngineFactory.getLanguageName();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getLanguageVersion()
*/
@Override
public String getLanguageVersion() {
return scriptEngineFactory.getLanguageVersion();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getParameter(java.lang.String)
*/
@Override
public Object getParameter(String key) {
return scriptEngineFactory.getParameter(key);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getMethodCallSyntax(java.lang.String, java.lang.String, java.lang.String[])
*/
@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return scriptEngineFactory.getMethodCallSyntax(obj, m, args);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getOutputStatement(java.lang.String)
*/
@Override
public String getOutputStatement(String toDisplay) {
return scriptEngineFactory.getOutputStatement(toDisplay);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getProgram(java.lang.String[])
*/
@Override
public String getProgram(String... statements) {
return scriptEngineFactory.getProgram(statements);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#isWritable()
*/
@Override
public boolean isWritable() {
return writable;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getLastCompileTimeMs()
*/
@Override
public long getLastCompileTimeMs() {
return compilationTimes.isEmpty() ? -1L : compilationTimes.get(0);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getRollingCompileTimeMs()
*/
@Override
public long getRollingCompileTimeMs() {
return compilationTimes.avg();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#getCustomFrequency()
*/
@Override
public long getCustomFrequency() {
return customFrequency;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.script.ScriptContainerMBean#setCustomFrequency(long)
*/
@Override
public void setCustomFrequency(long customFrequency) {
this.customFrequency = customFrequency;
}
}