com.tangosol.util.fsm.NonBlockingFiniteStateMachine Maven / Gradle / Ivy
Show all versions of coherence Show documentation
/*
* Copyright (c) 2000, 2022, Oracle and/or its affiliates.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* http://oss.oracle.com/licenses/upl.
*/
package com.tangosol.util.fsm;
import com.oracle.coherence.common.base.Blocking;
import com.oracle.coherence.common.base.Logger;
import com.tangosol.internal.util.DaemonPoolDependencies;
import com.tangosol.internal.util.Daemons;
import com.tangosol.internal.util.DaemonPool;
import com.tangosol.internal.util.DefaultDaemonPoolDependencies;
import com.tangosol.net.GuardSupport;
import com.tangosol.net.PriorityTask;
import com.tangosol.net.cache.KeyAssociation;
import com.tangosol.util.SafeHashSet;
import com.tangosol.util.fsm.Instruction.ProcessEvent;
import com.tangosol.util.fsm.Instruction.TransitionTo;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.EnumMap;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* An {@link NonBlockingFiniteStateMachine} is a specialized {@link
* FiniteStateMachine} implementation that performs transitions
* asynchronously to the threads that request state changes. That is,
* threads that request state transitions are never blocked. Instead their
* requests are queued for a single thread to later perform the appropriate
* transition to the requested state.
*
* @author Brian Oliver
* @since Coherence 12.2.1
*/
public class NonBlockingFiniteStateMachine>
implements FiniteStateMachine, ExecutionContext
{
// ----- constructors ---------------------------------------------------
/**
* Construct an {@link NonBlockingFiniteStateMachine} given a {@link Model}.
*
* @param sName the name of the {@link NonBlockingFiniteStateMachine}
* @param model the {@link Model} of the {@link NonBlockingFiniteStateMachine}
* @param stateInitial the initial state
* @param daemonPoolDeps Optional {@link DaemonPoolDependencies} for Daemon Pool that will be used
* for scheduling {@link Transition}s
* @param fIgnoreExceptions when true
{@link RuntimeException}s will be ignored,
* when false
{@link RuntimeException}s will immediately
* stop the {@link NonBlockingFiniteStateMachine}
* @param deps the {@link TaskDependencies} specifies the event processing configs.
*/
public NonBlockingFiniteStateMachine(String sName, Model model, S stateInitial,
DaemonPoolDependencies daemonPoolDeps, boolean fIgnoreExceptions, TaskDependencies deps)
{
// TODO: we should prove that the model is valid
// ie: no isolated/unreachable states
// ie: no multiple paths from one state to another state, ensuring that two or more transitions
// from A to B are not defined ie: that there are no cycles formed by potential "synchronous"
// state transitions, ensuring that A and B don't have any cycles formed by synchronous state
// transitions between them, and thus deadlocks in the finite state machine can't occur.
f_sName = sName;
m_stateInitial = stateInitial;
m_fAllowTransitions = true;
f_atomicTransitions = new AtomicLong();
f_atomicPendingEvents = new AtomicLong();
m_fIgnoreExceptions = fIgnoreExceptions;
m_setListeners = new SafeHashSet();
f_dependencies = new DefaultTaskDependencies(deps);
// force single thread pool to control processing order of state transitions
DefaultDaemonPoolDependencies depsPool = new DefaultDaemonPoolDependencies(daemonPoolDeps);
depsPool.setThreadCount(1);
depsPool.setThreadCountMax(1);
f_daemonPool = Daemons.newDaemonPool(depsPool);
// build the transitions table based on the model
S[] states = model.getStates();
Class clzState = model.getStateClass();
f_mapTransitions = new EnumMap<>(clzState);
for (S stateFrom : states)
{
f_mapTransitions.put(stateFrom, new EnumMap<>(clzState));
}
for (Transition transition : model.getTransitions())
{
for (S stateFrom : states)
{
if (transition.isStartingState(stateFrom))
{
f_mapTransitions.get(stateFrom).put(transition.getEndingState(), transition);
}
}
}
// create the state entry and exit action tables based on the model
f_mapEntryActions = new EnumMap<>(clzState);
f_mapExitActions = new EnumMap<>(clzState);
for (S state : states)
{
f_mapEntryActions.put(state, model.getStateEntryActions().get(state));
f_mapExitActions.put(state, model.getStateExitActions().get(state));
}
// set the current state
m_state = null;
}
// ----- FiniteStateMachine interface -----------------------------------
/**
* {@inheritDoc}
*/
@Override
public void addListener(FiniteStateMachineListener listener)
{
m_setListeners.add(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void removeListener(FiniteStateMachineListener listener)
{
m_setListeners.remove(listener);
}
/**
* {@inheritDoc}
*/
@Override
public String getName()
{
return f_sName;
}
/**
* {@inheritDoc}
*/
@Override
public S getState()
{
return m_state;
}
/**
* {@inheritDoc}
*/
@Override
public long getTransitionCount()
{
return f_atomicTransitions.get();
}
/**
* {@inheritDoc}
*/
@Override
public synchronized boolean start()
{
if (!m_fAllowTransitions)
{
throw new IllegalStateException("The FiniteStateMachine cannot be started because it was stopped");
}
// return true if this is the first start
boolean fStarting = false;
if (!m_fStarted)
{
f_daemonPool.start();
m_fAcceptEvents = true;
process(new TransitionTo<>(m_stateInitial));
m_fStarted = true;
fStarting = true;
}
return fStarting;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized boolean stop()
{
if (!m_fStarted)
{
throw new IllegalStateException("The FiniteStateMachine cannot be stopped " +
"because it has never been started");
}
boolean fStopping = false;
if (m_fAcceptEvents)
{
f_daemonPool.stop();
m_fAcceptEvents = false;
m_fAllowTransitions = false;
fStopping = true;
}
return fStopping;
}
// ----- NonBlockingFiniteStateMachine methods --------------------------
/**
* Requests the {@link FiniteStateMachine} to stop accepting new {@link
* Event}s to process, wait for any existing queued {@link Event}s to be
* processed and then stop.
*
* Note: Once stopped a {@link FiniteStateMachine} can't be restarted.
* Instead a new {@link FiniteStateMachine} should be created.
*
* @return true
if the {@link FiniteStateMachine} was
* stopped or false
if it was already stopped
*
* @throws IllegalStateException if the FiniteStateMachine was never started
*/
public synchronized boolean quiesceThenStop()
{
if (!m_fStarted)
{
throw new IllegalStateException("The FiniteStateMachine cannot be stopped " +
"because it has never been started");
}
boolean fStopped = false;
if (m_fAcceptEvents)
{
m_fAcceptEvents = false;
while (f_atomicPendingEvents.get() > 0)
{
try
{
// wait for half a second to see if there are no more pending transitions (this
// non-infinite wait is to protect us against the possibility that we miss being notified)
Blocking.wait(this, 500);
}
catch (InterruptedException e)
{
Thread.interrupted();
Logger.log(String.format(
"[%s]: Thread interrupted while quiescing; stopping immediately", f_sName), Logger.ALWAYS);
Logger.log(toString(), Logger.ALWAYS);
break;
}
}
m_fAllowTransitions = false;
f_daemonPool.stop(); // COH-21710 - stop the worker thread even if there are more pending events;
// it will no longer process pending events anyway
fStopped = f_atomicPendingEvents.get() == 0;
}
return fStopped;
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return "NonBlockingFSM status {name:" + f_sName + " isStarted:" + m_fStarted + " isAcceptingEvents:" +
m_fAcceptEvents + " PendingEvents:" + f_atomicPendingEvents.get() + "}";
}
/**
* {@inheritDoc}
*/
@Override
public void process(Event event)
{
processLater(event, 0, TimeUnit.SECONDS);
}
/**
* Request the {@link FiniteStateMachine} to process the specified {@link
* Event} as soon as possible.
*
* Note: This method is semantically equivalent to {@link #process(Event)}.
*
* @param event the {@link Event} for the {@link FiniteStateMachine} to process
*/
public void processLater(Event event)
{
processLater(event, 0, TimeUnit.SECONDS);
}
/**
* Request the {@link FiniteStateMachine} to process the specified {@link
* Event} at some point in the future (represented as a duration to wait
* from the moment the method is called).
*
* Note: There's no guarantee that the {@link Event} will processed
* because:
*
* - the {@link Transition} to be performed for the
* {@link Event} is invalid as the {@link FiniteStateMachine} is not in the
* required starting state.
*
- the {@link FiniteStateMachine} may have been stopped.
*
*
* @param event the {@link Event} for the {@link FiniteStateMachine} to process
* @param duration the amount of the {@link TimeUnit} to wait before the {@link Event} is processed
* @param timeUnit the {@link TimeUnit}
*/
public void processLater(Event event, long duration, TimeUnit timeUnit)
{
if (m_fAcceptEvents)
{
final Event preparedEvent = prepareEvent(event);
if (preparedEvent == null)
{
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: Ignoring event %s as it vetoed being prepared",
// f_sName, event));
}
else
{
f_daemonPool.schedule(new Task(preparedEvent, f_dependencies), timeUnit.toMillis(duration));
}
}
else
{
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: Ignoring request to process the event %s in %d %s as the " +
// "machine is no longer accepting new transitions",
// f_sName, event, duration, timeUnit));
}
}
/**
* A PriorityTask implementation to process a requested event.
*/
protected class Task
implements Runnable, PriorityTask, KeyAssociation
{
/**
* Create a Task with given event.
*
* @param event the event that needs to be processed
* @param deps the task dependencies
*/
public Task(Event event, TaskDependencies deps)
{
f_event = event;
f_dependencies = deps;
}
/**
* {@inheritDoc}
*/
@Override
public Object getAssociatedKey()
{
return f_dependencies.getAssociatedKey();
}
/**
* {@inheritDoc}
*/
@Override
public void run()
{
processEvent(f_event);
}
/**
* {@inheritDoc}
*/
@Override
public int getSchedulingPriority()
{
return PriorityTask.SCHEDULE_STANDARD;
}
/**
* {@inheritDoc}
*/
@Override
public long getExecutionTimeoutMillis()
{
return f_dependencies.getExecutionTimeoutMillis();
}
/**
* {@inheritDoc}
*/
@Override
public long getRequestTimeoutMillis()
{
// this is not used in event processing
return 0;
}
/**
* {@inheritDoc}
*/
@Override
public void runCanceled(boolean fAbandoned)
{
}
/**
* An event needs to be processed.
*/
private final Event f_event;
/**
* The dependencies to configure the Task.
*/
private final TaskDependencies f_dependencies;
}
/**
* Dependencies for Task.
*/
public interface TaskDependencies
{
/**
* Return the execution timeout for the task in millisecond.
* @return the execution timeout
*/
public long getExecutionTimeoutMillis();
/**
* Return the associated key for the task.
* @return the associated key
*/
public Object getAssociatedKey();
}
/**
* Implementation of Dependencies for Task
*/
public static class DefaultTaskDependencies
implements TaskDependencies
{
/**
* Default constructor.
*/
public DefaultTaskDependencies()
{
}
/**
* Create a DefaultTaskDependencies with provided {@link TaskDependencies}.
*
* @param deps the TaskDependencies
*/
public DefaultTaskDependencies(TaskDependencies deps)
{
if (deps != null)
{
m_cExecutionTimeout = deps.getExecutionTimeoutMillis();
m_oAssociatedKey = deps.getAssociatedKey();
}
}
/**
* {@inheritDoc}
*/
@Override
public long getExecutionTimeoutMillis()
{
return m_cExecutionTimeout;
}
/**
* Configure the execution timeout for Task.
*
* @param timeout execution timeout in millisecond
*
* @return this object
*/
public DefaultTaskDependencies setExecutionTimeoutMillis(long timeout)
{
m_cExecutionTimeout = timeout;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Object getAssociatedKey()
{
return m_oAssociatedKey;
}
/**
* Configure the associated key for Task.
*
* @param key the associated key
*
* @return this object.
*/
public DefaultTaskDependencies setAssociatedKey(Object key)
{
m_oAssociatedKey = key;
return this;
}
/**
* The execution timeout for Task.
*/
private long m_cExecutionTimeout = 5000L;
/**
* The associated key for Task.
*/
private Object m_oAssociatedKey = "FSM-Task";
}
/**
* Prepares an {@link Event} to be accepted for processing.
*
* @param event the {@link Event} to prepare
*
* @return the prepared {@link Event} (or null
if the event
* should not be processed)
*/
private Event prepareEvent(Event event)
{
// assume the worst - no event is prepared
Event prepared = null;
if (m_fAcceptEvents)
{
// ensure lifecycle aware events are notified
if (event instanceof LifecycleAwareEvent)
{
LifecycleAwareEvent lifecycleAwareEvent = (LifecycleAwareEvent) event;
prepared = lifecycleAwareEvent.onAccept(this) ? lifecycleAwareEvent : null;
}
else
{
prepared = event;
}
}
if (prepared != null)
{
// increase the number of events that are now pending
f_atomicPendingEvents.incrementAndGet();
}
return prepared;
}
/**
* Determines if there are any pending {@link Event}s for the {@link
* FiniteStateMachine} to process.
*
* Note: If the {@link FiniteStateMachine} can no longer process {@link
* Event}s false will be returned.
*
* @return true
if there are pending {@link Event}s and
* {@link Event}s can be processed, false
otherwise
*/
public boolean hasPendingEvents()
{
return m_fAllowTransitions && f_atomicPendingEvents.get() > 0;
}
/**
* Processes the specified {@link Event}, causing the {@link
* FiniteStateMachine} to {@link Transition} to a new state if required.
*
* @param event the {@link Event} to process
*/
@SuppressWarnings("unchecked")
private void processEvent(Event event)
{
// loop through to handle subsequent events, if any
while (event != null && m_fAllowTransitions)
{
// notify the lifecycle aware event of the commencement of processing
if (event instanceof LifecycleAwareEvent)
{
LifecycleAwareEvent lifecycleAwareEvent = (LifecycleAwareEvent) event;
lifecycleAwareEvent.onProcessing(this);
}
// determine the desired state from the event
S stateCurrent = getState();
S stateDesired = event.getDesiredState(stateCurrent, this);
boolean fIsInitialState = stateCurrent == null;
// as we're processing an event, decrease the counter of
// pending events
f_atomicPendingEvents.decrementAndGet();
// if there's no desired state, we do nothing
if (stateDesired == null)
{
// do nothing for the event
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: Ignoring event %s as it produced a null desired state.",
// f_sName, event));
// no more events to process
event = null;
}
else
{
// assume no transition will be made
Transition transition = null;
// when we have a current and desired state, we can
// perform a transition
if (!fIsInitialState)
{
// determine the appropriate transition from the
// current state to the desired state (using the transition table)
transition = f_mapTransitions.get(stateCurrent).get(stateDesired);
if (transition == null)
{
// there's no transition from the current state to the
// desired state, so ignore the request
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: Can't find a valid transition from %s to %s. " +
// "Ignoring event %s.", f_sName, stateCurrent, stateDesired, event));
event = null;
}
else
{
// fetch the action to execute for the transition
TransitionAction actionTransition = transition.getAction();
// attempt to execute the action for the transition
// (if we have one)
if (actionTransition != null)
{
try
{
// perform the action
actionTransition.onTransition(
transition.getName(),
stateCurrent,
transition.getEndingState(),
event,
this);
}
catch (RollbackTransitionException e)
{
Logger.log(String.format("[%s]: Transition for event %s from " +
"%s to %s has been rolled back due to:\n%s", f_sName, event,
stateCurrent, stateDesired, e), Logger.ALWAYS);
event = null;
}
catch (RuntimeException e)
{
// todo: temporary, redo this when we redo all logging
Logger.log(e, Logger.ALWAYS);
if (m_fIgnoreExceptions)
{
Logger.finest(String.format("[%s]: Transition Action %s for event %s " +
"from %s to %s raised runtime exception (continuing with " +
"transition and ignoring the exception):\n%s", f_sName,
actionTransition, event, stateCurrent, stateDesired, e));
}
else
{
m_fAcceptEvents = false;
m_fAllowTransitions = false;
StringWriter writerString = new StringWriter();
PrintWriter writerPrint = new PrintWriter(writerString);
e.printStackTrace(writerPrint);
writerPrint.close();
Logger.log(String.format("[%s]: Stopping the machine as the " +
"Transition Action %s for event %s from %s to %s raised "+
"runtime exception %s:\n%s", f_sName, actionTransition, event,
stateCurrent, stateDesired, e, writerString .toString()), Logger.ALWAYS);
break;
}
}
}
}
}
// now perform exit and entry actions
if (event != null)
{
// notify the lifecycle aware event of the completion of processing
if (event instanceof LifecycleAwareEvent)
{
LifecycleAwareEvent lifecycleAwareEvent = (LifecycleAwareEvent) event;
lifecycleAwareEvent.onProcessed(this);
}
// perform the exit action
if (!fIsInitialState)
{
StateExitAction actionExit = f_mapExitActions.get(stateCurrent);
if (actionExit != null)
{
try
{
actionExit.onExitState(stateCurrent, event, this);
}
catch (RuntimeException e)
{
if (m_fIgnoreExceptions)
{
Logger.warn(String
.format("[%s]: State Exit Action %s for event %s from %s to %s " +
"raised runtime exception (continuing with transition " +
"and ignoring the exception):\n%s", f_sName, actionExit,
event, stateCurrent, stateDesired, e));
}
else
{
m_fAcceptEvents = false;
m_fAllowTransitions = false;
StringWriter writerString = new StringWriter();
PrintWriter writerPrint = new PrintWriter(writerString);
e.printStackTrace(writerPrint);
writerPrint.close();
Logger.err(String
.format("[%s]: Stopping the machine as the State Exit Action %s "+
"for event %s from %s to %s raised runtime exception %s:\n%s",
f_sName, actionExit, event, stateCurrent,
stateDesired, e, writerString.toString()));
break;
}
}
}
else
{
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: No Exit Action defined for %s",
// f_sName, stateCurrent));
}
}
// we're now in the desired state so set it
m_state = stateDesired;
// as we've made a transition, count it
if (!fIsInitialState)
{
f_atomicTransitions.incrementAndGet();
}
// notify the listeners of the transition
for (FiniteStateMachineListener listener : m_setListeners)
{
try
{
listener.onTransition(stateCurrent, stateDesired);
}
catch (RuntimeException e)
{
Logger.warn("Exception occurred in FiniteStateMachineListener", e);
}
}
// the instruction to perform after setting the state
Instruction instruction = Instruction.NOTHING;
// perform the entry action
StateEntryAction actionEntry = f_mapEntryActions.get(stateDesired);
if (actionEntry != null)
{
try
{
// execute the enter state action and determine what to do next
instruction = actionEntry.onEnterState(stateCurrent, stateDesired, event, this);
}
catch (RuntimeException e)
{
if (m_fIgnoreExceptions)
{
Logger.finest(String
.format("[%s]: State Entry Action %s for event %s from %s to %s raised runtime " +
"exception (continuing and ignoring the exception):\n%s",
f_sName, actionEntry, event, stateCurrent, stateDesired, e));
}
else
{
m_fAcceptEvents = false;
m_fAllowTransitions = false;
StringWriter writerString = new StringWriter();
PrintWriter writerPrint = new PrintWriter(writerString);
e.printStackTrace(writerPrint);
writerPrint.close();
Logger.log(String
.format("[%s]: Stopping the machine as the State Entry Action %s for event %s " +
"from %s to %s raised runtime exception %s:\n%s",
f_sName, actionEntry, event, stateCurrent,
stateDesired, e, writerString.toString()), Logger.ALWAYS);
break;
}
}
}
else
{
// uncomment log when need to troubleshoot problem with FSM
// Logger.fine(String.format("[%s]: No Entry Action defined for %s",
// f_sName, stateDesired));
}
// now perform the appropriate instruction based on the entry action
if (instruction == null || instruction == Instruction.NOTHING)
{
// nothing to do for the next instruction
event = null;
}
else if (instruction == Instruction.STOP)
{
// stop the machine immediately (don't wait for scheduled transitions to complete)
stop();
}
else if (instruction instanceof TransitionTo)
{
// when the instruction is to "transition", we execute the transition
// immediately as this prevents the possible race-condition where
// asynchronously scheduled events can become "interleaved"
// between the completion of a state change and a move to another the desired state
TransitionTo eventTransitionTo = (TransitionTo) instruction;
event = prepareEvent(eventTransitionTo);
}
else if (instruction instanceof DelayedTransitionTo)
{
DelayedTransitionTo eventDelayedTransitionTo = (DelayedTransitionTo) instruction;
// schedule the transition event to be processed (and prepared) in the future
processLater(eventDelayedTransitionTo,
eventDelayedTransitionTo.getDuration(),
eventDelayedTransitionTo.getTimeUnit());
event = null;
}
else if (instruction instanceof ProcessEvent)
{
ProcessEvent eventDelegating = (ProcessEvent) instruction;
event = prepareEvent(eventDelegating.getEvent());
}
else if (instruction instanceof ProcessEventLater)
{
ProcessEventLater eventDelayedInstruction = (ProcessEventLater) instruction;
// schedule the event to be processed (and prepared) in the future
processLater(eventDelayedInstruction.getEvent(),
eventDelayedInstruction.getDuration(),
eventDelayedInstruction.getTimeUnit());
event = null;
}
else
{
Logger.warn(String.format("[%s]: Ignoring Instruction [%s] returned as part of "
+ "transition to %s as it an unknown type for this Finite State Machine.",
f_sName, instruction, stateDesired));
}
}
}
GuardSupport.heartbeat();
}
// when this is the last pending transition and we're not accepting any more,
// notify waiting threads that we're done
if (!m_fAcceptEvents && f_atomicPendingEvents.get() == 0)
{
synchronized(this)
{
notifyAll();
}
}
}
// ----- inner class CoalescedEvent -------------------------------------
/**
* A {@link CoalescedEvent} is a {@link LifecycleAwareEvent} that
* coalesces other (wrapped) {@link Event}s with the same discriminator
* so that only one {@link Event} actually executes.
*
* For example: Given 10 {@link Event}s submitted to a {@link
* NonBlockingFiniteStateMachine} with the same discriminator, only one
* of the said {@link Event}s will be processed. All others will be
* discarded. Once the {@link CoalescedEvent} has been processed, a new
* batch may be created when another {@link CoalescedEvent} of the same
* discriminator is submitted.
*
* The actual {@link Event} processed depends on the mode of coalescing
* required. The first {@link CoalescedEvent} submitted to a {@link
* NonBlockingFiniteStateMachine} for a specific discriminator
* effectively starts the coalescing of {@link Event}s for the said
* discriminator. When the mode is set to {@link CoalescedEvent.Process#FIRST}, then
* the first {@link Event} (starting the coalescing) will be processed
* and others will be discarded. When the mode is set of {@link
* CoalescedEvent.Process#MOST_RECENT} then the most recently submitted {@link Event}
* will be processed and likewise, all others for the same discriminator
* will be discarded.
*
* @param the type of the state for the {@link FiniteStateMachine}
*/
public static class CoalescedEvent>
implements LifecycleAwareEvent
{
// ----- constructors -----------------------------------------------
/**
* Constructs a {@link CoalescedEvent} of the specified {@link Event}
* type using {@link CoalescedEvent.Process#FIRST}.
*
* @param event the {@link Event} to be executed when coalesced
*/
public CoalescedEvent(Event event)
{
this(event, Process.FIRST, event.getClass());
}
/**
* Constructs a {@link CoalescedEvent} of the specified {@link Event} type.
*
* @param event the {@link Event} to be coalesced
* @param mode which {@link CoalescedEvent}s to process
*/
public CoalescedEvent(Event event, Process mode)
{
this(event, mode, event.getClass());
}
/**
* Constructs a {@link CoalescedEvent} with the specified discriminator and {@link Event}.
*
* @param event the {@link Event} to be coalesced
* @param mode which {@link CoalescedEvent}s to process
* @param discriminator the descriminator used to uniquely coalesce
* the {@link Event}
*/
public CoalescedEvent(Event event, Process mode, Object discriminator)
{
m_oDiscriminator = discriminator == null ? Void.class : discriminator;
m_event = event;
m_mode = mode;
m_eventChosen = null;
}
// ----- Event interface --------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public S getDesiredState(S state, ExecutionContext context)
{
return m_eventChosen.getDesiredState(state, context);
}
// ----- LifecycleAwareEvent interface ------------------------------
/**
* {@inheritDoc}
*/
@SuppressWarnings("rawtypes")
@Override
public boolean onAccept(ExecutionContext context)
{
Event event = m_event;
// CoalescingEvents may only be accepted by NonBlockingFiniteStateMachines
if (context instanceof NonBlockingFiniteStateMachine)
{
// ensure that the actual event is accepted
// (there's no reason to accept unacceptable events)
boolean fIsAccepted = event instanceof LifecycleAwareEvent
? ((LifecycleAwareEvent) event).onAccept(context)
: true;
// replace the provided discriminator with one that is scoped
// by the NonBlockingFiniteStateMachine;
Discriminator discriminator = new Discriminator((NonBlockingFiniteStateMachine) context,
m_oDiscriminator);
m_oDiscriminator = discriminator;
if (fIsAccepted)
{
fIsAccepted = m_mode == Process.FIRST
? s_mapEventsByDiscriminator.putIfAbsent(discriminator, event) == null
: s_mapEventsByDiscriminator.put(discriminator, event) == null;
}
return fIsAccepted;
}
else
{
throw new UnsupportedOperationException(String.format(
"CoalescingEvents may only be used with %s instance",
NonBlockingFiniteStateMachine.class.getName()));
}
}
/**
* {@inheritDoc}
*/
@Override
public void onProcessed(ExecutionContext context)
{
if (m_eventChosen instanceof LifecycleAwareEvent)
{
((LifecycleAwareEvent) m_eventChosen).onProcessed(context);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onProcessing(ExecutionContext context)
{
// remove the actual event to be processed for the discriminator
// (we do this because this event that we're processing may have
// been replaced ie: coalesced by another event)
Event event = m_eventChosen = (Event) s_mapEventsByDiscriminator.remove(m_oDiscriminator);
if (event instanceof LifecycleAwareEvent)
{
((LifecycleAwareEvent) event).onProcessing(context);
}
}
// ----- Object methods ---------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return String.format("CoalescedEvent{%s, discriminator=%s, mode=%s}",
m_event, m_oDiscriminator, m_mode);
}
// ----- inner class Discriminator ----------------------------------
/**
* A {@link Discriminator} is an object that is used to uniquely
* differentiate events to be coalesced, scoped by a {@link
* NonBlockingFiniteStateMachine}.
*/
public static class Discriminator
{
/**
* Constructs a {@link Discriminator} for the specified {@link
* NonBlockingFiniteStateMachine}.
*
* @param machine the {@link NonBlockingFiniteStateMachine}
* @param oDiscriminator the discriminator
*/
public Discriminator(NonBlockingFiniteStateMachine> machine, Object oDiscriminator)
{
m_machine = machine;
m_oDiscriminator = oDiscriminator;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((m_oDiscriminator == null) ? 0 : m_oDiscriminator.hashCode());
result = prime * result + ((m_machine == null) ? 0 : m_machine.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
Discriminator other = (Discriminator) obj;
if (m_oDiscriminator == null)
{
if (other.m_oDiscriminator != null)
{
return false;
}
}
else if (!m_oDiscriminator.equals(other.m_oDiscriminator))
{
return false;
}
if (m_machine == null)
{
if (other.m_machine != null)
{
return false;
}
}
else if (!m_machine.equals(other.m_machine))
{
return false;
}
return true;
}
/**
* The {@link NonBlockingFiniteStateMachine} to which the
* discriminator applies.
*/
private NonBlockingFiniteStateMachine> m_machine;
/**
* The actual discriminator (not null).
*/
private Object m_oDiscriminator;
}
/**
* Initialization of shared state.
*/
static
{
s_mapEventsByDiscriminator = new ConcurrentHashMap>();
}
// ----- inner enum Process -----------------------------------------
/**
* The {@link CoalescedEvent} to process.
*/
public static enum Process
{
/**
* FIRST indicates that the first submitted {@link Event}
* for a specific discriminator will be the one which is
* processed. All other submitted {@link CoalescedEvent}s of the
* same discriminator will be discarded.
*/
FIRST,
/**
* MOST_RECENT indicates that the most recently
* submitted {@link Event} for a specified discriminator will be
* processed. All other previously submitted {@link Event}s of
* the same discriminator will be discarded.
*/
MOST_RECENT
}
// ----- data members -----------------------------------------------
/**
* The {@link Event}s to be processed, arranged by discriminator.
*/
private static ConcurrentHashMap> s_mapEventsByDiscriminator;
/**
* The discriminator/identifier that is used to coalesce {@link
* Event}s of the same "type".
*/
private Object m_oDiscriminator;
/**
* The {@link Event} to be coalesced.
*/
private Event m_event;
/**
* The mode of coalescing to use for the {@link Event}.
*/
private Process m_mode;
/**
* The {@link Event} that is eventually chosen to process (from all
* of those submitted and coalesced).
*/
private Event m_eventChosen;
}
// ----- inner class DelayedTransitionTo --------------------------------
/**
* A {@link DelayedTransitionTo} is a specialized {@link Instruction} for
* {@link NonBlockingFiniteStateMachine}s that enables a {@link
* StateEntryAction} to request a delayed transition to another state,
* unlike a {@link TransitionTo} {@link Instruction} which occurs
* immediately.
*
* @see TransitionTo
*/
public static class DelayedTransitionTo>
implements Instruction, Event
{
// ----- constructors -----------------------------------------------
/**
* Constructs a {@link DelayedTransitionTo} without a specified time
* (to be schedule as soon as possible).
*
* @param desiredState the desired state to which to transition
*/
public DelayedTransitionTo(S desiredState)
{
this(desiredState, 0, TimeUnit.MILLISECONDS);
}
/**
* Constructs a {@link DelayedTransitionTo} with the specified time.
*
* @param desiredState the desired state to which to transition
* @param lDuration the amount of time to wait before the desired
* transition should occur
* @param timeUnit the unit of time measure
*/
public DelayedTransitionTo(S desiredState, long lDuration, TimeUnit timeUnit)
{
m_desiredState = desiredState;
m_lDuration = lDuration;
m_timeUnit = timeUnit;
}
// ----- Event interface --------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public S getDesiredState(S currentState, ExecutionContext context)
{
return m_desiredState;
}
// ----- DelayedTransitionTo methods --------------------------------
/**
* Obtains the amount of time to wait before the transition to the
* desired state should occur
*
* @return the amount of time in the {@link #getTimeUnit()}
*/
public long getDuration()
{
return m_lDuration;
}
/**
* Obtains the {@link TimeUnit} for the {@link #getDuration()}
*
* @return the {@link TimeUnit}
*/
public TimeUnit getTimeUnit()
{
return m_timeUnit;
}
// ----- data members -----------------------------------------------
/**
* The desired state.
*/
private S m_desiredState;
/**
* The amount of time to wait before the transition should occur.
*/
private long m_lDuration;
/**
* The {@link TimeUnit} for the delay time.
*/
private TimeUnit m_timeUnit;
}
// ----- inner class ProcessEventLater ----------------------------------
/**
* A specialized {@link Instruction} for {@link NonBlockingFiniteStateMachine}s
* that enables a {@link StateEntryAction} to request an {@link Event} to
* be processed at some point in the future.
*
* This is the same as calling {@link NonBlockingFiniteStateMachine#processLater(Event,
* long, TimeUnit)}
*
* @see ProcessEvent
*/
public static class ProcessEventLater>
implements Instruction
{
// ----- constructors -----------------------------------------------
/**
* Constructs a {@link ProcessEventLater} without a specified time
* (to be schedule as soon as possible).
*
* @param event the {@link Event} to process later
*/
public ProcessEventLater(Event event)
{
this(event, 0, TimeUnit.MILLISECONDS);
}
/**
* Constructs a {@link ProcessEventLater} with the specified delay time.
*
* @param event the {@link Event} to process later
* @param duration the amount of time to wait before processing the {@link Event}
* @param timeUnit the unit of time measure
*/
public ProcessEventLater(Event event, long duration, TimeUnit timeUnit)
{
m_event = event;
m_lDuration = duration;
m_timeUnit = timeUnit;
}
// ----- ProcessEventLater methods ----------------------------------
/**
* Obtain the {@link Event} to process later.
*
* @return the {@link Event} to process
*/
public Event getEvent()
{
return m_event;
}
/**
* Obtains the amount of time to wait before the transition to the
* desired state should occur.
*
* @return the amount of time in the {@link #getTimeUnit()}
*/
public long getDuration()
{
return m_lDuration;
}
/**
* Obtains the {@link TimeUnit} for the {@link #getDuration()}.
*
* @return the {@link TimeUnit}
*/
public TimeUnit getTimeUnit()
{
return m_timeUnit;
}
// ----- data members -----------------------------------------------
/**
* The {@link Event} to process later.
*/
private Event m_event;
/**
* The amount of time to wait before the processing the {@link Event}.
*/
private long m_lDuration;
/**
* The {@link TimeUnit} for the delay time.
*/
private TimeUnit m_timeUnit;
}
// ----- inner class SubsequentEvent ------------------------------------
/**
* A {@link SubsequentEvent} is an {@link Event} that ensures that
* another (wrapped) {@link Event} to occur if an only if the {@link
* FiniteStateMachine} is at a certain transition count. Should an
* attempt to process the wrapped {@link Event} occur at another
* transition count, processing of the said event is ignored.
*
* {@link SubsequentEvent}s are designed to provide the ability for
* future scheduled {@link Event}s to be skipped if another {@link Event}
* has been processed between the time when the {@link SubsequentEvent}
* was requested to be processed and when it was actually processed. That
* is, the purpose of this is to allow an {@link Event} to be skipped if
* other {@link Event}s interleave between the time when the said {@link
* Event} was actually scheduled and when it was actually meant to be
* processed.
*
* @param the state type of the {@link FiniteStateMachine}
*/
public static class SubsequentEvent>
implements LifecycleAwareEvent
{
// ----- constructors -----------------------------------------------
/**
* Constructs a {@link SubsequentEvent}.
*
* @param event the actual event to process
*/
public SubsequentEvent(Event event)
{
m_cTransitions = -1;
m_event = event;
}
// ----- LifecycleAwareEvent interface ------------------------------
/**
* {@inheritDoc}
*/
@Override
public boolean onAccept(ExecutionContext context)
{
// when being accepted use context to determine the transition count
// at which the event should be processed
m_cTransitions = context.getTransitionCount();
// ensure the event can be accepted (if it's a lifecycle aware event)
// otherwise always accept it
return m_event instanceof LifecycleAwareEvent ?
((LifecycleAwareEvent) m_event).onAccept(context) :
true;
}
/**
* {@inheritDoc}
*/
@Override
public void onProcessed(ExecutionContext context)
{
if (m_event instanceof LifecycleAwareEvent)
{
((LifecycleAwareEvent) m_event).onProcessed(context);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onProcessing(ExecutionContext context)
{
if (m_event instanceof LifecycleAwareEvent)
{
((LifecycleAwareEvent) m_event).onProcessing(context);
}
}
// ----- Event interface --------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public S getDesiredState(S currentState, ExecutionContext context)
{
if (context.getTransitionCount() == m_cTransitions)
{
return m_event.getDesiredState(currentState, context);
}
else
{
// uncomment log when need to troubleshoot problem with FSM
// Logger.finest(String.format("[%s]: Skipping event %s since another event " +
// "was interleaved between when it was scheduled and when it was processed",
// context.getName(), this));
// by returning null we skip the processing of the event
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return String.format("SubsequentEvent{%s, @Transition #%d}", m_event,
m_cTransitions + 1);
}
// ----- data members -----------------------------------------------
/**
* The transition count that the {@link FiniteStateMachine} must be
* at in order for the wrapped {@link Event} to be processed.
*/
private long m_cTransitions;
/**
* The actual {@link Event}.
*/
private Event m_event;
}
// ----- data members ---------------------------------------------------
/**
* The dependencies used to configure event processing Task.
*/
private final TaskDependencies f_dependencies;
/**
* The name of the {@link NonBlockingFiniteStateMachine}.
*/
private final String f_sName;
/**
* The state of the {@link FiniteStateMachine}.
*/
private volatile S m_state;
/**
* The initial state.
*/
private final S m_stateInitial;
/**
* The {@link Transition} table (by starting and ending states).
*/
private final EnumMap>> f_mapTransitions;
/**
* The {@link StateEntryAction} table (by state).
*/
private final EnumMap> f_mapEntryActions;
/**
* The {@link StateExitAction} table (by state).
*/
private final EnumMap> f_mapExitActions;
/**
* The number of transitions that have occurred in the {@link FiniteStateMachine}.
*/
private final AtomicLong f_atomicTransitions;
/**
* Is the {@link FiniteStateMachine} accepting {@link Event}s to trigger
* {@link Transition}s?
*
* This flag allows us to stop the {@link FiniteStateMachine} from
* accepting {@link Event}s (that may cause {@link Transition}s), but
* allows the {@link FiniteStateMachine} to continue processing
* previously accepted {@link Event}s.
*/
private volatile boolean m_fAcceptEvents;
/**
* True if the FiniteStateMachine has been started.
*/
private volatile boolean m_fStarted;
/**
* Is the {@link FiniteStateMachine} allowed to perform {@link
* Transition}s?
*
* This flag determines if the {@link FiniteStateMachine} is operational.
* Once it can no longer perform {@link Transition}s, the {@link
* FiniteStateMachine} is "dead" and can no longer be used.
*/
private volatile boolean m_fAllowTransitions;
/**
* The number of pending, ie: queued, {@link Event}s to be processed.
*/
private final AtomicLong f_atomicPendingEvents;
/**
* A {@link ScheduledExecutorService} that will be used to schedule
* {@link Transition}s for the {@link FiniteStateMachine}.
*
* Note: Only threads on this {@link ScheduledExecutorService} may apply
* a {@link Transition}.
*/
private final DaemonPool f_daemonPool;
/**
* When true
{@link RuntimeException}s be ignored (will not
* stop the {@link FiniteStateMachine}).
*
* When false
{@link RuntimeException}s be will immediately
* stop the {@link FiniteStateMachine}.
*/
private final boolean m_fIgnoreExceptions;
/**
* The set of {@link FiniteStateMachineListener}s.
*/
private final Set> m_setListeners;
}