001package co.codewizards.cloudstore.ls.core.invoke;
002
003import static co.codewizards.cloudstore.core.util.AssertUtil.*;
004import static co.codewizards.cloudstore.core.util.ReflectionUtil.*;
005
006import java.util.Collections;
007import java.util.HashMap;
008import java.util.Iterator;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012import java.util.SortedSet;
013import java.util.Timer;
014import java.util.TimerTask;
015import java.util.TreeSet;
016import java.util.concurrent.Executor;
017import java.util.concurrent.Executors;
018import java.util.concurrent.atomic.AtomicInteger;
019
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023import co.codewizards.cloudstore.core.Uid;
024import co.codewizards.cloudstore.ls.core.invoke.filter.ExtMethodInvocationRequest;
025import co.codewizards.cloudstore.ls.core.invoke.filter.InvocationFilterRegistry;
026
027public class InvokeMethodExecutor {
028
029        private static final Logger logger = LoggerFactory.getLogger(InvokeMethodExecutor.class);
030
031        private static final AtomicInteger nextInstanceId = new AtomicInteger();
032        private final int instanceId = nextInstanceId.getAndIncrement();
033
034        private final Executor executor = Executors.newCachedThreadPool();
035        private final Map<Uid, InvocationRunnable> delayedResponseId2InvocationRunnable = Collections.synchronizedMap(new HashMap<Uid, InvocationRunnable>());
036        private final SortedSet<DelayedResponseIdScheduledEviction> delayedResponseIdScheduledEvictions = Collections.synchronizedSortedSet(new TreeSet<DelayedResponseIdScheduledEviction>());
037
038        private final Timer evictOldDataTimer = new Timer(String.format("InvokeMethodExecutor[%d].evictOldDataTimer", instanceId), true);
039        private final TimerTask evictOldDataTimerTask = new TimerTask() {
040                @Override
041                public void run() {
042                        try {
043                                final List<Uid> delayedResponseIdsToEvict = new LinkedList<>();
044                                synchronized (delayedResponseIdScheduledEvictions) {
045                                        for (final Iterator<DelayedResponseIdScheduledEviction> it = delayedResponseIdScheduledEvictions.iterator(); it.hasNext(); ) {
046                                                final DelayedResponseIdScheduledEviction delayedResponseIdScheduledEviction = it.next();
047
048                                                if (System.currentTimeMillis() < delayedResponseIdScheduledEviction.getScheduledEvictionTimestamp())
049                                                        break;
050
051                                                delayedResponseIdsToEvict.add(delayedResponseIdScheduledEviction.getDelayedResponseId());
052                                                it.remove();
053                                        }
054                                }
055
056                                synchronized (delayedResponseId2InvocationRunnable) {
057                                        for (Uid delayedResponseId : delayedResponseIdsToEvict)
058                                                delayedResponseId2InvocationRunnable.remove(delayedResponseId);
059                                }
060                        } catch (final Throwable t) {
061                                logger.error("evictOldDataTimerTask.run: " + t, t);
062                        }
063                }
064        };
065
066        public InvokeMethodExecutor() {
067                evictOldDataTimer.schedule(evictOldDataTimerTask, 60000L, 60000L);
068        }
069
070        public MethodInvocationResponse execute(final ExtMethodInvocationRequest extMethodInvocationRequest) throws Exception {
071                assertNotNull(extMethodInvocationRequest, "extMethodInvocationRequest");
072
073                InvocationFilterRegistry.getInstance().assertCanInvoke(extMethodInvocationRequest);
074
075                final InvocationRunnable invocationRunnable = new InvocationRunnable(extMethodInvocationRequest);
076                executor.execute(invocationRunnable);
077
078                synchronized (invocationRunnable) {
079                        MethodInvocationResponse methodInvocationResponse = invocationRunnable.getMethodInvocationResponse();
080                        if (methodInvocationResponse != null)
081                                return methodInvocationResponse;
082
083                        Throwable error = invocationRunnable.getError();
084                        if (error != null)
085                                throwError(error);
086
087                        try {
088                                invocationRunnable.wait(45000L);
089                        } catch (InterruptedException e) {
090                                logger.debug("performMethodInvocation: " + e, e);
091                        }
092
093                        methodInvocationResponse = invocationRunnable.getMethodInvocationResponse();
094                        if (methodInvocationResponse != null)
095                                return methodInvocationResponse;
096
097                        error = invocationRunnable.getError();
098                        if (error != null)
099                                throwError(error);
100
101                        final Uid delayedResponseId = invocationRunnable.getDelayedResponseId();
102                        assertNotNull(delayedResponseId, "delayedResponseId");
103                        delayedResponseId2InvocationRunnable.put(delayedResponseId, invocationRunnable);
104
105                        return new DelayedMethodInvocationResponse(delayedResponseId);
106                }
107        }
108
109        public MethodInvocationResponse getDelayedResponse(final Uid delayedResponseId) throws Exception {
110                assertNotNull(delayedResponseId, "delayedResponseId");
111
112                long schedEvTiSt = System.currentTimeMillis() + 240000; // scheduled eviction in 4 minutes
113                final InvocationRunnable invocationRunnable = delayedResponseId2InvocationRunnable.get(delayedResponseId);
114                if (invocationRunnable == null)
115                        throw new IllegalArgumentException("delayedResponseId unknown: " + delayedResponseId);
116
117                synchronized (invocationRunnable) {
118                        MethodInvocationResponse methodInvocationResponse = invocationRunnable.getMethodInvocationResponse();
119                        Throwable error = invocationRunnable.getError();
120
121                        if (methodInvocationResponse != null) {
122                                delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId));
123                                return methodInvocationResponse;
124                        }
125                        if (error != null) {
126                                delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId));
127                                throwError(error);
128                        }
129
130                        try {
131                                invocationRunnable.wait(45000L);
132                        } catch (InterruptedException e) {
133                                logger.debug("performMethodInvocation: " + e, e);
134                        }
135
136                        schedEvTiSt = System.currentTimeMillis() + 240000; // scheduled eviction in 4 minutes
137
138                        methodInvocationResponse = invocationRunnable.getMethodInvocationResponse();
139                        if (methodInvocationResponse != null) {
140                                delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId));
141                                return methodInvocationResponse;
142                        }
143                        error = invocationRunnable.getError();
144                        if (error != null) {
145                                delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(schedEvTiSt, delayedResponseId));
146                                throwError(error);
147                        }
148
149                        return new DelayedMethodInvocationResponse(delayedResponseId);
150                }
151        }
152
153        private static void throwError(final Throwable error) throws Exception {
154                assertNotNull(error, "error");
155                if (error instanceof RuntimeException)
156                        throw (RuntimeException) error;
157                else if (error instanceof Error)
158                        throw (Error) error;
159                else
160                        throw new RuntimeException(error);
161        }
162
163        private class InvocationRunnable implements Runnable {
164                private final Logger logger = LoggerFactory.getLogger(InvocationRunnable.class);
165
166                private final ExtMethodInvocationRequest extMethodInvocationRequest;
167                private MethodInvocationResponse methodInvocationResponse;
168                private Throwable error;
169                private Uid delayedResponseId;
170
171                public InvocationRunnable(final ExtMethodInvocationRequest extMethodInvocationRequest) {
172                        this.extMethodInvocationRequest = assertNotNull(extMethodInvocationRequest, "extMethodInvocationRequest");
173                }
174
175                @Override
176                public void run() {
177                        final ObjectManager objectManager = extMethodInvocationRequest.getObjectManager();
178                        final MethodInvocationRequest methodInvocationRequest = extMethodInvocationRequest.getMethodInvocationRequest();
179                        final ClassManager classManager = extMethodInvocationRequest.getObjectManager().getClassManager();
180                        final Class<?> clazz = extMethodInvocationRequest.getTargetClass();
181                        final Object object = methodInvocationRequest.getObject();
182                        final String methodName = methodInvocationRequest.getMethodName();
183
184                        final String[] argumentTypeNames = methodInvocationRequest.getArgumentTypeNames();
185                        final Class<?>[] argumentTypes = argumentTypeNames == null ? null : classManager.getClassesOrFail(argumentTypeNames);
186
187                        final Object[] arguments = methodInvocationRequest.getArguments();
188
189                        objectManager.getReferenceCleanerRegistry().preInvoke(extMethodInvocationRequest);
190
191                        Throwable error = null;
192                        Object resultObject = null;
193                        try {
194
195                                final InvocationType invocationType = methodInvocationRequest.getInvocationType();
196                                switch (invocationType) {
197                                        case CONSTRUCTOR:
198                                                resultObject = invokeConstructor(clazz, arguments);
199                                                break;
200                                        case OBJECT:
201                                                resultObject = invoke(object, methodName, argumentTypes, arguments);
202                                                break;
203                                        case STATIC:
204                                                resultObject = invokeStatic(clazz, methodName, arguments);
205                                                break;
206                                        default:
207                                                throw new IllegalStateException("Unknown InvocationType: " + invocationType);
208                                }
209
210                        } catch (final Throwable x) {
211                                resultObject = null;
212                                error = x;
213                                synchronized (this) {
214                                        this.error = x;
215                                }
216                                logger.debug("run: " + x, x);
217                        } finally {
218                                objectManager.getReferenceCleanerRegistry().postInvoke(extMethodInvocationRequest, resultObject, error);
219                        }
220
221                        synchronized (this) {
222                                if (this.error == null) {
223                                        methodInvocationResponse = MethodInvocationResponse.forInvocation(resultObject, filterWritableArguments(arguments));
224                                        assertNotNull(methodInvocationResponse, "methodInvocationResponse");
225                                }
226
227                                this.notifyAll(); // note: other threads only continue running, *after* this synchronized block is finished entirely!
228
229                                if (delayedResponseId != null) {
230                                        // We give the client 15 minutes to fetch the result.
231                                        delayedResponseIdScheduledEvictions.add(new DelayedResponseIdScheduledEviction(
232                                                        System.currentTimeMillis() + 900L * 1000L, delayedResponseId));
233                                }
234                        }
235                }
236
237                public synchronized MethodInvocationResponse getMethodInvocationResponse() {
238                        return methodInvocationResponse;
239                }
240
241                public synchronized Throwable getError() {
242                        return error;
243                }
244
245                public synchronized Uid getDelayedResponseId() {
246                        if (delayedResponseId == null)
247                                delayedResponseId = new Uid();
248
249                        return delayedResponseId;
250                }
251        }
252
253        private Object[] filterWritableArguments(final Object[] arguments) {
254                if (arguments == null || arguments.length == 0)
255                        return null;
256
257                boolean atLeastOneWritable = false;
258                final Object[] result = new Object[arguments.length];
259                for (int i = 0; i < arguments.length; ++i) {
260                        final Object argument = arguments[i];
261                        if (argument != null && argument.getClass().isArray()) {
262                                result[i] = argument;
263                                atLeastOneWritable = true;
264                        }
265                }
266                return atLeastOneWritable ? result : null;
267        }
268}