diff --git a/src/helma/framework/core/Application.java b/src/helma/framework/core/Application.java index cfec17df..627ab771 100644 --- a/src/helma/framework/core/Application.java +++ b/src/helma/framework/core/Application.java @@ -146,6 +146,11 @@ public final class Application implements IPathElement, Runnable { // the name under which this app serves XML-RPC requests. Defaults to the app name private String xmlrpcHandlerName; + // the list of cron jobs + private Map activeCronJobs = null; + private Vector cronJobs = null; + Hashtable customCronJobs = null; + /** * Build an application with the given name in the app directory. No Server-wide * properties are created or used. @@ -305,6 +310,8 @@ public final class Application implements IPathElement, Runnable { } activeRequests = new Hashtable(); + activeCronJobs = new WeakHashMap(); + customCronJobs = new Hashtable(); skinmgr = new SkinManager(this); @@ -1355,42 +1362,88 @@ public final class Application implements IPathElement, Runnable { } } - // check if we should call scheduler - if ((now - lastScheduler) > scheduleSleep) { - lastScheduler = now; - - Object val = null; - - try { - val = eval.invokeFunction((INode) null, "scheduler", new Object[0]); - } catch (Exception ignore) { - } - - try { - int ret = ((Number) val).intValue(); - - if (ret < 1000) { - scheduleSleep = 60000L; - } else { - scheduleSleep = ret; - } - } catch (Exception ignore) { - } - - // logEvent ("Called scheduler for "+name+", will sleep for "+scheduleSleep+" millis"); + if ((cronJobs == null) || (props.lastModified() > lastPropertyRead)) { + updateProperties(); + cronJobs = CronJob.parse(props); } - // sleep until we have work to do + Date d = new Date(); + List jobs = (List) cronJobs.clone(); + + jobs.addAll(customCronJobs.values()); + CronJob.sort(jobs); + + for (Iterator i = jobs.iterator(); i.hasNext();) { + CronJob j = (CronJob) i.next(); + + if (j.appliesToDate(d)) { + // check if the job is already active ... + if (activeCronJobs.containsKey(j.getName())) { + logEvent(j + " is still active, skipped in this minute"); + + continue; + } + + RequestEvaluator thisEvaluator; + + try { + thisEvaluator = getEvaluator(); + } catch (RuntimeException rt) { + if (stopped == false) { + logEvent("couldn't execute " + j + + ", maximum thread count reached"); + + continue; + } else { + break; + } + } + + // if the job has a long timeout or we're already late during this minute + // the job is run from an extra thread + if ((j.getTimeout() > 20000) || + (CronJob.millisToNextFullMinute() < 30000)) { + CronRunner r = new CronRunner(thisEvaluator, j); + + r.start(); + activeCronJobs.put(j.getName(), r); + } else { + try { + thisEvaluator.invokeFunction((INode) null, j.getFunction(), + new Object[0], j.getTimeout()); + } catch (Exception ex) { + logEvent("error running " + j + ": " + ex.toString()); + } finally { + if (stopped == false) { + releaseEvaluator(thisEvaluator); + } + } + } + + thisEvaluator = null; + } + } + + // sleep until the next full minute try { - worker.sleep(Math.min(cleanupSleep, scheduleSleep)); + worker.sleep(CronJob.millisToNextFullMinute()); } catch (InterruptedException x) { logEvent("Scheduler for " + name + " interrupted"); worker = null; - break; } } + // when interrupted, shutdown running cron jobs + synchronized (activeCronJobs) { + for (Iterator i = activeCronJobs.keySet().iterator(); i.hasNext();) { + String jobname = (String) i.next(); + + ((CronRunner) activeCronJobs.get(jobname)).interrupt(); + activeCronJobs.remove(jobname); + } + } + logEvent("Scheduler for " + name + " exiting"); } @@ -1761,4 +1814,29 @@ public final class Application implements IPathElement, Runnable { logEvent("error loading session data: " + e.toString()); } } + + class CronRunner extends Thread { + RequestEvaluator thisEvaluator; + CronJob job; + + public CronRunner(RequestEvaluator thisEvaluator, CronJob job) { + this.thisEvaluator = thisEvaluator; + this.job = job; + } + + public void run() { + try { + thisEvaluator.invokeFunction((INode) null, job.getFunction(), + new Object[0], job.getTimeout()); + } catch (Exception ex) { + } + + if (stopped == false) { + releaseEvaluator(thisEvaluator); + } + + thisEvaluator = null; + activeCronJobs.remove(job.getName()); + } + } } diff --git a/src/helma/framework/core/ApplicationBean.java b/src/helma/framework/core/ApplicationBean.java index 8eedf01e..a702f24a 100644 --- a/src/helma/framework/core/ApplicationBean.java +++ b/src/helma/framework/core/ApplicationBean.java @@ -17,6 +17,7 @@ package helma.framework.core; import helma.objectmodel.INode; +import helma.util.CronJob; import java.io.File; import java.io.Serializable; import java.util.ArrayList; @@ -254,6 +255,45 @@ public class ApplicationBean implements Serializable { return (SessionBean[]) userSessions.toArray(new SessionBean[0]); } + /** + * + * + * @param functionName ... + */ + public void addCronJob(String functionName) { + CronJob job = new CronJob(functionName); + + job.setFunction(functionName); + app.customCronJobs.put(functionName, job); + } + + /** + * + * + * @param functionName ... + * @param year ... + * @param month ... + * @param day ... + * @param weekday ... + * @param hour ... + * @param minute ... + */ + public void addCronJob(String functionName, String year, String month, String day, + String weekday, String hour, String minute) { + CronJob job = CronJob.newJob(functionName, year, month, day, weekday, hour, minute); + + app.customCronJobs.put(functionName, job); + } + + /** + * + * + * @param functionName ... + */ + public void removeCronJob(String functionName) { + app.customCronJobs.remove(functionName); + } + // getter methods for readonly properties of this application public int getcacheusage() { return app.getCacheUsage(); diff --git a/src/helma/framework/core/RequestEvaluator.java b/src/helma/framework/core/RequestEvaluator.java index d2ee4e19..029d599d 100644 --- a/src/helma/framework/core/RequestEvaluator.java +++ b/src/helma/framework/core/RequestEvaluator.java @@ -737,6 +737,25 @@ public final class RequestEvaluator implements Runnable { public synchronized Object invokeFunction(Object object, String functionName, Object[] args) throws Exception { + // give internal call more time (15 minutes) to complete + return invokeFunction(object, functionName, args, 60000L * 15); + } + + /** + * + * + * @param object ... + * @param functionName ... + * @param args ... + * @param timeout ... + * + * @return ... + * + * @throws Exception ... + */ + public synchronized Object invokeFunction(Object object, String functionName, + Object[] args, long timeout) + throws Exception { reqtype = INTERNAL; session = null; thisObject = object; @@ -747,7 +766,7 @@ public final class RequestEvaluator implements Runnable { exception = null; checkThread(); - wait(60000L * 15); // give internal call more time (15 minutes) to complete + wait(timeout); if (reqtype != NONE) { stopThread(); @@ -799,7 +818,6 @@ public final class RequestEvaluator implements Runnable { if (exception != null) { throw (exception); } - return result; } diff --git a/src/helma/util/CronJob.java b/src/helma/util/CronJob.java index 05ce44ac..159fe2af 100644 --- a/src/helma/util/CronJob.java +++ b/src/helma/util/CronJob.java @@ -86,7 +86,7 @@ public class CronJob { private String name = null; private String function = null; - private long timeout = 30000; + private long timeout = 20000; /** A method for parsing properties. It looks through the properties * file for entries that look like this: @@ -161,7 +161,26 @@ public class CronJob { */ - public static Collection parse(Properties props) { + public static CronJob newJob (String functionName, String year, String month, String day, String weekday, String hour, String minute) { + CronJob job = new CronJob (functionName); + job.setFunction (functionName); + if (year != null) + parseYear (job, year); + if (month != null) + parseMonth (job, month); + if (day != null) + parseDay (job, day); + if (weekday != null) + parseWeekDay (job, weekday); + if (hour != null) + parseHour (job, hour); + if (minute != null) + parseMinute (job, minute); + return job; + } + + + public static Vector parse(Properties props) { Hashtable jobs = new Hashtable (); Enumeration e = props.keys (); while (e.hasMoreElements ()) { @@ -195,13 +214,40 @@ public class CronJob { parseHour (job, value); } else if (jobSpec.equalsIgnoreCase("minute")) { parseMinute (job, value); + } else if (jobSpec.equalsIgnoreCase("timeout")) { + parseTimeout (job, value); } } catch (NoSuchElementException nsee) { } } - return jobs.values (); + Vector jobVec = new Vector (jobs.values ()); + return (Vector) sort (jobVec); } + public static List sort (List list) { + Collections.sort (list, new Comparator() { + public int compare (Object o1, Object o2) { + CronJob cron1 = (CronJob) o1; + CronJob cron2 = (CronJob) o2; + if (cron1.getTimeout () > cron2.getTimeout ()) + return 1; + else if (cron1.getTimeout () < cron2.getTimeout ()) + return -1; + else + return 0; + } + public boolean equals (Object o1, Object o2) { + if (o1!=null) { + return o1.equals (o2); + } else { + return false; + } + } + + }); + return list; + } + public static void parseYear (CronJob job, String value) { if (value.equals("*")) { @@ -351,6 +397,22 @@ public class CronJob { } + public static void parseTimeout (CronJob job, String timeout) { + long timeoutValue = 1000 * Long.valueOf(timeout).longValue (); + job.setTimeout (timeoutValue); + } + + public static long nextFullMinute () { + long now = System.currentTimeMillis(); + long millisAfterMinute = (now % 60000); + return (now + 60000 - millisAfterMinute); + } + + public static long millisToNextFullMinute () { + long now = System.currentTimeMillis(); + long millisAfterMinute = (now % 60000); + return (60000 - millisAfterMinute); + } /** * Create an empty CronJob. @@ -657,13 +719,6 @@ public class CronJob { this.timeout = timeout; } - /** - * Set this entry's timeout - */ - public void setTimeout(String timeout) - { - this.timeout = Long.valueOf(timeout).longValue (); - } /** * Get this entry's timeout @@ -672,4 +727,9 @@ public class CronJob { { return this.timeout; } + + public String toString () { + return "[CronJob " + name + "]"; + } + }