package co.codewizards.cloudstore.ls.core.invoke; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.ReflectionUtil.*; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.Uid; import co.codewizards.cloudstore.ls.core.invoke.filter.ExtMethodInvocationRequest; import co.codewizards.cloudstore.ls.core.invoke.filter.InvocationFilterRegistry; public class InvokeMethodExecutor { private static final Logger logger = LoggerFactory.getLogger(InvokeMethodExecutor.class); private static final AtomicInteger nextInstanceId = new AtomicInteger(); private final int instanceId = nextInstanceId.getAndIncrement(); private final Executor executor = Executors.newCachedThreadPool(); private final Map<Uid, InvocationRunnable> delayedResponseId2InvocationRunnable = Collections.synchronizedMap(new HashMap<Uid, InvocationRunnable>()); private final SortedSet<DelayedResponseIdScheduledEviction> delayedResponseIdScheduledEvictions = Collections.synchronizedSortedSet(new TreeSet<DelayedResponseIdScheduledEviction>()); private final Timer evictOldDataTimer = new Timer(String.format("InvokeMethodExecutor[%d].evictOldDataTimer", instanceId), true); private final TimerTask evictOldDataTimerTask = new TimerTask() { @Override public void run() { try { final List<Uid> delayedResponseIdsToEvict = new LinkedList<>(); synchronized (delayedResponseIdScheduledEvictions) { for (final Iterator<DelayedResponseIdScheduledEviction> it = delayedResponseIdScheduledEvictions.iterator(); it.hasNext(); ) { final DelayedResponseIdScheduledEviction delayedResponseIdScheduledEviction = it.next(); if (System.currentTimeMillis() < delayedResponseIdScheduledEviction.getScheduledEvictionTimestamp()) break; delayedResponseIdsToEvict.add(delayedResponseIdScheduledEviction.getDelayedResponseId()); it.remove(); } } synchronized (delayedResponseId2InvocationRunnable) { for (Uid delayedResponseId : delayedResponseIdsToEvict) delayedResponseId2InvocationRunnable.remove(delayedResponseId); } } catch (final Throwable t) { logger.error("evictOldDataTimerTask.run: " + t, t); } } }; public InvokeMethodExecutor() { evictOldDataTimer.schedule(evictOldDataTimerTask, 60000L, 60000L); } public MethodInvocationResponse execute(final ExtMethodInvocationRequest extMethodInvocationRequest) throws Exception { assertNotNull(extMethodInvocationRequest, "extMethodInvocationRequest"); InvocationFilterRegistry.getInstance().assertCanInvoke(extMethodInvocationRequest); final InvocationRunnable invocationRunnable = new InvocationRunnable(extMethodInvocationRequest); executor.execute(invocationRunnable); synchronized (invocationRunnable) { MethodInvocationResponse methodInvocationResponse = invocationRunnable.getMethodInvocationResponse(); if (methodInvocationResponse != null) return methodInvocationResponse; Throwable error = invocationRunnable.getError(); if (error != null) throwError(error); try { invocationRunnable.wait(45000L); } catch (InterruptedException e) { logger.debug("performMethodInvocation: " + e, e); } methodInvocationResponse = invocationRunnable.getMethodInvocationResponse(); if (methodInvocationResponse != null) return methodInvocationResponse; error = invocationRunnable.getError(); if (error != null) throwError(error); final Uid delayedResponseId = invocationRunnable.getDelayedResponseId(); assertNotNull(delayedResponseId, "delayedResponseId"); delayedResponseId2InvocationRunnable.put(delayedResponseId, invocationRunnable); return new DelayedMethodInvocationResponse(delayedResponseId); } } public MethodInvocationResponse getDelayedResponse(final Uid delayedResponseId) throws Exception { assertNotNull(delayedResponseId, "delayedResponseId"); long schedEvTiSt = System.currentTimeMillis() + 240000; // scheduled eviction in 4 minutes final InvocationRunnable invocationRunnable = delayedResponseId2InvocationRunnable.get(delayedResponseId); if (invocationRunnable == null) throw new IllegalArgumentException("delayedResponseId unknown: " + delayedResponseId); synchronized (invocationRunnable) { MethodInvocationResponse methodInvocationResponse = invocationRunnable.getMethodInvocationResponse(); Throwable error = invocationRunnable.getError(); if (methodInvocationResponse != null) { delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId)); return methodInvocationResponse; } if (error != null) { delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId)); throwError(error); } try { invocationRunnable.wait(45000L); } catch (InterruptedException e) { logger.debug("performMethodInvocation: " + e, e); } schedEvTiSt = System.currentTimeMillis() + 240000; // scheduled eviction in 4 minutes methodInvocationResponse = invocationRunnable.getMethodInvocationResponse(); if (methodInvocationResponse != null) { delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId)); return methodInvocationResponse; } error = invocationRunnable.getError(); if (error != null) { delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId)); throwError(error); } return new DelayedMethodInvocationResponse(delayedResponseId); } } private static void throwError(final Throwable error) throws Exception { assertNotNull(error, "error"); if (error instanceof RuntimeException) throw (RuntimeException) error; else if (error instanceof Error) throw (Error) error; else throw new RuntimeException(error); } private class InvocationRunnable implements Runnable { private final Logger logger = LoggerFactory.getLogger(InvocationRunnable.class); private final ExtMethodInvocationRequest extMethodInvocationRequest; private MethodInvocationResponse methodInvocationResponse; private Throwable error; private Uid delayedResponseId; public InvocationRunnable(final ExtMethodInvocationRequest extMethodInvocationRequest) { this.extMethodInvocationRequest = assertNotNull(extMethodInvocationRequest, "extMethodInvocationRequest"); } @Override public void run() { final ObjectManager objectManager = extMethodInvocationRequest.getObjectManager(); final MethodInvocationRequest methodInvocationRequest = extMethodInvocationRequest.getMethodInvocationRequest(); final ClassManager classManager = extMethodInvocationRequest.getObjectManager().getClassManager(); final Class<?> clazz = extMethodInvocationRequest.getTargetClass(); final Object object = methodInvocationRequest.getObject(); final String methodName = methodInvocationRequest.getMethodName(); final String[] argumentTypeNames = methodInvocationRequest.getArgumentTypeNames(); final Class<?>[] argumentTypes = argumentTypeNames == null ? null : classManager.getClassesOrFail(argumentTypeNames); final Object[] arguments = methodInvocationRequest.getArguments(); objectManager.getReferenceCleanerRegistry().preInvoke(extMethodInvocationRequest); Throwable error = null; Object resultObject = null; try { final InvocationType invocationType = methodInvocationRequest.getInvocationType(); switch (invocationType) { case CONSTRUCTOR: resultObject = invokeConstructor(clazz, arguments); break; case OBJECT: resultObject = invoke(object, methodName, argumentTypes, arguments); break; case STATIC: resultObject = invokeStatic(clazz, methodName, arguments); break; default: throw new IllegalStateException("Unknown InvocationType: " + invocationType); } } catch (final Throwable x) { resultObject = null; error = x; synchronized (this) { this.error = x; } logger.debug("run: " + x, x); } finally { objectManager.getReferenceCleanerRegistry().postInvoke(extMethodInvocationRequest, resultObject, error); } synchronized (this) { if (this.error == null) { methodInvocationResponse = MethodInvocationResponse.forInvocation(resultObject, filterWritableArguments(arguments)); assertNotNull(methodInvocationResponse, "methodInvocationResponse"); } this.notifyAll(); // note: other threads only continue running, *after* this synchronized block is finished entirely! if (delayedResponseId != null) { // We give the client 15 minutes to fetch the result. delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction( System.currentTimeMillis() + 900L * 1000L, delayedResponseId)); } } } public synchronized MethodInvocationResponse getMethodInvocationResponse() { return methodInvocationResponse; } public synchronized Throwable getError() { return error; } public synchronized Uid getDelayedResponseId() { if (delayedResponseId == null) delayedResponseId = new Uid(); return delayedResponseId; } } private Object[] filterWritableArguments(final Object[] arguments) { if (arguments == null || arguments.length == 0) return null; boolean atLeastOneWritable = false; final Object[] result = new Object[arguments.length]; for (int i = 0; i < arguments.length; ++i) { final Object argument = arguments[i]; if (argument != null && argument.getClass().isArray()) { result[i] = argument; atLeastOneWritable = true; } } return atLeastOneWritable ? result : null; } }