package games.strategy.engine.message.unifiedmessenger;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import games.strategy.engine.message.ConnectionLostException;
import games.strategy.engine.message.HubInvocationResults;
import games.strategy.engine.message.HubInvoke;
import games.strategy.engine.message.RemoteMethodCall;
import games.strategy.engine.message.RemoteMethodCallResults;
import games.strategy.engine.message.RemoteName;
import games.strategy.engine.message.RemoteNotFoundException;
import games.strategy.engine.message.SpokeInvocationResults;
import games.strategy.engine.message.SpokeInvoke;
import games.strategy.engine.message.UnifiedMessengerHub;
import games.strategy.net.GUID;
import games.strategy.net.IMessageListener;
import games.strategy.net.IMessenger;
import games.strategy.net.IMessengerErrorListener;
import games.strategy.net.INode;
import games.strategy.util.ThreadUtil;
/**
* A messenger general enough that both Channel and Remote messenger can be
* based on it.
*/
public class UnifiedMessenger {
private static final Logger s_logger = Logger.getLogger(UnifiedMessenger.class.getName());
private static final ExecutorService threadPool = Executors.newFixedThreadPool(15);
// the messenger we are based on
private final IMessenger m_messenger;
// lock on this for modifications to create or remove local end points
private final Object m_endPointMutex = new Object();
// maps String -> EndPoint
// these are the end points that
// have local implementors
private final Map<String, EndPoint> m_localEndPoints = new HashMap<>();
private final Object m_pendingLock = new Object();
// threads wait on these latches for the hub to return invocations
// the latch should be removed from the map when you countdown the last result
// access should be synchronized on m_pendingLock
// TODO: how do these get shutdown when we exit a game or close triplea?
private final Map<GUID, CountDownLatch> m_pendingInvocations = new HashMap<>();
// after the remote has invoked, the results are placed here
// access should be synchronized on m_pendingLock
private final Map<GUID, RemoteMethodCallResults> m_results = new HashMap<>();
// only non null for the server
private UnifiedMessengerHub m_hub;
/**
* Creates a new instance of UnifiedMessanger.
*/
public UnifiedMessenger(final IMessenger messenger) {
m_messenger = messenger;
final IMessageListener m_messageListener = (msg, from) -> UnifiedMessenger.this.messageReceived(msg, from);
m_messenger.addMessageListener(m_messageListener);
final IMessengerErrorListener m_messengerErrorListener =
(messenger1, reason) -> UnifiedMessenger.this.messengerInvalid();
m_messenger.addErrorListener(m_messengerErrorListener);
if (m_messenger.isServer()) {
m_hub = new UnifiedMessengerHub(m_messenger, this);
}
}
UnifiedMessengerHub getHub() {
return m_hub;
}
private void messengerInvalid() {
synchronized (m_pendingLock) {
for (final GUID id : m_pendingInvocations.keySet()) {
final CountDownLatch latch = m_pendingInvocations.remove(id);
latch.countDown();
m_results.put(id, new RemoteMethodCallResults(new ConnectionLostException("Connection Lost")));
}
}
}
/**
* Invoke and wait for all implementors on all vms to finish executing.
*/
public RemoteMethodCallResults invokeAndWait(final String endPointName, final RemoteMethodCall remoteCall) {
EndPoint local;
synchronized (m_endPointMutex) {
local = m_localEndPoints.get(endPointName);
}
if (local == null) {
return invokeAndWaitRemote(remoteCall);
// we have the implementor here, just invoke it
} else {
final long number = local.takeANumber();
final List<RemoteMethodCallResults> results = local.invokeLocal(remoteCall, number, getLocalNode());
if (results.size() == 0) {
throw new RemoteNotFoundException("Not found:" + endPointName);
}
if (results.size() > 1) {
throw new IllegalStateException("Too many implementors, got back:" + results);
}
return results.get(0);
}
}
private RemoteMethodCallResults invokeAndWaitRemote(final RemoteMethodCall remoteCall) {
final GUID methodCallID = new GUID();
final CountDownLatch latch = new CountDownLatch(1);
synchronized (m_pendingLock) {
m_pendingInvocations.put(methodCallID, latch);
}
// invoke remotely
final Invoke invoke = new HubInvoke(methodCallID, true, remoteCall);
send(invoke, m_messenger.getServerNode());
try {
latch.await();
} catch (final InterruptedException e) {
s_logger.log(Level.WARNING, e.getMessage());
}
synchronized (m_pendingLock) {
final RemoteMethodCallResults results = m_results.remove(methodCallID);
if (results == null) {
throw new IllegalStateException(
"No results from remote call. Method returned:" + remoteCall.getMethodName() + " for remote name:"
+ remoteCall.getRemoteName() + " with id:" + methodCallID);
}
return results;
}
}
/**
* invoke without waiting for remote nodes to respond.
*/
public void invoke(final String endPointName, final RemoteMethodCall call) {
// send the remote invocation
final Invoke invoke = new HubInvoke(null, false, call);
send(invoke, m_messenger.getServerNode());
// invoke locally
EndPoint endPoint;
synchronized (m_endPointMutex) {
endPoint = m_localEndPoints.get(endPointName);
}
if (endPoint != null) {
final long number = endPoint.takeANumber();
final List<RemoteMethodCallResults> results = endPoint.invokeLocal(call, number, getLocalNode());
for (final RemoteMethodCallResults r : results) {
if (r.getException() != null) {
// don't swallow errors
s_logger.log(Level.WARNING, r.getException().getMessage(), r.getException());
}
}
}
}
public void addImplementor(final RemoteName endPointDescriptor, final Object implementor,
final boolean singleThreaded) {
if (!endPointDescriptor.getClazz().isAssignableFrom(implementor.getClass())) {
throw new IllegalArgumentException(implementor + " does not implement " + endPointDescriptor.getClazz());
}
final EndPoint endPoint = getLocalEndPointOrCreate(endPointDescriptor, singleThreaded);
endPoint.addImplementor(implementor);
}
public INode getLocalNode() {
return m_messenger.getLocalNode();
}
/**
* Get the 1 and only implementor for the endpoint. throws an exception if there are not exctly 1 implementors
*/
public Object getImplementor(final String name) {
synchronized (m_endPointMutex) {
final EndPoint endPoint = m_localEndPoints.get(name);
return endPoint.getFirstImplementor();
}
}
public void removeImplementor(final String name, final Object implementor) {
EndPoint endPoint;
synchronized (m_endPointMutex) {
endPoint = m_localEndPoints.get(name);
if (endPoint == null) {
throw new IllegalStateException("No end point for:" + name);
}
if (implementor == null) {
throw new IllegalArgumentException("null implementor");
}
final boolean noneLeft = endPoint.removeImplementor(implementor);
if (noneLeft) {
m_localEndPoints.remove(name);
send(new NoLongerHasEndPointImplementor(name), m_messenger.getServerNode());
}
}
}
private EndPoint getLocalEndPointOrCreate(final RemoteName endPointDescriptor, final boolean singleThreaded) {
EndPoint endPoint;
synchronized (m_endPointMutex) {
if (m_localEndPoints.containsKey(endPointDescriptor.getName())) {
return m_localEndPoints.get(endPointDescriptor.getName());
}
endPoint = new EndPoint(endPointDescriptor.getName(), endPointDescriptor.getClazz(), singleThreaded);
m_localEndPoints.put(endPointDescriptor.getName(), endPoint);
}
final HasEndPointImplementor msg = new HasEndPointImplementor(endPointDescriptor.getName());
send(msg, m_messenger.getServerNode());
return endPoint;
}
private void send(final Serializable msg, final INode to) {
if (m_messenger.getLocalNode().equals(to)) {
m_hub.messageReceived(msg, getLocalNode());
} else {
m_messenger.send(msg, to);
}
}
public boolean isServer() {
return m_messenger.isServer();
}
/**
* Wait for the messenger to know about the given endpoint.
*/
public void waitForLocalImplement(final String endPointName, long timeoutMS) {
// dont use Long.MAX_VALUE since that will overflow
if (timeoutMS <= 0) {
timeoutMS = Integer.MAX_VALUE;
}
final long endTime = timeoutMS + System.currentTimeMillis();
while (System.currentTimeMillis() < endTime && !hasLocalEndPoint(endPointName)) {
ThreadUtil.sleep(50);
}
}
private boolean hasLocalEndPoint(final String endPointName) {
synchronized (m_endPointMutex) {
return m_localEndPoints.containsKey(endPointName);
}
}
public int getLocalEndPointCount(final RemoteName descriptor) {
synchronized (m_endPointMutex) {
if (!m_localEndPoints.containsKey(descriptor.getName())) {
return 0;
}
return m_localEndPoints.get(descriptor.getName()).getLocalImplementorCount();
}
}
private void assertIsServer(final INode from) {
if (!from.equals(m_messenger.getServerNode())) {
throw new IllegalStateException("Not from server! Instead from:" + from);
}
}
public void messageReceived(final Serializable msg, final INode from) {
if (msg instanceof SpokeInvoke) {
// if this isn't the server, something is wrong
// maybe an attempt to spoof a message
assertIsServer(from);
final SpokeInvoke invoke = (SpokeInvoke) msg;
EndPoint local;
synchronized (m_endPointMutex) {
local = m_localEndPoints.get(invoke.call.getRemoteName());
}
// something a bit strange here, it may be the case
// that the endpoint was deleted locally
// regardless, the other side is expecting our reply
if (local == null) {
if (invoke.needReturnValues) {
send(new HubInvocationResults(
new RemoteMethodCallResults(new RemoteNotFoundException("No implementors for " + invoke.call)),
invoke.methodCallID), from);
}
return;
}
// very important
// we are guaranteed that here messages will be
// read in the same order that they are sent from the client
// however, once we delegate to the thread pool, there is no
// guarantee that the thread pool task will run before
// we get the next message notification
// get the number for the invocation here
final long methodRunNumber = local.takeANumber();
// we dont want to block the message thread, only one thread is
// reading messages
// per connection, so run with out thread pool
final EndPoint localFinal = local;
final Runnable task = () -> {
final List<RemoteMethodCallResults> results =
localFinal.invokeLocal(invoke.call, methodRunNumber, invoke.getInvoker());
if (invoke.needReturnValues) {
RemoteMethodCallResults result = null;
if (results.size() == 1) {
result = results.get(0);
} else {
result = new RemoteMethodCallResults(
new IllegalStateException("Invalid result count" + results.size()) + " for end point:" + localFinal);
}
send(new HubInvocationResults(result, invoke.methodCallID), from);
}
};
threadPool.execute(task);
} else if (msg instanceof SpokeInvocationResults) { // a remote machine is returning results
// if this isn't the server, something is wrong
// maybe an attempt to spoof a message
assertIsServer(from);
final SpokeInvocationResults results = (SpokeInvocationResults) msg;
final GUID methodID = results.methodCallID;
// both of these should already be populated
// this list should be a synchronized list so we can do the add
// all
synchronized (m_pendingLock) {
m_results.put(methodID, results.results);
m_pendingInvocations.remove(methodID).countDown();
}
}
}
public void dumpState(final PrintStream stream) {
synchronized (m_endPointMutex) {
stream.println("Local Endpoints:" + m_localEndPoints);
}
synchronized (m_endPointMutex) {
stream.println("Remote nodes with implementors:" + m_results);
stream.println("Remote nodes with implementors:" + m_pendingInvocations);
}
}
@Override
public String toString() {
return "Server:" + m_messenger.isServer() + " EndPoints:" + m_localEndPoints;
}
}