package eu.hgross.blaubot.core.statemachine.states;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import eu.hgross.blaubot.core.BlaubotConnectionManager;
import eu.hgross.blaubot.core.BlaubotKingdomConnection;
import eu.hgross.blaubot.core.ConnectionStateMachineConfig;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.State;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.StateMachineSession;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractTimeoutStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.DiscoveredKingEvent;
import eu.hgross.blaubot.core.statemachine.events.KingTimeoutEvent;
import eu.hgross.blaubot.core.statemachine.events.PronouncedPrinceACKTimeoutStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.states.PeasantState.ConnectionAccomplishmentType;
import eu.hgross.blaubot.admin.ACKPronouncePrinceAdminMessage;
import eu.hgross.blaubot.admin.AbstractAdminMessage;
import eu.hgross.blaubot.admin.BowDownToNewKingAdminMessage;
import eu.hgross.blaubot.admin.CensusMessage;
import eu.hgross.blaubot.admin.PronouncePrinceAdminMessage;
import eu.hgross.blaubot.messaging.BlaubotMessage;
import eu.hgross.blaubot.util.Log;
/**
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
public class KingState implements IBlaubotState {
private static final String LOG_TAG = "KingState";
private Timer noConnectionsTimer;
private boolean connectingToAnotherKing = false; // TODO: i think this is
// now usesless ->
// validate
private Object timerTaskMonitor = new Object();
private StateMachineSession session;
private String currentPrinceUniqueId = null;
private PrinceWatcher princeWatcher;
/**
* This listener will be called whenever we get a {@link IBlaubotConnection}
* in THIS {@link KingState}.
*/
private IBlaubotIncomingConnectionListener peasantConnectionsListener;
/**
* Manages the pronounce prince and ACKPrince cycle. If a connection gets
* lost or is established, we pronounce a prince and
* {@link PronouncePrinceAdminMessage} as well as a {@link CensusMessage} is
* sent to all clients. The prince will then answer with a
* {@link ACKPronouncePrinceAdminMessage}. If there is no answer from the
* prince for a given time span, we pronounce the prince again.
*
*
* We use a little ACK-Protocol to pronounce the prince.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class PrinceWatcher {
private static final String LOG_TAG = "PrinceWatcher";
private PronouncePrinceAdminMessage lastPronouncedPrinceMessage;
private Timer currentTimer;
/**
* needs to be called if an {@link ACKPronouncePrinceAdminMessage}
* message arrives.
*
* @param ackMessage
*/
synchronized void onAck(ACKPronouncePrinceAdminMessage ackMessage) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got ACK from prince");
}
// if we get an ACK from the wrong prince, we assume he knows he was
// to late
if (lastPronouncedPrinceMessage == null || !lastPronouncedPrinceMessage.getUniqueDeviceId().equals(ackMessage.getUniqueDeviceId())) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "ACK is from invalid prince, ignoring");
}
return;
}
// -- got ack from the prince, install him
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Prince ACK is valid - intalling prince and sending CensusMessage.");
}
if (currentTimer != null) {
currentTimer.cancel();
currentTimer = null;
}
currentPrinceUniqueId = ackMessage.getUniqueDeviceId();
sendCencusMessage();
}
/**
* need to be called if a {@link PronouncePrinceAdminMessage} was sent
*
* @param pronounceMessage
* @param ack_timeout
* the timeout after which a
* {@link PronouncedPrinceACKTimeoutStateMachineEvent} is
* pushed to the queue if no
* {@link ACKPronouncePrinceAdminMessage} arrived from the
* pronounced prince
*/
synchronized void onPronouncedMessageSent(final PronouncePrinceAdminMessage pronounceMessage, final int ack_timeout) {
this.lastPronouncedPrinceMessage = pronounceMessage;
final Timer t = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// if another prince was pronounced in the meantime, we do
// nothing
if (lastPronouncedPrinceMessage != pronounceMessage)
return;
// if the timer got canceled do nothing
if (currentTimer != t)
return;
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Got no ACK from desired prince " + pronounceMessage.getUniqueDeviceId() + " for " + ack_timeout + " ms. Pushing TimeoutEvent to Queue.");
}
// otherwise we push the timeout
PronouncedPrinceACKTimeoutStateMachineEvent ev = new PronouncedPrinceACKTimeoutStateMachineEvent(KingState.this);
session.getConnectionStateMachine().pushStateMachineEvent(ev);
}
};
t.schedule(task, ack_timeout);
this.currentTimer = t;
}
/**
* needs to be called if this state receives an
* {@link PronouncedPrinceACKTimeoutStateMachineEvent}.
*
* @param princeACKTimeout
*/
synchronized void onTimeout(PronouncedPrinceACKTimeoutStateMachineEvent princeACKTimeout) {
// ignore timeouts from other state instances
if (princeACKTimeout.getConnectionStateMachineState() != KingState.this) {
return;
}
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Timeout event for prince pronouncing received: re-pronouncing");
}
currentPrinceUniqueId = null;
pronouncePrince();
}
}
/**
* Builds and sends the cencus message to all connected devices.
*/
private void sendCencusMessage() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Sending cencus message");
}
// the map should contain all connected devices (uniqueIds) and their
// state (Peasant or Prince)
final HashMap<String, State> connectedDevicesStates = new HashMap<String, State>();
// add king device
final IBlaubotDevice ownDevice = session.getOwnDevice();
connectedDevicesStates.put(ownDevice.getUniqueDeviceID(), State.King);
// add peasants and prince
for (IBlaubotConnection conn : session.getConnectionManager().getAllConnections()) {
String uniqueDeviceID = conn.getRemoteDevice().getUniqueDeviceID();
State state = currentPrinceUniqueId != null && uniqueDeviceID.equals(currentPrinceUniqueId) ? State.Prince : State.Peasant;
connectedDevicesStates.put(uniqueDeviceID, state);
}
// create and send the message
final CensusMessage censusMessage = new CensusMessage(connectedDevicesStates);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "CensusMessage: " + censusMessage);
}
session.getChannelManager().broadcastAdminMessage(censusMessage.toBlaubotMessage());
}
/**
* Pronounces a new prince based on the currently connected devices.
*/
private void pronouncePrince() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Pronouncing new prince");
}
// select the new prince
List<IBlaubotConnection> connections = session.getConnectionManager().getAllConnections();
Collections.sort(connections, new Comparator<IBlaubotConnection>() {
@Override
public int compare(IBlaubotConnection o1, IBlaubotConnection o2) {
return o1.getRemoteDevice().compareTo(o2.getRemoteDevice());
}
});
Collections.reverse(connections);
// filter server connection(s)
BlaubotKingdomConnection currentlyUsedServerConnection = session.getServerConnectionManager().getCurrentlyUsedServerConnection();
if(currentlyUsedServerConnection != null) {
ArrayList<IBlaubotConnection> toRemove = new ArrayList<>();
for(IBlaubotConnection connection : connections) {
final String uniqueDeviceID = connection.getRemoteDevice().getUniqueDeviceID();
if (session.isServerUniqueDeviceId(uniqueDeviceID)) {
toRemove.add(connection);
}
}
connections.removeAll(toRemove);
}
// if there is at least one connection, take the one with the highest
// device unique id (first in list)
if (!connections.isEmpty()) {
IBlaubotConnection princeConnection = connections.get(0);
String newPrinceUniqueId = princeConnection.getRemoteDevice().getUniqueDeviceID();
final List<ConnectionMetaDataDTO> lastKnownConnectionMetaData = session.getBeaconService().getBeaconStore().getLastKnownConnectionMetaData(newPrinceUniqueId);
PronouncePrinceAdminMessage princeAdminMessage = new PronouncePrinceAdminMessage(newPrinceUniqueId, lastKnownConnectionMetaData);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "New prince is " + newPrinceUniqueId + ". Sending PronouncePrinceAdminMessage ...");
}
// first we ensure that the prince will receive the message
BlaubotMessage pronouncePrinceBlaubotMessage = princeAdminMessage.toBlaubotMessage();
// we broadcast the message to all (The prince will ignore this, if
// still prince. If not, he will step down and be peasant again)
final ConnectionStateMachineConfig connectionStateMachineConfigForDevice = session.getConnectionStateMachineConfigForDevice(princeConnection.getRemoteDevice());
int pronouncing_ack_timeout = connectionStateMachineConfigForDevice.getPrinceAckTimeout();
session.getChannelManager().publishToAllConnections(pronouncePrinceBlaubotMessage);
this.princeWatcher.onPronouncedMessageSent(princeAdminMessage, pronouncing_ack_timeout);
// currentPrinceUniqueId = newPrinceUniqueId; // currently done by
// princeWatcher
} else {
currentPrinceUniqueId = null;
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Prince could not be pronounced - i have no peasants at all!");
}
}
// send census to all devices
sendCencusMessage();
}
@Override
public IBlaubotState onConnectionEstablished(IBlaubotConnection connection) {
this.noConnectionsTimer.cancel();
pronouncePrince();
synchronized (listenerLock) {
if (this.peasantConnectionsListener != null)
this.peasantConnectionsListener.onConnectionEstablished(connection);
}
return this;
}
@Override
public IBlaubotState onConnectionClosed(IBlaubotConnection connection) {
int connectedDevices = countConnections();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "A connection was lost/closed. We have " + connectedDevices + " connected devices now.");
}
this.pronouncePrince();
if (connectedDevices != 0) {
return this;
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting timeout and awaiting new connections. We will move to FreeState, if we get no new peasants!");
}
createAndStartNewTimer();
return this;
}
/**
* Counts the DIRECT connections to other devices.
* This means all connections minus the connection to the server (if any)
* @return number of connections wihtout server connection
*/
private int countConnections() {
// all connections
int connectedDevicesCount = session.getConnectionManager().getConnectedDevices().size();
// subtract server connection, if any
IBlaubotConnection serverConnection = session.getServerConnectionManager().getCurrentlyUsedServerConnection();
if(serverConnection != null) {
connectedDevicesCount -= 1;
}
return connectedDevicesCount;
}
/**
* Starts the timer for the king timeout (no peasants for some time)
*/
private void createAndStartNewTimer() {
// TODO: using the first adapter available. This must be changed if we
// use multiple adapters in the future
ConnectionStateMachineConfig config = session.getAdapters().get(0).getConnectionStateMachineConfig();
final int TIMEOUT_INTERVAL = config.getKingWithoutPeasantsTimeout();
TimerTask task = new TimerTask() {
@Override
public void run() {
if (session.getConnectionStateMachine().getCurrentState() != KingState.this) {
return;
}
// count connected devices without the server
int connectedDevices = countConnections();
if (connectedDevices > 0 || connectingToAnotherKing) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "King-Timeout ignored: " + connectedDevices + " connected devices. Connecting to another king: " + connectingToAnotherKing + ", connections: " + session.getConnectionManager().getAllConnections());
}
return;
}
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "King-Timeout event posted to event queue (No connected peasants for at least " + TIMEOUT_INTERVAL + " ms)");
}
KingTimeoutEvent timeoutEvent = new KingTimeoutEvent(KingState.this);
session.getConnectionStateMachine().pushStateMachineEvent(timeoutEvent);
}
};
// start new timer
synchronized (timerTaskMonitor) {
if (this.noConnectionsTimer != null)
this.noConnectionsTimer.cancel();
this.noConnectionsTimer = new Timer();
this.noConnectionsTimer.schedule(task, TIMEOUT_INTERVAL);
}
}
@Override
public IBlaubotState onDeviceDiscoveryEvent(AbstractBlaubotDeviceDiscoveryEvent discoveryEvent) {
if (discoveryEvent instanceof DiscoveredKingEvent) {
DiscoveredKingEvent discoveredKingEvent = (DiscoveredKingEvent) discoveryEvent;
IBlaubotDevice remoteDevice = discoveryEvent.getRemoteDevice();
if(session.isOwnDevice(remoteDevice.getUniqueDeviceID())) {
if (Log.logDebugMessages()) {
Log.w(LOG_TAG, "Discovered myself - what a surprise.");
}
return this; // ignore own discovery
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got informed about another King (" + discoveredKingEvent.getRemoteDevice() + ").");
}
if (session.isGreaterThanOurDevice(remoteDevice)) {
// -- we have to join this good looking king!
connectingToAnotherKing = true; // to signal the timers to hold
// still when all the
// connections go down
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Found a greater king than i am :-/ Have to join the new king (" + remoteDevice + ")");
Log.d(LOG_TAG, "Connecting to king " + remoteDevice);
}
// connect to the king using the exponential backoff strategy
IBlaubotConnection conn = session.getConnectionManager().connectToBlaubotDevice(remoteDevice, BlaubotConnectionManager.AUTO_MAX_RETRIES);
boolean connect = conn != null;
if (connect) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connection to new king succeeded.");
}
// first prevent new connections
BlaubotAdapterHelper.stopAcceptors(session.getConnectionStateMachine().getConnectionAcceptors());
// tell the peasants to connect to the other king
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Commanding my peasants to bow down to the new king " + remoteDevice);
}
final List<ConnectionMetaDataDTO> lastKnownConnectionMetaData = session.getBeaconService().getBeaconStore().getLastKnownConnectionMetaData(remoteDevice.getUniqueDeviceID());
final BowDownToNewKingAdminMessage bowDownMessage = new BowDownToNewKingAdminMessage(remoteDevice.getUniqueDeviceID(), lastKnownConnectionMetaData);
// send only to peasants (not our new king) and to the
// prince first
List<IBlaubotConnection> connections = session.getConnectionManager().getAllConnections();
connections.remove(conn);
Collections.sort(connections, new Comparator<IBlaubotConnection>() {
@Override
public int compare(IBlaubotConnection arg0, IBlaubotConnection arg1) {
if (session.getLastCensusMessage() == null)
return 0;
// prince is always bigger
if (session.getLastCensusMessage().getDeviceStates().get(arg1.getRemoteDevice().getUniqueDeviceID()) == State.Prince) {
return -1;
}
return 0;
}
});
Collections.reverse(connections);
for (final IBlaubotConnection c : connections) {
// send bow down message only to remote device
session.getChannelManager().publishToSingleDevice(bowDownMessage.toBlaubotMessage(), c.getRemoteDevice().getUniqueDeviceID());
}
// sleep here for some time and hope the bow down messages
// reaches all peasants in time
// if they haven't disconnected themselves by now, the king
// disconnects them after that.
final ConnectionStateMachineConfig stateMachineConfigForDevice = session.getConnectionStateMachineConfigForDevice(remoteDevice);
int kingdomMergeOldKingBowDownTimeout = stateMachineConfigForDevice.getKingdomMergeOldKingBowDownTimeout();
try {
Thread.sleep(kingdomMergeOldKingBowDownTimeout);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// disconnect ALL connected peasants (but spare our
// connection to the new king)
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Disconnecting all remaining connections (if any).");
}
// first disconnect the prince
IBlaubotConnection princeConnection = null;
for (IBlaubotConnection c : session.getConnectionManager().getAllConnections()) {
if (currentPrinceUniqueId != null && c.getRemoteDevice().getUniqueDeviceID().equals(currentPrinceUniqueId)) {
princeConnection = c;
princeConnection.disconnect();
break;
}
}
// now disconnect the rest
for (IBlaubotConnection c : session.getConnectionManager().getAllConnections()) {
// do not terminate the new connection (to the new king) and the (previously disconnected) prince connection
if (c != conn && c != princeConnection) {
c.disconnect();
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Will now transition to PeasantState (BOWED_DOWN).");
}
return new PeasantState(conn, ConnectionAccomplishmentType.BOWED_DOWN);
} else {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connection to new king failed.");
}
connectingToAnotherKing = false;
createAndStartNewTimer(); // will go to free, if there are
// no connections
}
} else {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "I am the greater king, not bowing down.");
}
// we are the awesomeness in person - the other king has to join
// us
}
}
return this;
}
@Override
public void handleState(StateMachineSession session) {
this.session = session;
this.princeWatcher = new PrinceWatcher();
BlaubotAdapterHelper.startAcceptors(session.getConnectionStateMachine().getConnectionAcceptors());
BlaubotAdapterHelper.setDiscoveryActivated(session.getBeaconService(), false);
// start the timer
createAndStartNewTimer();
sendCencusMessage();
session.getServerConnectionManager().setMaster(true);
}
@Override
public IBlaubotState onAdminMessage(AbstractAdminMessage adminMessage) {
/**
* The PrinceFoundAKingAdminMessage is handled by the AdminMessageBeacon now
*/
// if (adminMessage instanceof PrinceFoundAKingAdminMessage) {
// final PrinceFoundAKingAdminMessage princeFoundAKingAdminMessage = (PrinceFoundAKingAdminMessage) adminMessage;
// String kingsUniqueDeviceId = princeFoundAKingAdminMessage.getKingsUniqueDeviceId();
// if (session.isOwnDevice(kingsUniqueDeviceId)) {
// // the idiotic prince seems to be blind - he discovered us ...
// if (Log.logWarningMessages()) {
// //Log.w(LOG_TAG, "Got information about another king BUT: We should buy our prince better glasses - he discovered us. Ignoring ...");
// }
// return this;
// }
// if (Log.logDebugMessages()) {
// Log.d(LOG_TAG, "Got informed about another King (" + kingsUniqueDeviceId + ") by my prince.");
// }
// // TODO: we need to know, to which adapter the found king's uniqueId belongs !!
// // just choosing the first adapter for now
// for (IBlaubotAdapter adapter : session.getAdapters()) {
// IBlaubotDevice discoveredKingDevice = adapter.getConnector().createRemoteDevice(kingsUniqueDeviceId);
// if (discoveredKingDevice == null) {
// // device with kingsUniqueDeviceId is not
// // known/bonded/constructable by connector
// if (Log.logWarningMessages()) {
// Log.w(LOG_TAG, "Connector could not create IBlaubotDevice instance out of the uniqueID " + kingsUniqueDeviceId + ". This happens if the device is not bonded/known or constructable by a connector. Be concerned!");
// }
// continue;
// }
// // we emulate a discovery event
// final List<ConnectionMetaDataDTO> connectionMetaDataList = princeFoundAKingAdminMessage.getConnectionMetaDataList();
// DiscoveredKingEvent event = new DiscoveredKingEvent(discoveredKingDevice, connectionMetaDataList);
// return onDeviceDiscoveryEvent(event);
// }
// }
if (adminMessage instanceof ACKPronouncePrinceAdminMessage) {
this.princeWatcher.onAck((ACKPronouncePrinceAdminMessage) adminMessage);
}
return this;
}
@Override
public String toString() {
return "KingState";
}
@Override
public IBlaubotState onTimeoutEvent(AbstractTimeoutStateMachineEvent timeoutEvent) {
if (timeoutEvent instanceof KingTimeoutEvent) {
if (timeoutEvent.getConnectionStateMachineState() == this) {
ConnectionStateMachineConfig config = session.getAdapters().get(0).getConnectionStateMachineConfig();
final int TIMEOUT_INTERVAL = config.getKingWithoutPeasantsTimeout();
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "King-Timeout event occured (No connected peasants for at least " + TIMEOUT_INTERVAL + " ms)");
}
return new FreeState();
}
} else if (timeoutEvent instanceof PronouncedPrinceACKTimeoutStateMachineEvent) {
this.princeWatcher.onTimeout((PronouncedPrinceACKTimeoutStateMachineEvent) timeoutEvent);
}
return this;
}
/**
* Set the {@link IBlaubotConnectionListener} that will be called if we get
* a new {@link IBlaubotConnection} from a peasant.
*
* @param peasantConnectionsListener
*/
private Object listenerLock = new Object();
public void setPeasantConnectionsListener(IBlaubotIncomingConnectionListener peasantConnectionsListener) {
synchronized (listenerLock) {
this.peasantConnectionsListener = peasantConnectionsListener;
}
}
}