package org.oddjob.scheduling; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import org.oddjob.FailedToStopException; import org.oddjob.Resetable; import org.oddjob.Stateful; import org.oddjob.arooa.deploy.annotations.ArooaAttribute; import org.oddjob.arooa.deploy.annotations.ArooaComponent; import org.oddjob.arooa.deploy.annotations.ArooaHidden; import org.oddjob.arooa.life.ComponentPersistException; import org.oddjob.arooa.utils.DateHelper; import org.oddjob.framework.ComponentBoundry; import org.oddjob.jobs.job.ResetAction; import org.oddjob.schedules.Interval; import org.oddjob.schedules.Schedule; import org.oddjob.schedules.ScheduleContext; import org.oddjob.schedules.ScheduleResult; import org.oddjob.scheduling.state.TimerState; import org.oddjob.state.IsAnyState; import org.oddjob.state.IsNot; import org.oddjob.state.State; import org.oddjob.state.StateCondition; import org.oddjob.state.StateEvent; import org.oddjob.state.StateListener; import org.oddjob.state.StateMatch; import org.oddjob.util.Clock; import org.oddjob.util.DefaultClock; import org.oddjob.util.OddjobLockedException; /** * Common functionality for Timers. * * @author rob * */ abstract public class TimerBase extends ScheduleBase { private static final long serialVersionUID = 2009091420120126L; /** * @oddjob.property schedule * @oddjob.description The Schedule used to provide execution * times. * @oddjob.required Yes. */ private transient volatile Schedule schedule; /** * @oddjob.property * @oddjob.description The time zone the schedule is to run * in. This is the text id of the time zone, such as "Europe/London". * More information can be found at * <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/TimeZone.html"> * TimeZone</a>. * @oddjob.required Set automatically. */ private transient volatile TimeZone timeZone; /** * @oddjob.property * @oddjob.description The clock to use. Tells the current time. * @oddjob.required Set automatically. */ private transient volatile Clock clock; /** The currently scheduled job future. */ private transient volatile Future<?> future; /** The scheduler to schedule on. */ private transient volatile ScheduledExecutorService scheduler; /** Provided to the schedule. */ protected final Map<Object, Object> contextData = Collections.synchronizedMap(new HashMap<Object, Object>()); /** * @oddjob.property * @oddjob.description The time the next execution is due. This property * is updated when the timer starts or after each execution. * @oddjob.required Read Only. */ private transient volatile Date nextDue; /** * @oddjob.property * @oddjob.description This is the current/next result from the * schedule. This properties fromDate is used to set the nextDue date for * the schedule and it's useNext (normally the same as toDate) property is * used to calculate the following new current property after execution. This * property is most useful for the Timer to pass limits to * the Retry, but is also useful for diagnostics. * @oddjob.required Set automatically. */ private volatile ScheduleResult current; /** * @oddjob.property * @oddjob.description The time the schedule was lastDue. This is set * from the nextDue property when the job begins to execute. * @oddjob.required Read only. */ private volatile Date lastDue; private transient volatile StateCondition haltOn; private transient volatile ResetAction reset; @ArooaHidden @Inject public void setScheduleExecutorService(ScheduledExecutorService scheduler) { this.scheduler = scheduler; } @Override protected void begin() throws ComponentPersistException { if (schedule == null) { throw new NullPointerException("No Schedule."); } if (scheduler == null) { throw new NullPointerException("No Scheduler."); } if (clock == null) { clock = new DefaultClock(); } } protected void onStop() { super.onStop(); Future<?> future = this.future; if (future != null) { future.cancel(false); future = null; } } @Override protected void postStop() { stateHandler.waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { getStateChanger().setState(TimerState.STARTABLE); } }); } protected void onReset() { contextData.clear(); nextDue = null; current = null; lastDue = null; } /** * Get the time zone id to use in this schedule. * * @return The time zone id being used. */ public String getTimeZone() { if (timeZone == null) { return null; } return timeZone.getID(); } /** * Set the time zone. * * @param timeZoneId the timeZoneId. */ public void setTimeZone(String timeZoneId) { if (timeZoneId == null) { this.timeZone = null; } else { this.timeZone = TimeZone.getTimeZone(timeZoneId); } } /** * Set the schedule. * * @param schedule The schedule. */ public void setSchedule(Schedule schedule) { this.schedule = schedule; } public Schedule getSchedule() { return schedule; } /** * @throws ComponentPersistException * @throws OddjobLockedException * * @oddjob.property reschedule * @oddjob.description Reschedule from the given date/time. * @oddjob.required Only available once the timer has started. */ @ArooaHidden public void setReschedule(final Date reSchedule) throws ComponentPersistException, OddjobLockedException { ComponentBoundry.push(loggerName(), this); try { if (!stateHandler().tryToWhen(new StateMatch(TimerState.STARTED), new Runnable() { @Override public void run() { logger().info("Rescheduling with " + reSchedule); try { CancelAndStopChild(); scheduleFrom(reSchedule); } catch (Exception e) { throw new RuntimeException(e); } } })) { logger().info("Can only reschedule once the timer has started."); } } finally { ComponentBoundry.pop(); } } /** * Cancel and child jobs that are submitted and stop any that are * running. This should only be run while locked because it * temporarily sets the stop flag to fool the state listener into * not rescheduling. * * @throws FailedToStopException */ protected void CancelAndStopChild() throws FailedToStopException { Future<?> future = this.future; if (future != null) { future.cancel(false); future = null; } stop = true; childHelper.stopChildren(); stop = false; } /** * Schedule a job from a given date. * * @param date The date to schedule the job from. * * @return true if the job is scheduled again, false if it is not to * be scheduled again. * * @throws ComponentPersistException */ protected boolean scheduleFrom(Date date) throws ComponentPersistException { logger().debug("Scheduling from [" + date + "]"); if (date == null) { return internalSetNextDue(null); } else { ScheduleContext context = new ScheduleContext( date, timeZone, contextData, getLimits()); current = schedule.nextDue(context); if (current == null) { return internalSetNextDue(null); } else { return internalSetNextDue(current.getFromDate()); } } } /** * Get the current clock. * * @return The clock */ public Clock getClock() { if (clock == null) { clock = new DefaultClock(); } return clock; } /** * Set the clock. Only useful for testing. * * @param clock The clock. */ public void setClock(Clock clock) { this.clock = clock; } /** * Manually set the Next Due Date. * * @param nextDue The Next Due Date. May be null. * * @throws OddjobLockedException */ @ArooaHidden public void setNextDue(final Date nextDue) throws OddjobLockedException { ComponentBoundry.push(loggerName(), this); try { if (!stateHandler().tryToWhen(new StateMatch(TimerState.STARTED), new Runnable() { @Override public void run() { logger().info("Manually setting nextDue to " + nextDue); try { CancelAndStopChild(); internalSetNextDue(nextDue); } catch (Exception e) { throw new RuntimeException(e); } } })) { logger().info("Can't set nextDue until timer has STARTED."); } } finally { ComponentBoundry.pop(); } } /** * Get the next due date. * * @return The next due date */ public Date getNextDue() { return nextDue; } /** * Set the next due date. * * @param nextDue The date schedule is next due. If null the job won't * be scheduled and the child state reflector will be called to reflect * the state of the child job. * * @return true if the job was scheduled. False it wasn't. * * @throws ComponentPersistException */ protected boolean internalSetNextDue(Date nextDue) throws ComponentPersistException { Date oldNextDue = this.nextDue; this.nextDue = nextDue; firePropertyChange("nextDue", oldNextDue, nextDue); if (nextDue == null) { logger().info("There is no nextDue, schedule finished."); childStateReflector.start(); return false; } // save the last complete. save(); long delay = nextDue.getTime() - getClock().getDate().getTime(); if (delay < 0) { delay = 0; } logger().info("Next due at " + nextDue + " in " + DateHelper.formatMilliseconds(delay) + "."); if (delay == 0) { changeStateLocked(TimerState.ACTIVE); } else { changeStateLocked(TimerState.STARTED); } future = scheduler.schedule( new Execution(), delay, TimeUnit.MILLISECONDS); return true; } /** * Get the current/next interval. * * @return The interval, null if not due again. */ public ScheduleResult getCurrent() { return current; } /** * Get the last due date. * * @return The last due date, null when a timer starts for the first time. */ public Date getLastDue() { return lastDue; } /** * @oddjob.property job * @oddjob.description The job to run when it's due. * @oddjob.required Yes. */ @ArooaComponent public void setJob(Runnable job) { if (job == null) { childHelper.removeChildAt(0); } else { if (! (job instanceof Stateful)) { throw new IllegalStateException("Child job must be Stateful. [" + job + "] is of class " + job.getClass().getName()); } childHelper.insertChild(0, job); } } /** * Implementation provided by sub classes so limits are available in * {@link #scheduleFrom(Date)}. * * @return The limits, or null. Retry has limits, timer doesn't. */ abstract protected Interval getLimits(); /** * Reschedule a job. * * @return True if it was schedule, false if it wasn't. * * @throws ComponentPersistException */ protected boolean reschedule() throws ComponentPersistException { Date use = getCurrent().getUseNext(); Date now = getClock().getDate(); if (use != null && isSkipMissedRuns() && use.before(now)) { use = now; } return scheduleFrom(use); } /** * Implementation provided by sub classes to decide what kind of reset to send * to the child. Timer sends a hard reset, Retry sends a soft reset. * * @param job The child job that will be reset. */ protected void reset(Resetable job) { ResetAction resetAction = this.reset; if (reset == null) { resetAction = getDefaultReset(); } logger().debug("Sending [" + resetAction + "] Reset to [" + job + "]"); resetAction.doWith(job); } /** * Listen for changed child job states. Note these could come in on * a different thread to that which launched the Executor. * */ class RescheduleStateListener implements StateListener { @Override public void jobStateChange(StateEvent event) { ComponentBoundry.push(loggerName(), TimerBase.this); try { handleChildState(event, this); } catch (final ComponentPersistException e) { stateHandler().waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { getStateChanger().setStateException(e); } }); } finally { ComponentBoundry.pop(); } } } protected void handleChildState(StateEvent event, StateListener listener) throws ComponentPersistException { State state = event.getState(); if (stop || state.isDestroyed()) { event.getSource().removeStateListener(listener); return; } if (state.isReady()) { return; } StateCondition haltOn = getHaltOn(); if (haltOn == null) { haltOn = getDefaultHaltOn(); } if (haltOn.test(state)) { event.getSource().removeStateListener(listener); internalSetNextDue(null); return; } if (state.isStoppable()) { if (state.isComplete()) { changeStateLocked(TimerState.STARTED); } else { changeStateLocked(TimerState.ACTIVE); } return; } // Order is important! Must remove this before scheduling again. event.getSource().removeStateListener(listener); reschedule(); } protected boolean changeStateLocked(final TimerState required) { return stateHandler().waitToWhen( new IsNot(new StateMatch(required)), new Runnable() { @Override public void run() { getStateChanger().setState(required); } }); } /** */ class Execution implements Runnable { public void run() { ComponentBoundry.push(loggerName(), this); try { try { // Wait for the timer to start to ensure predictable // state transitions. begun.await(); } catch (InterruptedException e) { logger().warn("Interrupted."); Thread.currentThread().interrupt(); return; } Runnable job = childHelper.getChild(); if (stop) { logger().info("Not Executing [" + job + "] + as we have now stopped."); return; } if (job == null) { logger().warn("Nothing to run. Job is null!"); return; } logger().info("Executing [" + job + "] due at " + nextDue); lastDue = nextDue; stateHandler.waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { getStateChanger().setState(TimerState.ACTIVE); } }); try { reset((Resetable) job); ((Stateful) job).addStateListener( new RescheduleStateListener()); job.run(); logger().info("Finished executing [" + job + "]"); } catch (final Exception t) { logger().error("Failed running scheduled job.", t); stateHandler().waitToWhen(new IsAnyState(), new Runnable() { public void run() { getStateChanger().setStateException(t); } }); } } finally { ComponentBoundry.pop(); } } @Override public String toString() { return TimerBase.this.toString(); } } public StateCondition getHaltOn() { return haltOn; } @ArooaAttribute public void setHaltOn(StateCondition haltOn) { this.haltOn = haltOn; } abstract protected StateCondition getDefaultHaltOn(); abstract protected boolean isSkipMissedRuns(); public ResetAction getReset() { return reset; } @ArooaAttribute public void setReset(ResetAction reset) { this.reset = reset; } abstract protected ResetAction getDefaultReset(); }