package co.codewizards.cloudstore.ls.rest.server;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import co.codewizards.cloudstore.core.Uid;
import co.codewizards.cloudstore.core.dto.Error;
import co.codewizards.cloudstore.core.dto.RemoteException;
import co.codewizards.cloudstore.core.dto.RemoteExceptionUtil;
import co.codewizards.cloudstore.core.io.TimeoutException;
import co.codewizards.cloudstore.core.util.ExceptionUtil;
import co.codewizards.cloudstore.ls.core.dto.ErrorResponse;
import co.codewizards.cloudstore.ls.core.dto.InverseServiceRequest;
import co.codewizards.cloudstore.ls.core.dto.InverseServiceResponse;
import co.codewizards.cloudstore.ls.core.dto.NullResponse;
import co.codewizards.cloudstore.ls.core.invoke.ClassInfo;
import co.codewizards.cloudstore.ls.core.invoke.ClassInfoMap;
import co.codewizards.cloudstore.ls.core.invoke.ClassManager;
import co.codewizards.cloudstore.ls.core.invoke.DelayedMethodInvocationResponse;
import co.codewizards.cloudstore.ls.core.invoke.IncDecRefCountQueue;
import co.codewizards.cloudstore.ls.core.invoke.InverseMethodInvocationRequest;
import co.codewizards.cloudstore.ls.core.invoke.InverseMethodInvocationResponse;
import co.codewizards.cloudstore.ls.core.invoke.Invoker;
import co.codewizards.cloudstore.ls.core.invoke.MethodInvocationRequest;
import co.codewizards.cloudstore.ls.core.invoke.MethodInvocationResponse;
import co.codewizards.cloudstore.ls.core.invoke.ObjectManager;
import co.codewizards.cloudstore.ls.core.invoke.ObjectRef;
import co.codewizards.cloudstore.ls.core.invoke.RemoteObjectProxy;
import co.codewizards.cloudstore.ls.core.invoke.RemoteObjectProxyFactory;
import co.codewizards.cloudstore.ls.core.invoke.RemoteObjectProxyInvocationHandler;
public class InverseInvoker implements Invoker {
/**
* Timeout (in milliseconds) before sending an empty HTTP response to the polling client. The client does
* <i>long polling</i> in order to allow for
* {@linkplain #performInverseServiceRequest(InverseServiceRequest) inverse service invocations}.
* <p>
* This timeout must be (significantly) shorter than {@link ObjectManager#EVICT_UNUSED_OBJECT_MANAGER_TIMEOUT_MS} to make sure, the
* {@linkplain #pollInverseServiceRequest() polling} serves additionally as a keep-alive for
* the server-side {@code ObjectManager}.
*/
private static final long POLL_INVERSE_SERVICE_REQUEST_TIMEOUT_MS = 15L * 1000L; // 15 seconds
/**
* Timeout for {@link #performInverseServiceRequest(InverseServiceRequest)}.
* <p>
* If an inverse service-request does not receive a response within this timeout, a {@link TimeoutException} is thrown.
* <p>
* Please note, that the {@code invoke*} methods (e.g. {@link #invoke(Object, String, Object...)} or
* {@link #invokeConstructor(Class, Object...)}) can take much longer, because the other side will return
* a {@link DelayedMethodInvocationResponse} after a much shorter timeout (a few dozen seconds). This allows
* the actual method to be invoked to take how long it wants (unlimited!) while at the same time detecting very
* quickly, if the other side is dead (this timeout).
*/
private static final long PERFORM_INVERSE_SERVICE_REQUEST_TIMEOUT_MS = 3L * 60L * 1000L; // 3 minutes is more than enough, because we have DelayedMethodInvocationResponse
private final IncDecRefCountQueue incDecRefCountQueue = new IncDecRefCountQueue(this);
private final ObjectManager objectManager;
private final LinkedList<InverseServiceRequest> inverseServiceRequests = new LinkedList<>();
private final Set<Uid> requestIdsWaitingForResponse = new HashSet<Uid>(); // synchronized by: requestId2InverseServiceResponse
private final Map<Uid, InverseServiceResponse> requestId2InverseServiceResponse = new HashMap<Uid, InverseServiceResponse>();
private final ClassInfoMap classInfoMap = new ClassInfoMap();
private volatile boolean diedOfTimeout;
public static InverseInvoker getInverseInvoker(final ObjectManager objectManager) {
assertNotNull(objectManager, "objectManager");
synchronized (objectManager) {
InverseInvoker inverseInvoker = (InverseInvoker) objectManager.getContextObject(InverseInvoker.class.getName());
if (inverseInvoker == null) {
inverseInvoker = new InverseInvoker(objectManager);
objectManager.putContextObject(InverseInvoker.class.getName(), inverseInvoker);
}
return inverseInvoker;
}
}
private InverseInvoker(final ObjectManager objectManager) {
this.objectManager = assertNotNull(objectManager, "objectManager");
}
@Override
public ObjectManager getObjectManager() {
return objectManager;
}
@Override
public <T> T invokeStatic(final Class<?> clazz, final String methodName, final Object ... arguments) {
assertNotNull(clazz, "clazz");
assertNotNull(methodName, "methodName");
return invokeStatic(clazz.getName(), methodName, (String[]) null, arguments);
}
@Override
public <T> T invokeStatic(final String className, final String methodName, final Object ... arguments) {
assertNotNull(className, "className");
assertNotNull(methodName, "methodName");
return invokeStatic(className, methodName, (String[]) null, arguments);
}
@Override
public <T> T invokeStatic(final Class<?> clazz, final String methodName, final Class<?>[] argumentTypes, final Object ... arguments) {
assertNotNull(clazz, "clazz");
assertNotNull(methodName, "methodName");
return invokeStatic(clazz.getName(), methodName, toClassNames(argumentTypes), arguments);
}
@Override
public <T> T invokeStatic(final String className, final String methodName, final String[] argumentTypeNames, final Object ... arguments) {
assertNotNull(className, "className");
assertNotNull(methodName, "methodName");
final MethodInvocationRequest methodInvocationRequest = MethodInvocationRequest.forStaticInvocation(
className, methodName, argumentTypeNames, arguments);
return invoke(methodInvocationRequest);
}
@Override
public <T> T invokeConstructor(final Class<T> clazz, final Object ... arguments) {
assertNotNull(clazz, "clazz");
return invokeConstructor(clazz.getName(), (String[]) null, arguments);
}
@Override
public <T> T invokeConstructor(final String className, final Object ... arguments) {
assertNotNull(className, "className");
return invokeConstructor(className, (String[]) null, arguments);
}
@Override
public <T> T invokeConstructor(final Class<T> clazz, final Class<?>[] argumentTypes, final Object ... arguments) {
assertNotNull(clazz, "clazz");
return invokeConstructor(clazz.getName(), toClassNames(argumentTypes), arguments);
}
@Override
public <T> T invokeConstructor(final String className, final String[] argumentTypeNames, final Object ... arguments) {
assertNotNull(className, "className");
final MethodInvocationRequest methodInvocationRequest = MethodInvocationRequest.forConstructorInvocation(
className, argumentTypeNames, arguments);
return invoke(methodInvocationRequest);
}
@Override
public <T> T invoke(final Object object, final String methodName, final Object ... arguments) {
assertNotNull(object, "object");
assertNotNull(methodName, "methodName");
if (!(object instanceof RemoteObjectProxy))
throw new IllegalArgumentException("object is not an instance of RemoteObjectProxy!");
return invoke(object, methodName, (Class<?>[]) null, arguments);
}
@Override
public <T> T invoke(final Object object, final String methodName, final Class<?>[] argumentTypes, final Object... arguments) {
assertNotNull(object, "object");
assertNotNull(methodName, "methodName");
return invoke(object, methodName, toClassNames(argumentTypes), arguments);
}
@Override
public <T> T invoke(final Object object, final String methodName, final String[] argumentTypeNames, final Object... arguments) {
assertNotNull(object, "object");
assertNotNull(methodName, "methodName");
final MethodInvocationRequest methodInvocationRequest = MethodInvocationRequest.forObjectInvocation(
object, methodName, argumentTypeNames, arguments);
return invoke(methodInvocationRequest);
}
private <T> T invoke(final MethodInvocationRequest methodInvocationRequest) {
assertNotNull(methodInvocationRequest, "methodInvocationRequest");
InverseMethodInvocationResponse inverseMethodInvocationResponse = performInverseServiceRequest(
new InverseMethodInvocationRequest(methodInvocationRequest));
assertNotNull(inverseMethodInvocationResponse, "inverseMethodInvocationResponse");
MethodInvocationResponse methodInvocationResponse = inverseMethodInvocationResponse.getMethodInvocationResponse();
while (methodInvocationResponse instanceof DelayedMethodInvocationResponse) {
final DelayedMethodInvocationResponse dmir = (DelayedMethodInvocationResponse) methodInvocationResponse;
final Uid delayedResponseId = dmir.getDelayedResponseId();
inverseMethodInvocationResponse = performInverseServiceRequest(
new InverseMethodInvocationRequest(delayedResponseId));
assertNotNull(inverseMethodInvocationResponse, "inverseMethodInvocationResponse");
methodInvocationResponse = inverseMethodInvocationResponse.getMethodInvocationResponse();
}
final Object result = methodInvocationResponse.getResult();
return cast(result);
}
@Override
public void incRefCount(final ObjectRef objectRef, final Uid refId) {
incDecRefCountQueue.incRefCount(objectRef, refId);
}
@Override
public void decRefCount(final ObjectRef objectRef, final Uid refId) {
incDecRefCountQueue.decRefCount(objectRef, refId);
}
private String[] toClassNames(Class<?> ... classes) {
final String[] classNames;
if (classes == null)
classNames = null;
else {
classNames = new String[classes.length];
for (int i = 0; i < classes.length; i++)
classNames[i] = classes[i].getName();
}
return classNames;
}
public Object getRemoteObjectProxyOrCreate(ObjectRef objectRef) {
return objectManager.getRemoteObjectProxyManager().getRemoteObjectProxyOrCreate(objectRef, new RemoteObjectProxyFactory() {
@Override
public RemoteObjectProxy createRemoteObjectProxy(ObjectRef objectRef) {
return _createRemoteObjectProxy(objectRef);
}
});
}
private RemoteObjectProxy _createRemoteObjectProxy(final ObjectRef objectRef) {
final Class<?>[] interfaces = getInterfaces(objectRef);
final ClassLoader classLoader = this.getClass().getClassLoader();
return (RemoteObjectProxy) Proxy.newProxyInstance(classLoader, interfaces,
new RemoteObjectProxyInvocationHandler(this, objectRef));
}
private Class<?>[] getInterfaces(final ObjectRef objectRef) {
ClassInfo classInfo = classInfoMap.getClassInfo(objectRef.getClassId());
if (classInfo == null) {
classInfo = objectRef.getClassInfo();
if (classInfo == null)
throw new IllegalStateException("There is no ClassInfo in the ClassInfoMap and neither in the ObjectRef! " + objectRef);
classInfoMap.putClassInfo(classInfo);
objectRef.setClassInfo(null);
}
final ClassManager classManager = objectManager.getClassManager();
final Set<String> interfaceNames = classInfo.getInterfaceNames();
final List<Class<?>> interfaces = new ArrayList<>(interfaceNames.size() + 1);
for (final String interfaceName : interfaceNames) {
Class<?> iface = null;
try {
iface = classManager.getClassOrFail(interfaceName);
} catch (RuntimeException x) {
if (ExceptionUtil.getCause(x, ClassNotFoundException.class) == null)
throw x;
}
if (iface != null)
interfaces.add(iface);
}
interfaces.add(RemoteObjectProxy.class);
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
/**
* Invokes a service on the client-side.
* <p>
* Normally, a client initiates a request-response-cycle by sending a request to a server-side service and waiting
* for the response. In order to notify client-side listeners, we need the inverse, though: the server must invoke
* a service on the client-side. This can be easily done by sending an implementation of {@code InverseServiceRequest}
* to a {@code InverseServiceRequestHandler} implementation on the client-side using this method.
*
* @param request the request to be processed on the client-side. Must not be <code>null</code>.
* @return the response created and sent back by the client-side {@code InverseServiceRequestHandler}.
* @throws TimeoutException if this method did not receive a response within the timeout
* {@link #PERFORM_INVERSE_SERVICE_REQUEST_TIMEOUT_MS}.
*/
public <T extends InverseServiceResponse> T performInverseServiceRequest(final InverseServiceRequest request) throws TimeoutException {
assertNotNull(request, "request");
if (diedOfTimeout)
throw new IllegalStateException(String.format("InverseInvoker[%s] died of timeout, already!", objectManager.getClientId()));
final Uid requestId = request.getRequestId();
assertNotNull(requestId, "request.requestId");
synchronized (requestId2InverseServiceResponse) {
if (!requestIdsWaitingForResponse.add(requestId))
throw new IllegalStateException("requestId already queued: " + requestId);
}
try {
synchronized (inverseServiceRequests) {
inverseServiceRequests.add(request);
inverseServiceRequests.notify();
}
// The request is pushed, hence from now on, we wait for the response until the timeout in PERFORM_INVERSE_SERVICE_REQUEST_TIMEOUT_MS.
final long startTimestamp = System.currentTimeMillis();
synchronized (requestId2InverseServiceResponse) {
boolean first = true;
while (first || System.currentTimeMillis() - startTimestamp < PERFORM_INVERSE_SERVICE_REQUEST_TIMEOUT_MS) {
if (first)
first = false;
else {
final long timeSpentTillNowMillis = System.currentTimeMillis() - startTimestamp;
final long waitTimeout = PERFORM_INVERSE_SERVICE_REQUEST_TIMEOUT_MS - timeSpentTillNowMillis;
if (waitTimeout > 0) {
try {
requestId2InverseServiceResponse.wait(waitTimeout);
} catch (InterruptedException e) {
doNothing();
}
}
}
final InverseServiceResponse response = requestId2InverseServiceResponse.remove(requestId);
if (response != null) {
if (response instanceof NullResponse)
return null;
else if (response instanceof ErrorResponse) {
final Error error = ((ErrorResponse) response).getError();
RemoteExceptionUtil.throwOriginalExceptionIfPossible(error);
throw new RemoteException(error);
}
else {
@SuppressWarnings("unchecked")
final T t = (T) response;
return t;
}
}
}
}
} finally {
boolean requestWasStillWaiting;
// in case, it was not yet polled, we make sure garbage does not pile up.
synchronized (requestId2InverseServiceResponse) {
requestWasStillWaiting = requestIdsWaitingForResponse.remove(requestId);
// Make sure, no garbage is left over by removing this together with the requestId from requestIdsWaitingForResponse.
requestId2InverseServiceResponse.remove(requestId);
}
if (requestWasStillWaiting) {
synchronized (inverseServiceRequests) {
inverseServiceRequests.remove(request);
}
}
}
if (request.isTimeoutDeadly())
diedOfTimeout = true;
throw new TimeoutException(String.format("InverseInvoker[%s] encountered timeout while waiting for response matching requestId=%s! diedOfTimeout=%s",
objectManager.getClientId(), requestId, diedOfTimeout));
}
public InverseServiceRequest pollInverseServiceRequest() {
final long startTimestamp = System.currentTimeMillis();
synchronized (inverseServiceRequests) {
boolean first = true;
while (first || System.currentTimeMillis() - startTimestamp < POLL_INVERSE_SERVICE_REQUEST_TIMEOUT_MS) {
if (first)
first = false;
else {
final long timeSpentTillNowMillis = System.currentTimeMillis() - startTimestamp;
final long waitTimeout = POLL_INVERSE_SERVICE_REQUEST_TIMEOUT_MS - timeSpentTillNowMillis;
if (waitTimeout > 0) {
try {
inverseServiceRequests.wait(waitTimeout);
} catch (InterruptedException e) {
doNothing();
}
}
}
final InverseServiceRequest request = inverseServiceRequests.poll();
if (request != null)
return request;
};
}
return null;
}
public void pushInverseServiceResponse(final InverseServiceResponse response) {
assertNotNull(response, "response");
final Uid requestId = response.getRequestId();
assertNotNull(requestId, "response.requestId");
synchronized (requestId2InverseServiceResponse) {
if (!requestIdsWaitingForResponse.contains(requestId))
throw new IllegalArgumentException(String.format("response.requestId=%s does not match any waiting request!", requestId));
requestId2InverseServiceResponse.put(requestId, response);
requestId2InverseServiceResponse.notifyAll();
}
}
@Override
public ClassInfoMap getClassInfoMap() {
return classInfoMap;
}
}