package org.oddjob.state; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.apache.log4j.Logger; import org.oddjob.Stateful; import org.oddjob.framework.JobDestroyedException; import org.oddjob.util.OddjobLockedException; /** * Helps Jobs handle state change. * <p> * Attempted to make {@link #waitToWhen(StateCondition, Runnable)} and * {@link #tryToWhen(StateCondition, Runnable) both use timeouts. This * now required interrupt handling and the tryLock was intermittently * interrupted. The cause of this could not be found so attempting to * implement timeouts was abandoned for the time being. * * @author Rob Gordon */ public class StateHandler<S extends State> implements Stateful, StateLock { private static final Logger logger = Logger.getLogger(StateHandler.class); private final S readyState; /** The source. */ private final Stateful source; /** State listeners */ private final List<StateListener> listeners = new CopyOnWriteArrayList<>(); /** The last event */ private volatile StateEvent lastEvent; /** Used to stop listeners changing state. */ private boolean fireing; /** Used for the state lock. */ private final ReentrantLock lock = new ReentrantLock(true) { private static final long serialVersionUID = 2010080400L; public String toString() { Thread o = getOwner(); return "[" + source + "]" + ((o == null) ? "[Unlocked]" : "[Locked by thread " + o.getName() + "]"); } }; private final Condition alarm = lock.newCondition(); /** * Constructor. * * @param source The source for events. * @param readyState The ready state. */ public StateHandler(Stateful source, S readyState) { this.source = source; lastEvent = new StateEvent(source, readyState, null); this.readyState = readyState; } /** * Get the last event. * * @return The last event. */ @Override public StateEvent lastStateEvent() { return callLocked(new Callable<StateEvent>() { @Override public StateEvent call() { return lastEvent; } }); } /** * Typically only called after restoring a jobstate handler after deserialisation. * * @param source */ public void restoreLastJobStateEvent(StateEvent.SerializableNoSource savedEvent) { // If state was saved when executing it now has to // be ready, because oddjob must have crashed last time. if (savedEvent.getState().isStoppable()) { lastEvent = new StateEvent(source, readyState); } else { lastEvent = new StateEvent(source, savedEvent.getState(), savedEvent.getTime(), savedEvent.getException()); } } /* * (non-Javadoc) * @see org.oddjob.state.StateChanger#setJobState(org.oddjob.state.JobState, java.util.Date) */ public void setState(S state, Date date) throws JobDestroyedException { setLastJobStateEvent(new StateEvent(source, state, date, null)); } /* * (non-Javadoc) * @see org.oddjob.state.StateChanger#setJobState(org.oddjob.state.JobState) */ public void setState(S state) throws JobDestroyedException { setLastJobStateEvent(new StateEvent(source, state, null)); } /* * (non-Javadoc) * @see org.oddjob.state.StateChanger#setJobStateException(java.lang.Throwable, java.util.Date) */ public void setStateException(S state, Throwable t, Date date) throws JobDestroyedException { setLastJobStateEvent( new StateEvent(source, state, date, t)); } /* * (non-Javadoc) * @see org.oddjob.state.StateChanger#setJobStateException(java.lang.Throwable) */ public void setStateException(State state, Throwable ex) throws JobDestroyedException { setLastJobStateEvent(new StateEvent(source, state, ex)); } private void setLastJobStateEvent(StateEvent event) throws JobDestroyedException { assertAlive(); assertLockHeld(); if (fireing) { throw new IllegalStateException( "Can't change state from a listener!"); } lastEvent = event; } /** * Return the current state of the job. * * @return The current state. */ public State getState() { return callLocked(new Callable<State>() { @Override public State call() throws Exception { return lastEvent.getState(); } }); } /** * Convenience method to check the job hasn't been destroyed. * * @throws JobDestroyedException If it has. */ public void assertAlive() throws JobDestroyedException { if (lastEvent.getState().isDestroyed()) { throw new JobDestroyedException(source); } } public void assertLockHeld() { if (!lock.isHeldByCurrentThread()) { throw new IllegalStateException("[" + source + "] State Lock not held by thread [" + Thread.currentThread().getName() + "]"); } } /* * (non-Javadoc) * @see org.oddjob.state.StateLock#tryToWhen(org.oddjob.state.StateCondition, java.lang.Runnable) */ public boolean tryToWhen(StateCondition when, Runnable runnable) throws OddjobLockedException { if (!lock.tryLock()) { throw new OddjobLockedException(lock.toString()); } try { return doWhen(when, runnable); } finally { lock.unlock(); } } /* * (non-Javadoc) * @see org.oddjob.state.StateLock#waitToWhen(org.oddjob.state.StateCondition, java.lang.Runnable) */ public boolean waitToWhen(StateCondition when, Runnable runnable) { lock.lock(); try { return doWhen(when, runnable); } finally { lock.unlock(); } } /** * Do the work that will be executed when this thread holds * the lock. * * @param when * @param runnable * * @return true if the test is true and the work is done, false * otherwise. */ private boolean doWhen(StateCondition when, Runnable runnable) { if (when.test(lastEvent.getState())) { runnable.run(); return true; } else { return false; } } /** * Runs the Callable locked. * * @param callable The callable. * @return The result of the callable. */ public <T> T callLocked(Callable<T> callable) { lock.lock(); try { return callable.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } /** * Sleep. * * @param time * @throws InterruptedException */ public void sleep(long time) throws InterruptedException { assertLockHeld(); if (time == 0) { alarm.await(); } else { alarm.await(time, TimeUnit.MILLISECONDS); } } /** * Wake any threads that are sleeping via {@link #sleep(long)}. * */ public void wake() { assertLockHeld(); alarm.signalAll(); } /** * Add a job state listener. This method will send the last event * to the new listener. It is possible that the listener may get the * notification twice. * * @param listener The listener. * * @throws JobDestroyedException */ public void addStateListener(final StateListener listener) throws JobDestroyedException { assertAlive(); waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { // setting pending event stops the listener chaining state. listeners.add(listener); fireing = true; try { listener.jobStateChange(lastEvent); } finally { fireing = false; } } }); } /** * Remove a job state listener. * * @param listener The listener. */ public void removeStateListener(final StateListener listener) { waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { listeners.remove(listener); } }); } /** * The number of listeners. * * @return The number of listeners. */ public int listenerCount() { return callLocked(new Callable<Integer>() { @Override public Integer call() throws Exception { return listeners.size(); } }).intValue(); } /** * Override toString. */ public String toString() { return "JobStateHandler( " + lastEvent.getState() + " )"; } /** * Fire the event, update last event. * * @param event The event. */ public void fireEvent() { assertLockHeld(); if (fireing) { throw new IllegalStateException( "Can't fire event from a listener!"); } fireing = true; try { doFireEvent(lastEvent); } finally { fireing = false; } } private void doFireEvent(StateEvent event) { if (event == null) { throw new NullPointerException("No JobStateEvent."); } for (StateListener listener : listeners) { try { listener.jobStateChange(event); } catch (Throwable t) { logger.error("Failed notifiying listener [" + listener + "] of event [" + event + "]", t); } } } }