/* * Copyright 2013 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package org.openntf.domino.impl; import java.lang.ref.ReferenceQueue; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import lotus.domino.Base; import org.openntf.domino.ext.Session.Fixes; import org.openntf.domino.types.SessionDescendant; import org.openntf.domino.utils.Factory; /** * Class to cache OpenNTF-Domino-wrapper objects. The wrapper and its delegate is stored in a phantomReference. This reference is queued if * the wrapper Object is GC. Then the delegate gets recycled. * * We don't optimize the map key any more (shifting unused bits out), because there was no measurable performance gain * * * The DominoReference tracks the wrapper lifetime and caches the delegate and key, so that it can do the cleanup if the wrapper dies! * * @author Roland Praml, Foconis AG */ public class DominoReferenceCache { private static final Logger log_ = Logger.getLogger(DominoReferenceCache.class.getName()); /** The delegate map contains the value wrapped in phantomReferences) **/ // private Map<Long, DominoReference> map = new HashMap<Long, DominoReference>(16, 0.75F); private Map<lotus.domino.Base, DominoReference> map = new IdentityHashMap<lotus.domino.Base, DominoReference>(2048); private Map<Long, AtomicInteger> cppMap = Collections.synchronizedMap(new HashMap<Long, AtomicInteger>(2048)); /** This is the queue with unreachable refs **/ private ReferenceQueue<Object> queue = new ReferenceQueue<Object>(); /** * needed to call GC periodically. The optimum is somewhere ~1500. 1024 seems to be a good value */ private static AtomicInteger cache_counter = new AtomicInteger(); public static int GARBAGE_INTERVAL = 1024; /** * Creates a new DominoReferencCache * * @param autorecycle * true if the cache should recycle objects if they are weakly reachable * */ public DominoReferenceCache() { super(); //autorecycle_ = autorecycle; } /** * Gets the value for the given key. * <p> * * @param key * key whose associated value, if any, is to be returned * @return the value to which this map maps the specified key. */ public org.openntf.domino.Base<?> get(final lotus.domino.Base key) { // processQueue(key, parent_key); // We don't need to remove garbage collected values here; // if they are garbage collected, the get() method returns null; // the next put() call with the same key removes the old value // automatically so that it can be completely garbage collected if (key == null) { return null; } else { return getReferenceObject(map.get(key)); } } public void setNoRecycle(final lotus.domino.Base key, final boolean value) { DominoReference ref = map.get(key); if (ref == null) { log_.log(Level.WARNING, "Attemped to set noRecycle on a DominoReference id " + key + " that's not in the local reference cache"); } else { ref.setNoRecycle(value); } } /** * Puts a new (key,value) into the map. * <p> * * @param key * key with which the specified value is to be associated. * @param value * value to be associated with the specified key. * @return The previous value associated with the delegate, if any */ public org.openntf.domino.Base<?> put(final lotus.domino.Base delegate, final org.openntf.domino.Base<?> value) { // If the map already contains an equivalent key, the new key // of a (key, value) pair is NOT stored in the map but the new // value only. But as the key is strongly referenced by the // map, it can not be removed from the garbage collector, even // if the key becomes weakly reachable due to the old // value. So, it isn't necessary to remove all garbage // collected values with their keys from the map before the // new entry is made. We only clean up here to distribute // clean up calls on different operations. if (value == null) { return null; } long cppId = 0; // If CPP tracking is enabled for this session, get the inner ID and count it if (value instanceof SessionDescendant && ((SessionDescendant) value).getAncestorSession().isFixEnabled(Fixes.PENDANTIC_GC_TRACKING)) { cppId = org.openntf.domino.impl.Base.GetCppObj(delegate); synchronized (cppMap) { if (!cppMap.containsKey(cppId)) { cppMap.put(cppId, new AtomicInteger(1)); } else { cppMap.get(cppId).incrementAndGet(); } } } // create and enqueue a reference that tracks lifetime of value DominoReference ref = new DominoReference(value, queue, delegate, cppId); if (delegate == null) { throw new IllegalArgumentException("key cannot be 0"); } Factory.countLotus(delegate.getClass()); return getReferenceObject(map.put(delegate, ref)); } /** * Removes all garbage collected values with their keys from the map. * */ public long processQueue(final lotus.domino.Base current, final Collection<lotus.domino.Base> prevent_recycling) { long result = 0; if (current instanceof lotus.domino.Item) { // do not count items, as we have many of them } else if (current instanceof lotus.domino.MIMEEntity) { // Mimeentities are recycled on closeMimeEntities } else if (current instanceof lotus.domino.MIMEHeader) { // same for header } else { // count only "heavy" objects (list above not yet complete) int counter = cache_counter.incrementAndGet(); if (counter % GARBAGE_INTERVAL == 0) { // We have to run GC from time to time, otherwise objects will die very late :( System.gc(); } } DominoReference ref = null; boolean died = false; while ((ref = (DominoReference) queue.poll()) != null) { Base unrefLotus = ref.getDelegate(); map.remove(unrefLotus); if (unrefLotus == null) { // should never happen } else if (unrefLotus == current) { ref.setNoRecycle(true); } else if (prevent_recycling != null) { for (Base curKey : prevent_recycling) { if (curKey == unrefLotus) { // TODO: This case does not handle the counters correctly // System.out.println("Parent object dropped out of the queue ---"); // RPr: This happens definitely, if you access the same document in series // Was reproduceable by iterating over several NoteIds and doing this in the loop (odd number required) // doc1 = d.getDocumentByID(id); // doc1 = null; // doc2 = d.getDocumentByID(id); // doc2 = null; // doc3 = d.getDocumentByID(id); // doc3 = null; // ref is not used any more, but this object must be protected from recycling, because it will be reused in the next step // Q RPr: Who sets this back to False? // A RPr: That is not needed, because this "ref" is not used any more and the lotus object gets wrapped in a new DominoReference. ref.setNoRecycle(true); break; } } } // Check its CPP id to see if it's the last one long cppId = ref.getCppId(); boolean recycle = false; if (cppId != 0) { // Then it must have had tracking enabled synchronized (cppMap) { if (cppMap.get(cppId).decrementAndGet() < 1) { // Then this must have been the last ref for that CPP ID recycle = true; cppMap.remove(cppId); } } } else { recycle = true; } if (recycle) { if (ref.recycle()) { if (current != null && current == unrefLotus) { if (log_.isLoggable(Level.SEVERE)) { log_.log(Level.SEVERE, "The " + current.getClass().getSimpleName() + " passed in to processQueue was recycled in the process"); } } else if (!died && current != null && unrefLotus != null && org.openntf.domino.impl.Base.isDead(current)) { if (log_.isLoggable(Level.SEVERE)) { log_.log(Level.SEVERE, "The " + current.getClass().getSimpleName() + " passed in to processQueue was recycled as a side effect of recycling a " + unrefLotus.getClass().getSimpleName()); } died = true; } result++; } } } return result; } public long finishThreadSafes() { long ret = 0; //NTF can't remove values from a map while iterating over them... Object[] raws = map.values().toArray(); for (Object raw : raws) { DominoReference ref = (DominoReference) raw; if (!(ref.get() instanceof BaseThreadSafe)) { continue; } if (ref.recycle()) { ret++; } map.remove(ref.getDelegate()); } return ret; } /** * A convenience method to return the object held by the weak reference or <code>null</code> if it does not exist. */ protected final org.openntf.domino.Base<?> getReferenceObject(final DominoReference ref) { if (ref == null) { return null; } org.openntf.domino.Base<?> result = ref.get(); //if (result == null) { // // this is the wrapper. If there is no wrapper inside (don't know how this should work) we do not return it // System.out.println("Wrapper lost! " + ref.isEnqueued()); // return null; //} // check if the delegate is still alive. (not recycled or null) if (result == null || ref.isEnqueued()) { // For debug // if (result == null) { // System.out.println("NULL"); // happens, if there is no strong ref to this wrapper. // } // if (ref.isDead()) { // System.out.println("DEAD"); // happens, if you call recycle on the parent() // } // if (ref.isEnqueued()) { // // result is also NULL if the reference is enqueued (makes sense, because result (=wrapper) is no longer reachable // System.out.println("ENQUEUED"); // } // we get dead elements if we do this in a loop // d = s.getDatabase("", "names.nsf"); // doc1 = d.getDocumentByID(id); // d = null; // doc1 = null; // So, these objects can be removed from the cache map.remove(ref.getDelegate()); return null; } else { return result; // the wrapper. } } }