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}