All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.hgross.blaubot.core.statemachine.ConnectionStateMachine Maven / Gradle / Ivy

Go to download

An easy to use publish/subscribe middleware to create and communicate through dynamically created adhoc networks.

There is a newer version: 2.0.0-beta.2
Show newest version
package eu.hgross.blaubot.core.statemachine;

import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;

import eu.hgross.blaubot.admin.AbstractAdminMessage;
import eu.hgross.blaubot.admin.RelayAdminMessage;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotConnectionManager;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.ServerConnectionManager;
import eu.hgross.blaubot.core.State;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionManagerListener;
import eu.hgross.blaubot.core.acceptor.discovery.BlaubotBeaconService;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractTimeoutStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.AdminMessageStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.ConnectionClosedStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.ConnectionEstablishedStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.StartStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.StopStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.states.FreeState;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.core.statemachine.states.StoppedState;
import eu.hgross.blaubot.messaging.IBlaubotAdminMessageListener;
import eu.hgross.blaubot.util.Log;

/**
 * Statemachine for the network creation. Simply delegates the events from the {@link BlaubotConnectionManager} and the
 * {@link AbstractBlaubotDeviceDiscoveryEvent}s from the beacons to the current state. The state will handle the event
 * based state changes by themselves (calling nextState).
 * 
 * @author Henning Gross 
 * 
 */
public class ConnectionStateMachine {
	private static final String LOG_TAG = "ConnectionStateMachine";
	protected final Blaubot blaubot;
	private final List connectionStateMachineListeners;
	private final List beacons;
	private final List acceptors;
	private final List adapters;
	private final BlaubotBeaconService beaconService;
	
	private final BlockingQueue stateMachineEventQueue;
	private final StateMachineSession stateMachineSession;
	private StateMachineEventDispatcher stateMachineEventDispatcher;
	protected IBlaubotState currentState;

    /**
     * Create the connection state machine for a blaubot instance.
     *
     * @param ownDevice
     * @param connectionManager the connection manager managing the blaubot acceptors
     * @param adapters the adapters
     * @param blaubot the blaubot instance
     */
	public ConnectionStateMachine(IBlaubotDevice ownDevice, final BlaubotConnectionManager connectionManager, final List adapters, List beacons, final Blaubot blaubot, ServerConnectionManager serverConnectionManager) {
		this.blaubot = blaubot;
		this.adapters = adapters;
		this.stateMachineEventQueue = new LinkedBlockingQueue();
		this.beacons = beacons;
		this.acceptors = BlaubotAdapterHelper.getConnectionAcceptors(adapters);
		
		this.beaconService = new BlaubotBeaconService(ownDevice, beacons, acceptors, this);
		this.connectionStateMachineListeners = new CopyOnWriteArrayList<>();


        // connect to admin messages
        this.blaubot.getChannelManager().addAdminMessageListener(adminMessageChannelListener);

		connectionManager.addConnectionListener(connectionListener);
		this.stateMachineSession = new StateMachineSession(this, ownDevice, serverConnectionManager);
		this.currentState = new StoppedState();
	}


	private final Object eventDispatcherLock = new Object();

    /**
     * Starts the CSM's event dispatcher. Mandatory for the CSM to do anything.
     */
	public void startEventDispatcher() {
		synchronized (eventDispatcherLock) {
			if(isEventDispatcherRunning()) {
				if(Log.logDebugMessages()) {
					Log.d(LOG_TAG, "EventDispatcher already running - ignoring startEventDispatcher()");
				}
				return;
			}
			stateMachineEventDispatcher = new StateMachineEventDispatcher();
			stateMachineEventDispatcher.start();
		}
	}
	
	public void stopEventDispatcher() {
		synchronized (eventDispatcherLock) {
			if(stateMachineEventDispatcher != null) {
				stateMachineEventDispatcher.interrupt();
				stateMachineEventDispatcher = null;
			}
		}
	}

    /**
     * Starts the state machine by pushing a start event to the CSM's event queue.
     */
	public void startStateMachine() {
		pushStateMachineEvent(new StartStateMachineEvent(currentState));
	}

    /**
     * Actually handles the start of the CSM
     */
	private void _startStateMachine() {
		if (!(currentState instanceof StoppedState)) {
			if(Log.logWarningMessages()) {
				Log.w(LOG_TAG, "ConnectionStateMachine already started. Ignoring _startStateMachine()");
			}
			return; // already started
		}
		StoppedState stoppedState = (StoppedState) this.currentState;
		stoppedState.handleState(this.stateMachineSession);
		changeState(new FreeState());
	}

    /**
     * Requests a stop of the CSM by pushing an event to the CSM's event queue.
     */
	public void stopStateMachine() {
		pushStateMachineEvent(new StopStateMachineEvent(currentState));
	}

    /**
     * Actually handles the stop of the state machine
     */
	private void _stopStateMachine() {
		this.changeState(new StoppedState());
	}

	/**
	 * changes state
	 *  
	 * @param newState
	 */
	private void changeState(IBlaubotState newState) {
		if(newState == currentState)
			return; // do nothing if same state
        IBlaubotState oldState = currentState;
		if(Log.logDebugMessages()) {
			Log.d(LOG_TAG, "[Current state: " + currentState + "] Changing to state " + newState);
		}
		assertStateChange(currentState, newState);
		boolean sendStarted = false;
		boolean sendStopped = false;
		if (currentState instanceof StoppedState &&  !(newState instanceof StoppedState)) {
			sendStarted = true;
		} else if (newState instanceof StoppedState && !(currentState instanceof StoppedState)) {
			sendStopped = true;
		}
		currentState = newState;

        // let the beacons signal the new state, if not a StoppedState, which would make no sense at all
        // inform the beacon service
        beaconService.onStateChanged(currentState);
        if (!(newState instanceof StoppedState)) {
			for (IBlaubotBeacon beacon : beacons) {
				beacon.onConnectionStateMachineStateChanged(newState);
			}
		}

        if (Log.logDebugMessages()) {
            Log.d(LOG_TAG, "Handling state " + currentState.getClass().getSimpleName() + " ...");
        }
		currentState.handleState(stateMachineSession);
        if (Log.logDebugMessages()) {
            Log.d(LOG_TAG, "State " + currentState.getClass().getSimpleName() + " handled.");
        }

		if (Log.logDebugMessages()) {
			Log.d(LOG_TAG, "Notifying ConnectionStateMachineListeners onStateChanged() ...");
		}
		// inform the listeners
		for (IBlaubotConnectionStateMachineListener connectionStateMachineListener : this.connectionStateMachineListeners) {
			if (Log.logDebugMessages()) {
				Log.d(LOG_TAG, "Notifying listener " + connectionStateMachineListener);
			}
			if (sendStarted)
				connectionStateMachineListener.onStateMachineStarted();
			if (sendStopped)
				connectionStateMachineListener.onStateMachineStopped();
			connectionStateMachineListener.onStateChanged(oldState, newState);
			if (Log.logDebugMessages()) {
				Log.d(LOG_TAG, "Done notifying listener " + connectionStateMachineListener);
			}
		}
		if (Log.logDebugMessages()) {
			Log.d(LOG_TAG, "Done notifying onStateChanged() ...");
		}
	}
	
	/**
	 * @param currentState
	 * @param nextState
	 */
	private static void assertStateChange(IBlaubotState currentState, IBlaubotState nextState) {
		State cur = State.getStateByStatemachineClass(currentState.getClass());
		State to = State.getStateByStatemachineClass(nextState.getClass());
		if(!cur.isStateChangeAllowed(to)) {
			throw new IllegalStateException("A state change from " + cur + " to " + to + " is not allowed.");
		}
	}

    /**
     * Registers a listener to get informed about state changes of the state machine
     * @param connectionStateMachineListener the listener to add
     */
	public void addConnectionStateMachineListener(IBlaubotConnectionStateMachineListener connectionStateMachineListener) {
		this.connectionStateMachineListeners.add(connectionStateMachineListener);
	}

    /**
     * Removes a previously added listener
     * @param connectionStateMachineListener the listener to remove
     */
    public void removeConnectionStateMachineListener(IBlaubotConnectionStateMachineListener connectionStateMachineListener) {
		this.connectionStateMachineListeners.remove(connectionStateMachineListener);
	}

	public List getConnectionAcceptors() {
		return acceptors;
	}

	public BlaubotBeaconService getBeaconService() {
		return beaconService;
	}


	private IBlaubotConnectionManagerListener connectionListener = new IBlaubotConnectionManagerListener() {
		@Override
		public void onConnectionClosed(IBlaubotConnection connection) {
			if(Log.logDebugMessages()) {
				Log.d(LOG_TAG, "[Current state: " + currentState + "] Connection down: " + connection + ". Pushing as CSM event to queue.");
			}
			ConnectionClosedStateMachineEvent event = new ConnectionClosedStateMachineEvent(connection);
			event.setConnectionStateMachineState(currentState);
			pushStateMachineEvent(event);
		}

		@Override
		public void onConnectionEstablished(IBlaubotConnection connection) {
			if(Log.logDebugMessages()) {
				Log.d(LOG_TAG, "[Current state: " + currentState + "] Got new connection: " + connection + ". Pushing as CSM event to queue.");
			}
			ConnectionEstablishedStateMachineEvent event = new ConnectionEstablishedStateMachineEvent(connection);
			event.setConnectionStateMachineState(currentState);
			pushStateMachineEvent(event);
		}
	};

	private IBlaubotAdminMessageListener adminMessageChannelListener = new IBlaubotAdminMessageListener() {
		@Override
		public void onAdminMessage(AbstractAdminMessage adminMessage) {
			if (adminMessage instanceof RelayAdminMessage) {
				// don't process relay messages
				return;
			}
			if (Log.logDebugMessages()) {
				Log.d(LOG_TAG, "[Current state: " + currentState + "] Got admin message: " + adminMessage + ". Pushing as CSM event to queue.");
			}
			pushStateMachineEvent(new AdminMessageStateMachineEvent(currentState, adminMessage));
		}
	};
	

	public IBlaubotState getCurrentState() {
		return this.currentState;
	}
	
	public boolean isStateMachineStarted() {
		return !(currentState instanceof StoppedState);
	}
	
	private boolean isEventDispatcherRunning() {
		synchronized (eventDispatcherLock) {
			return this.stateMachineEventDispatcher != null && this.stateMachineEventDispatcher.isAlive();
		}
	}

	/**
	 * Dispatches events from the eventQueue to the current state. (UI-Thread alike)
	 *
	 * @author Henning Gross 
	 * 
	 */
	class StateMachineEventDispatcher extends Thread {
		private static final String LOG_TAG = "StateMachineEventDispatcher";
		/**
		 * Max time the processing of an event may take. If it takes longer,
		 * an exception will be thrown.
		 */
		private static final int MAX_EVENT_PROCESSING_TIME = 60000; // ms
		private Timer processingTimeoutTimer;

		public StateMachineEventDispatcher() {
			setName("csm-event-dispatcher");
		}
		private void handleState(IBlaubotState state) {
			changeState(state);
		}

		/**
		 * Starts a timer that will log warnings, if the ConnectionStateMachine takes too long 
		 * to process an event.
         * @param event the event that took too long to be processed
		 */
		private void startTimer(final AbstractBlaubotStateMachineEvent event) {
			processingTimeoutTimer = new Timer();
			final TimerTask timerTask = new TimerTask() {
				@Override
				public void run() {
                    final String message = " [curState: " + currentState + "] The processing of " + event + " took longer than " + MAX_EVENT_PROCESSING_TIME + " ms";
                    if (Log.logWarningMessages()) {
                        Log.e(LOG_TAG, message);
                    }
//                    throw new RuntimeException(new TimeoutException(message));
				}
			};
			processingTimeoutTimer.schedule(timerTask, MAX_EVENT_PROCESSING_TIME);
		}

		/**
		 * cancels the timer
		 */
		private void cancelTimer() {
			if (processingTimeoutTimer != null) {
				processingTimeoutTimer.cancel();
				processingTimeoutTimer = null;
			}
		}

		@Override
		public void run() {
            if(Log.logDebugMessages()) {
                Log.d(LOG_TAG, "StateMachineEventDispatcher started.");
            }
			while(!isInterrupted() && Thread.currentThread() == stateMachineEventDispatcher) {
				try {
					AbstractBlaubotStateMachineEvent event = stateMachineEventQueue.take();
					if(Log.logDebugMessages()) {
						Log.d(LOG_TAG, "[curState: "+ currentState +"] CSM EventQueue (size=" + stateMachineEventQueue.size() + ") took: " + event);
					}
                    // start the timeout timer
                    startTimer(event);
                    
                    // we measure the processing time
                    long startTime = System.currentTimeMillis();
					if(event instanceof AdminMessageStateMachineEvent) {
						AbstractAdminMessage aam = ((AdminMessageStateMachineEvent)event).getAdminMessage();
						IBlaubotState state = currentState.onAdminMessage(aam);
						handleState(state);
					} else if(event instanceof ConnectionClosedStateMachineEvent) {
						IBlaubotState state = currentState.onConnectionClosed(((ConnectionClosedStateMachineEvent) event).getConnection());
						handleState(state);
					} else if(event instanceof ConnectionEstablishedStateMachineEvent) {
						IBlaubotState state = currentState.onConnectionEstablished(((ConnectionEstablishedStateMachineEvent) event).getConnection());
						handleState(state);
					} else if(event instanceof AbstractBlaubotDeviceDiscoveryEvent) {
						// filter discovery events that discover ourselves
						if(!stateMachineSession.getOwnDevice().getUniqueDeviceID().equals(((AbstractBlaubotDeviceDiscoveryEvent) event).getRemoteDevice().getUniqueDeviceID())) {
							IBlaubotState state = currentState.onDeviceDiscoveryEvent((AbstractBlaubotDeviceDiscoveryEvent) event);
							handleState(state);
						}
					} else if (event instanceof AbstractTimeoutStateMachineEvent) {
						IBlaubotState state = currentState.onTimeoutEvent((AbstractTimeoutStateMachineEvent) event);
						handleState(state);
					} else if(event instanceof StopStateMachineEvent) {
						_stopStateMachine();
					} else if(event instanceof StartStateMachineEvent) {
						_startStateMachine();
					} else {
						throw new RuntimeException("Unknown event in event queue!");
					}
                    // stop timeout timer
                    cancelTimer();
                    
                    if(Log.logDebugMessages()) {
                        Log.d(LOG_TAG, "Event processing took " + (System.currentTimeMillis() - startTime) + " ms");
                    }
				} catch (InterruptedException e) {
					break;
				}
			}
            cancelTimer();
            if(Log.logDebugMessages()) {
                Log.d(LOG_TAG, "StateMachineEventDispatcher stopped.");
            }
		}

	}

    /**
     * Pushes a AbstractBlaubotStateMachineEvent to the StateMachine's event queue.
     * @param stateMachineEvent the event
     */
	public void pushStateMachineEvent(AbstractBlaubotStateMachineEvent stateMachineEvent) {
		try {
			stateMachineEventQueue.put(stateMachineEvent);
		} catch (InterruptedException e) {
			// ignore
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy