001package co.codewizards.cloudstore.ls.core.invoke;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004import static java.util.Objects.*;
005
006import java.io.Serializable;
007import java.lang.reflect.Proxy;
008import java.math.BigInteger;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.Date;
013import java.util.HashMap;
014import java.util.HashSet;
015import java.util.IdentityHashMap;
016import java.util.LinkedList;
017import java.util.List;
018import java.util.Map;
019import java.util.Set;
020import java.util.Timer;
021import java.util.TimerTask;
022
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import co.codewizards.cloudstore.core.Uid;
027import co.codewizards.cloudstore.ls.core.invoke.refjanitor.ReferenceJanitorRegistry;
028
029public class ObjectManager {
030        /**
031         * Timeout after which an unused {@code ObjectManager} is evicted.
032         * <p>
033         * If a client is closed normally (or crashes) we must make sure that the object-references held
034         * by this {@code ObjectManager} in the server's JVM are released and can be garbage-collected.
035         * Therefore, we track the {@linkplain #getLastUseDate() last use timestamp} (e.g.
036         * {@linkplain #updateLastUseDate() update it} when {@link #getInstance(Uid)} is called).
037         * <p>
038         * Periodically, all {@code ObjectManager}s not being used for a time period longer than this timeout
039         * are "forgotten" and thus both the {@code ObjectManager}s and all the objects they hold can be
040         * garbage-collected.
041         * <p>
042         * This timeout must be (significantly) longer than {@code InverseInvoker.POLL_INVERSE_SERVICE_REQUEST_TIMEOUT_MS}
043         * to make sure, the long-polling of inverse-service-invocation-requests serves additionally as a keep-alive for
044         * the server-side {@code ObjectManager}.
045         */
046        protected static final long EVICT_UNUSED_OBJECT_MANAGER_TIMEOUT_MS = 2 * 60 * 1000L; // 2 minutes
047        protected static final long EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS = 60 * 1000L;
048
049        /**
050         * The other side must notify us that an object is actually used (by invoking {@link #incRefCount(Object, Uid)})
051         * within this timeout.
052         * <p>
053         * Thus, this timeout must be longer than the maximum time it ever takes to
054         * (1) transmit the entire object graph from here to the other side and (2) notify in the inverse direction
055         * (increment reference count).
056         * <p>
057         * Note, that the inverse notification is deferred for performance reasons!
058         * {@link IncDecRefCountQueue#INC_DEC_REF_COUNT_PERIOD_MS} thus must be significantly shorter than this timeout
059         * here.
060         */
061        protected static final long EVICT_ZERO_REFERENCE_OBJECT_REFS_TIMEOUT_MS = 30 * 1000L; // 30 seconds
062        protected static final long EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS = 5 * 1000L;
063
064        private static final Logger logger = LoggerFactory.getLogger(ObjectManager.class);
065
066        private final Uid clientId;
067
068        private long nextObjectId;
069
070        private volatile Date lastUseDate;
071        private volatile boolean neverEvict;
072
073        private boolean closed;
074
075        private final Map<ObjectRef, Object> objectRef2Object = new HashMap<>();
076        private final Map<Object, ObjectRef> object2ObjectRef = new IdentityHashMap<>();
077        private final Map<String, Object> contextObjectMap = new HashMap<>();
078
079        private final Map<ObjectRef, Long> zeroReferenceObjectRef2Timestamp = new HashMap<>();
080        private final Map<ObjectRef, Set<Uid>> objectRef2RefIds = new HashMap<>();
081
082        private final RemoteObjectProxyManager remoteObjectProxyManager = new RemoteObjectProxyManager();
083        private final ClassManager classManager;
084        private final ReferenceJanitorRegistry referenceJanitorRegistry;
085
086        private static final Map<Uid, ObjectManager> clientId2ObjectManager = new HashMap<>();
087
088        private static long evictOldObjectManagersLastInvocation = 0;
089        private static long evictZeroReferenceObjectRefsLastInvocation = 0;
090
091        private static final Timer timer = new Timer(true);
092        private static final TimerTask timerTask = new TimerTask() {
093                @Override
094                public void run() {
095                        try {
096                                evictOldObjectManagers();
097                        } catch (Exception x) {
098                                logger.error("run: " + x, x);
099                        }
100                        try {
101                                allObjectManagers_evictZeroReferenceObjectRefs();
102                        } catch (Exception x) {
103                                logger.error("run: " + x, x);
104                        }
105                }
106        };
107        static {
108                final long period = BigInteger.valueOf(EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS)
109                                .gcd(BigInteger.valueOf(EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS)).longValue();
110
111                timer.schedule(timerTask, period, period);
112        }
113
114        public static synchronized ObjectManager getInstance(final Uid clientId) {
115                requireNonNull(clientId, "clientId");
116                ObjectManager objectManager = clientId2ObjectManager.get(clientId);
117                if (objectManager == null) {
118                        objectManager = new ObjectManager(clientId);
119                        clientId2ObjectManager.put(clientId, objectManager);
120                }
121                objectManager.updateLastUseDate();
122                return objectManager;
123        }
124
125        /**
126         * @deprecated Only used for tests! Don't use this method productively!
127         */
128        @Deprecated
129        public static synchronized void clearObjectManagers() {
130                clientId2ObjectManager.clear();
131        }
132
133        private static void evictOldObjectManagers() {
134                int objectManagerCountTotal = 0;
135                int objectManagerCountNeverEvict = 0;
136
137                final List<ObjectManager> objectManagersToClose = new LinkedList<>();
138                synchronized (ObjectManager.class) {
139                        final long now = System.currentTimeMillis();
140
141                        if (evictOldObjectManagersLastInvocation > now - EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS)
142                                return;
143
144                        evictOldObjectManagersLastInvocation = now;
145
146                        for (final ObjectManager objectManager : clientId2ObjectManager.values()) {
147                                ++objectManagerCountTotal;
148
149                                if (objectManager.isNeverEvict()) {
150                                        ++objectManagerCountNeverEvict;
151                                        continue;
152                                }
153
154                                if (objectManager.getLastUseDate().getTime() < now - EVICT_UNUSED_OBJECT_MANAGER_TIMEOUT_MS) {
155                                        objectManagersToClose.add(objectManager);
156                                        logger.debug("evictOldObjectManagers: evicting ObjectManager with clientId={}", objectManager.getClientId());
157                                }
158                        }
159                }
160
161                for (final ObjectManager objectManager : objectManagersToClose)
162                        objectManager.close();
163
164                logger.debug("evictOldObjectManagers: objectManagerCountTotal={} objectManagerCountNeverEvict={} objectManagerCountEvicted={}",
165                                objectManagerCountTotal, objectManagerCountNeverEvict, objectManagersToClose.size());
166        }
167
168        private static synchronized List<ObjectManager> getObjectManagers() {
169                final List<ObjectManager> objectManagers = new ArrayList<ObjectManager>(clientId2ObjectManager.values());
170                return objectManagers;
171        }
172
173        private static void allObjectManagers_evictZeroReferenceObjectRefs() {
174                final long now = System.currentTimeMillis();
175
176                if (evictZeroReferenceObjectRefsLastInvocation > now - EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS)
177                        return;
178
179                evictZeroReferenceObjectRefsLastInvocation = now;
180
181                final List<ObjectManager> objectManagers = getObjectManagers();
182                for (final ObjectManager objectManager : objectManagers)
183                        objectManager.evictZeroReferenceObjectRefs();
184        }
185
186        private synchronized void evictZeroReferenceObjectRefs() {
187                final long now = System.currentTimeMillis();
188
189                final LinkedList<ObjectRef> objectRefsToRemove = new LinkedList<>();
190                for (final Map.Entry<ObjectRef, Long> me : zeroReferenceObjectRef2Timestamp.entrySet()) {
191                        final ObjectRef objectRef = me.getKey();
192                        final long timestamp = me.getValue();
193
194                        if (timestamp < now - EVICT_ZERO_REFERENCE_OBJECT_REFS_TIMEOUT_MS)
195                                objectRefsToRemove.add(objectRef);
196                }
197
198                for (final ObjectRef objectRef : objectRefsToRemove)
199                        remove(objectRef);
200        }
201
202        protected ObjectManager(final Uid clientId) {
203                this.clientId = requireNonNull(clientId, "clientId");
204                classManager = new ClassManager(clientId);
205                referenceJanitorRegistry = new ReferenceJanitorRegistry(this);
206                logger.debug("[{}].<init>: Created ObjectManager.", clientId);
207        }
208
209        protected Date getLastUseDate() {
210                return lastUseDate;
211        }
212        private void updateLastUseDate() {
213                this.lastUseDate = new Date();
214        }
215
216        public boolean isNeverEvict() {
217                return neverEvict;
218        }
219
220        public void setNeverEvict(boolean neverEvict) {
221                this.neverEvict = neverEvict;
222        }
223
224        /**
225         * Gets the id of the client using this {@code ObjectManager}. This is either the remote client talking to a server
226         * or it is the server (when the remote client holds references e.g. to listeners or other callbacks for the server).
227         * @return the id of the client.
228         */
229        public Uid getClientId() {
230                return clientId;
231        }
232
233        public synchronized Object getContextObject(String key) {
234                return contextObjectMap.get(key);
235        }
236
237        public synchronized void putContextObject(String key, Object object) {
238                contextObjectMap.put(key, object);
239        }
240
241        protected synchronized ObjectRef createObjectRef(Class<?> clazz) {
242                assertNotClosed();
243
244                final int classId = classManager.getClassIdOrCreate(clazz);
245                final ObjectRef objectRef = new ObjectRef(clientId, classId, nextObjectId++);
246
247                if (! classManager.isClassIdKnownByRemoteSide(classId))
248                        objectRef.setClassInfo(classManager.getClassInfo(classId));
249
250                return objectRef;
251        }
252
253        public synchronized Object getObjectRefOrObject(final Object object) {
254                if (isObjectRefMappingEnabled(object))
255                        return getObjectRefOrCreate(object);
256                else
257                        return object;
258        }
259
260        public synchronized ObjectRef getObjectRefOrCreate(final Object object) {
261                ObjectRef objectRef = getObjectRef(object);
262                if (objectRef == null) {
263                        objectRef = createObjectRef(object.getClass());
264
265                        if (logger.isDebugEnabled())
266                                logger.debug("[{}].getObjectRefOrCreate: Created {} for {} ({}).", clientId, objectRef, toIdentityString(object), object);
267
268                        objectRef2Object.put(objectRef, object);
269                        object2ObjectRef.put(object, objectRef);
270                        zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis());
271                }
272                else {
273                        // Must refresh timestamp to guarantee enough time for reference handling.
274                        // Otherwise it might be released after maybe only a few millis!
275                        if (zeroReferenceObjectRef2Timestamp.containsKey(objectRef))
276                                zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis());
277                }
278                return objectRef;
279        }
280
281        public synchronized ObjectRef getObjectRefOrFail(final Object object) {
282                final ObjectRef objectRef = getObjectRef(object);
283                if (objectRef == null)
284                        throw new IllegalArgumentException(String.format("ObjectManager[%s] does not have ObjectRef for this object: %s (%s)",
285                                        clientId, toIdentityString(object), object));
286
287                return objectRef;
288        }
289
290        public synchronized ObjectRef getObjectRef(final Object object) {
291                requireNonNull(object, "object");
292                assertNotInstanceOfObjectRef(object);
293                final ObjectRef objectRef = object2ObjectRef.get(object);
294                updateLastUseDate();
295                return objectRef;
296        }
297
298        public synchronized Object getObjectOrFail(final ObjectRef objectRef) {
299                final Object object = getObject(objectRef);
300                if (object == null)
301                        throw new IllegalArgumentException(String.format("ObjectManager[%s] does not have object for this ObjectRef: %s",
302                                        clientId, objectRef));
303
304                return object;
305        }
306
307        public synchronized Object getObject(final ObjectRef objectRef) {
308                requireNonNull(objectRef, "objectRef");
309                final Object object = objectRef2Object.get(objectRef);
310                updateLastUseDate();
311                return object;
312        }
313
314        private synchronized void remove(final ObjectRef objectRef) {
315                requireNonNull(objectRef, "objectRef");
316
317                if (!objectRef2Object.containsKey(objectRef))
318                        throw new IllegalStateException("!objectRef2Object.containsKey(objectRef): " + objectRef);
319
320                zeroReferenceObjectRef2Timestamp.remove(objectRef);
321                final Object object = objectRef2Object.remove(objectRef);
322                object2ObjectRef.remove(object);
323                updateLastUseDate();
324
325                logger.debug("remove: {}", objectRef);
326        }
327
328        public synchronized void incRefCount(final Object object, final Uid refId) {
329                requireNonNull(object, "object");
330                requireNonNull(refId, "refId");
331
332                int refCountBefore;
333                int refCountAfter;
334
335                final ObjectRef objectRef = getObjectRefOrFail(object);
336                if (zeroReferenceObjectRef2Timestamp.remove(objectRef) != null) {
337                        if (objectRef2RefIds.put(objectRef, new HashSet<Uid>(Collections.singleton(refId))) != null)
338                                throw new IllegalStateException("Collision! WTF?!");
339
340                        refCountBefore = 0;
341                        refCountAfter = 1;
342                }
343                else {
344                        final Set<Uid> refIds = objectRef2RefIds.get(objectRef);
345                        refCountBefore = refIds.size();
346                        requireNonNull(refIds, "objectRef2RefIds.get(" + objectRef + ")");
347                        refIds.add(refId);
348                        refCountAfter = refIds.size();
349                }
350                classManager.setClassIdKnownByRemoteSide(objectRef.getClassId());
351
352                logger.trace("[{}].incRefCount: {} refCountAfter={} refCountBefore={} refId={}",
353                                clientId, objectRef, refCountAfter, refCountBefore, refId);
354        }
355
356        public synchronized void decRefCount(final Object object, final Uid refId) {
357                requireNonNull(object, "object");
358                requireNonNull(refId, "refId");
359
360                int refCountBefore = 0;
361                int refCountAfter = 0;
362
363                final ObjectRef objectRef = getObjectRefOrFail(object);
364                final Set<Uid> refIds = objectRef2RefIds.get(objectRef);
365                if (refIds != null) {
366                        refCountBefore = refIds.size();
367                        refIds.remove(refId);
368                        refCountAfter = refIds.size();
369
370                        if (refIds.isEmpty()) {
371                                objectRef2RefIds.remove(objectRef);
372                                zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis());
373                        }
374                }
375                logger.trace("[{}].decRefCount: {} refCountAfter={} refCountBefore={} refId={}",
376                                clientId, objectRef, refCountAfter, refCountBefore, refId);
377        }
378
379        private static void assertNotInstanceOfObjectRef(final Object object) {
380                if (object instanceof ObjectRef)
381                        throw new IllegalArgumentException("object is an instance of ObjectRef! " + object);
382        }
383
384        public RemoteObjectProxyManager getRemoteObjectProxyManager() {
385                return remoteObjectProxyManager;
386        }
387
388        public ClassManager getClassManager() {
389                return classManager;
390        }
391
392        public static boolean isObjectRefMappingEnabled(final Object object) {
393                if (object == null)
394                        return false;
395
396                if (object instanceof ObjectRef)
397                        return false;
398
399                final Class<?> clazz = getClassOrArrayComponentType(object);
400
401                if (Proxy.isProxyClass(clazz))
402                        return true;
403
404                if (Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz)) // a collection can be modified on the server-side - we want this to be reflected on the client-side, hence we proxy it
405                        return true;
406
407                if (object instanceof Serializable)
408                        return false;
409
410                return true;
411        }
412
413        private static Class<?> getClassOrArrayComponentType(final Object object) {
414                final Class<?> clazz = object.getClass();
415                if (clazz.isArray())
416                        return clazz.getComponentType();
417                else
418                        return clazz;
419        }
420
421        public ReferenceJanitorRegistry getReferenceCleanerRegistry() {
422                return referenceJanitorRegistry;
423        }
424
425        public synchronized boolean isClosed() {
426                return closed;
427        }
428
429        protected synchronized void assertNotClosed() {
430                if (closed)
431                        throw new IllegalStateException(String.format("ObjectManager[%s] is closed!", clientId));
432        }
433
434        public void close() {
435                synchronized (this) {
436                        if (closed)
437                                return;
438
439                        closed = true;
440                }
441                synchronized (ObjectManager.class) {
442                        clientId2ObjectManager.remove(clientId);
443                }
444                referenceJanitorRegistry.cleanUp();
445        }
446}