package co.codewizards.cloudstore.ls.core.invoke; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.io.Serializable; import java.lang.reflect.Proxy; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.Uid; import co.codewizards.cloudstore.ls.core.invoke.refjanitor.ReferenceJanitorRegistry; public class ObjectManager { /** * Timeout after which an unused {@code ObjectManager} is evicted. * <p> * If a client is closed normally (or crashes) we must make sure that the object-references held * by this {@code ObjectManager} in the server's JVM are released and can be garbage-collected. * Therefore, we track the {@linkplain #getLastUseDate() last use timestamp} (e.g. * {@linkplain #updateLastUseDate() update it} when {@link #getInstance(Uid)} is called). * <p> * Periodically, all {@code ObjectManager}s not being used for a time period longer than this timeout * are "forgotten" and thus both the {@code ObjectManager}s and all the objects they hold can be * garbage-collected. * <p> * This timeout must be (significantly) longer than {@code InverseInvoker.POLL_INVERSE_SERVICE_REQUEST_TIMEOUT_MS} * to make sure, the long-polling of inverse-service-invocation-requests serves additionally as a keep-alive for * the server-side {@code ObjectManager}. */ protected static final long EVICT_UNUSED_OBJECT_MANAGER_TIMEOUT_MS = 2 * 60 * 1000L; // 2 minutes protected static final long EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS = 60 * 1000L; /** * The other side must notify us that an object is actually used (by invoking {@link #incRefCount(Object, Uid)}) * within this timeout. * <p> * Thus, this timeout must be longer than the maximum time it ever takes to * (1) transmit the entire object graph from here to the other side and (2) notify in the inverse direction * (increment reference count). * <p> * Note, that the inverse notification is deferred for performance reasons! * {@link IncDecRefCountQueue#INC_DEC_REF_COUNT_PERIOD_MS} thus must be significantly shorter than this timeout * here. */ protected static final long EVICT_ZERO_REFERENCE_OBJECT_REFS_TIMEOUT_MS = 30 * 1000L; // 30 seconds protected static final long EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS = 5 * 1000L; private static final Logger logger = LoggerFactory.getLogger(ObjectManager.class); private final Uid clientId; private long nextObjectId; private volatile Date lastUseDate; private volatile boolean neverEvict; private boolean closed; private final Map<ObjectRef, Object> objectRef2Object = new HashMap<>(); private final Map<Object, ObjectRef> object2ObjectRef = new IdentityHashMap<>(); private final Map<String, Object> contextObjectMap = new HashMap<>(); private final Map<ObjectRef, Long> zeroReferenceObjectRef2Timestamp = new HashMap<>(); private final Map<ObjectRef, Set<Uid>> objectRef2RefIds = new HashMap<>(); private final RemoteObjectProxyManager remoteObjectProxyManager = new RemoteObjectProxyManager(); private final ClassManager classManager; private final ReferenceJanitorRegistry referenceJanitorRegistry; private static final Map<Uid, ObjectManager> clientId2ObjectManager = new HashMap<>(); private static long evictOldObjectManagersLastInvocation = 0; private static long evictZeroReferenceObjectRefsLastInvocation = 0; private static final Timer timer = new Timer(true); private static final TimerTask timerTask = new TimerTask() { @Override public void run() { try { evictOldObjectManagers(); } catch (Exception x) { logger.error("run: " + x, x); } try { allObjectManagers_evictZeroReferenceObjectRefs(); } catch (Exception x) { logger.error("run: " + x, x); } } }; static { final long period = BigInteger.valueOf(EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS) .gcd(BigInteger.valueOf(EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS)).longValue(); timer.schedule(timerTask, period, period); } public static synchronized ObjectManager getInstance(final Uid clientId) { assertNotNull(clientId, "clientId"); ObjectManager objectManager = clientId2ObjectManager.get(clientId); if (objectManager == null) { objectManager = new ObjectManager(clientId); clientId2ObjectManager.put(clientId, objectManager); } objectManager.updateLastUseDate(); return objectManager; } /** * @deprecated Only used for tests! Don't use this method productively! */ @Deprecated public static synchronized void clearObjectManagers() { clientId2ObjectManager.clear(); } private static void evictOldObjectManagers() { int objectManagerCountTotal = 0; int objectManagerCountNeverEvict = 0; final List<ObjectManager> objectManagersToClose = new LinkedList<>(); synchronized (ObjectManager.class) { final long now = System.currentTimeMillis(); if (evictOldObjectManagersLastInvocation > now - EVICT_UNUSED_OBJECT_MANAGER_PERIOD_MS) return; evictOldObjectManagersLastInvocation = now; for (final ObjectManager objectManager : clientId2ObjectManager.values()) { ++objectManagerCountTotal; if (objectManager.isNeverEvict()) { ++objectManagerCountNeverEvict; continue; } if (objectManager.getLastUseDate().getTime() < now - EVICT_UNUSED_OBJECT_MANAGER_TIMEOUT_MS) { objectManagersToClose.add(objectManager); logger.debug("evictOldObjectManagers: evicting ObjectManager with clientId={}", objectManager.getClientId()); } } } for (final ObjectManager objectManager : objectManagersToClose) objectManager.close(); logger.debug("evictOldObjectManagers: objectManagerCountTotal={} objectManagerCountNeverEvict={} objectManagerCountEvicted={}", objectManagerCountTotal, objectManagerCountNeverEvict, objectManagersToClose.size()); } private static synchronized List<ObjectManager> getObjectManagers() { final List<ObjectManager> objectManagers = new ArrayList<ObjectManager>(clientId2ObjectManager.values()); return objectManagers; } private static void allObjectManagers_evictZeroReferenceObjectRefs() { final long now = System.currentTimeMillis(); if (evictZeroReferenceObjectRefsLastInvocation > now - EVICT_ZERO_REFERENCE_OBJECT_REFS_PERIOD_MS) return; evictZeroReferenceObjectRefsLastInvocation = now; final List<ObjectManager> objectManagers = getObjectManagers(); for (final ObjectManager objectManager : objectManagers) objectManager.evictZeroReferenceObjectRefs(); } private synchronized void evictZeroReferenceObjectRefs() { final long now = System.currentTimeMillis(); final LinkedList<ObjectRef> objectRefsToRemove = new LinkedList<>(); for (final Map.Entry<ObjectRef, Long> me : zeroReferenceObjectRef2Timestamp.entrySet()) { final ObjectRef objectRef = me.getKey(); final long timestamp = me.getValue(); if (timestamp < now - EVICT_ZERO_REFERENCE_OBJECT_REFS_TIMEOUT_MS) objectRefsToRemove.add(objectRef); } for (final ObjectRef objectRef : objectRefsToRemove) remove(objectRef); } protected ObjectManager(final Uid clientId) { this.clientId = assertNotNull(clientId, "clientId"); classManager = new ClassManager(clientId); referenceJanitorRegistry = new ReferenceJanitorRegistry(this); logger.debug("[{}].<init>: Created ObjectManager.", clientId); } protected Date getLastUseDate() { return lastUseDate; } private void updateLastUseDate() { this.lastUseDate = new Date(); } public boolean isNeverEvict() { return neverEvict; } public void setNeverEvict(boolean neverEvict) { this.neverEvict = neverEvict; } /** * Gets the id of the client using this {@code ObjectManager}. This is either the remote client talking to a server * or it is the server (when the remote client holds references e.g. to listeners or other callbacks for the server). * @return the id of the client. */ public Uid getClientId() { return clientId; } public synchronized Object getContextObject(String key) { return contextObjectMap.get(key); } public synchronized void putContextObject(String key, Object object) { contextObjectMap.put(key, object); } protected synchronized ObjectRef createObjectRef(Class<?> clazz) { assertNotClosed(); final int classId = classManager.getClassIdOrCreate(clazz); final ObjectRef objectRef = new ObjectRef(clientId, classId, nextObjectId++); if (! classManager.isClassIdKnownByRemoteSide(classId)) objectRef.setClassInfo(classManager.getClassInfo(classId)); return objectRef; } public synchronized Object getObjectRefOrObject(final Object object) { if (isObjectRefMappingEnabled(object)) return getObjectRefOrCreate(object); else return object; } public synchronized ObjectRef getObjectRefOrCreate(final Object object) { ObjectRef objectRef = getObjectRef(object); if (objectRef == null) { objectRef = createObjectRef(object.getClass()); if (logger.isDebugEnabled()) logger.debug("[{}].getObjectRefOrCreate: Created {} for {} ({}).", clientId, objectRef, toIdentityString(object), object); objectRef2Object.put(objectRef, object); object2ObjectRef.put(object, objectRef); zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis()); } else { // Must refresh timestamp to guarantee enough time for reference handling. // Otherwise it might be released after maybe only a few millis! if (zeroReferenceObjectRef2Timestamp.containsKey(objectRef)) zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis()); } return objectRef; } public synchronized ObjectRef getObjectRefOrFail(final Object object) { final ObjectRef objectRef = getObjectRef(object); if (objectRef == null) throw new IllegalArgumentException(String.format("ObjectManager[%s] does not have ObjectRef for this object: %s (%s)", clientId, toIdentityString(object), object)); return objectRef; } public synchronized ObjectRef getObjectRef(final Object object) { assertNotNull(object, "object"); assertNotInstanceOfObjectRef(object); final ObjectRef objectRef = object2ObjectRef.get(object); updateLastUseDate(); return objectRef; } public synchronized Object getObjectOrFail(final ObjectRef objectRef) { final Object object = getObject(objectRef); if (object == null) throw new IllegalArgumentException(String.format("ObjectManager[%s] does not have object for this ObjectRef: %s", clientId, objectRef)); return object; } public synchronized Object getObject(final ObjectRef objectRef) { assertNotNull(objectRef, "objectRef"); final Object object = objectRef2Object.get(objectRef); updateLastUseDate(); return object; } private synchronized void remove(final ObjectRef objectRef) { assertNotNull(objectRef, "objectRef"); if (!objectRef2Object.containsKey(objectRef)) throw new IllegalStateException("!objectRef2Object.containsKey(objectRef): " + objectRef); zeroReferenceObjectRef2Timestamp.remove(objectRef); final Object object = objectRef2Object.remove(objectRef); object2ObjectRef.remove(object); updateLastUseDate(); logger.debug("remove: {}", objectRef); } public synchronized void incRefCount(final Object object, final Uid refId) { assertNotNull(object, "object"); assertNotNull(refId, "refId"); int refCountBefore; int refCountAfter; final ObjectRef objectRef = getObjectRefOrFail(object); if (zeroReferenceObjectRef2Timestamp.remove(objectRef) != null) { if (objectRef2RefIds.put(objectRef, new HashSet<Uid>(Collections.singleton(refId))) != null) throw new IllegalStateException("Collision! WTF?!"); refCountBefore = 0; refCountAfter = 1; } else { final Set<Uid> refIds = objectRef2RefIds.get(objectRef); refCountBefore = refIds.size(); assertNotNull(refIds, "objectRef2RefIds.get(" + objectRef + ")"); refIds.add(refId); refCountAfter = refIds.size(); } classManager.setClassIdKnownByRemoteSide(objectRef.getClassId()); logger.trace("[{}].incRefCount: {} refCountAfter={} refCountBefore={} refId={}", clientId, objectRef, refCountAfter, refCountBefore, refId); } public synchronized void decRefCount(final Object object, final Uid refId) { assertNotNull(object, "object"); assertNotNull(refId, "refId"); int refCountBefore = 0; int refCountAfter = 0; final ObjectRef objectRef = getObjectRefOrFail(object); final Set<Uid> refIds = objectRef2RefIds.get(objectRef); if (refIds != null) { refCountBefore = refIds.size(); refIds.remove(refId); refCountAfter = refIds.size(); if (refIds.isEmpty()) { objectRef2RefIds.remove(objectRef); zeroReferenceObjectRef2Timestamp.put(objectRef, System.currentTimeMillis()); } } logger.trace("[{}].decRefCount: {} refCountAfter={} refCountBefore={} refId={}", clientId, objectRef, refCountAfter, refCountBefore, refId); } private static void assertNotInstanceOfObjectRef(final Object object) { if (object instanceof ObjectRef) throw new IllegalArgumentException("object is an instance of ObjectRef! " + object); } public RemoteObjectProxyManager getRemoteObjectProxyManager() { return remoteObjectProxyManager; } public ClassManager getClassManager() { return classManager; } public static boolean isObjectRefMappingEnabled(final Object object) { if (object == null) return false; if (object instanceof ObjectRef) return false; final Class<?> clazz = getClassOrArrayComponentType(object); if (Proxy.isProxyClass(clazz)) return true; 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 return true; if (object instanceof Serializable) return false; return true; } private static Class<?> getClassOrArrayComponentType(final Object object) { final Class<?> clazz = object.getClass(); if (clazz.isArray()) return clazz.getComponentType(); else return clazz; } public ReferenceJanitorRegistry getReferenceCleanerRegistry() { return referenceJanitorRegistry; } public synchronized boolean isClosed() { return closed; } protected synchronized void assertNotClosed() { if (closed) throw new IllegalStateException(String.format("ObjectManager[%s] is closed!", clientId)); } public void close() { synchronized (this) { if (closed) return; closed = true; } synchronized (ObjectManager.class) { clientId2ObjectManager.remove(clientId); } referenceJanitorRegistry.cleanUp(); } }