/* $Id: GeneralCache.java 988245 2010-08-23 18:39:35Z kwright $ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.manifoldcf.core.cachemanager; import org.apache.manifoldcf.core.interfaces.*; import java.util.*; /** General cache class. This class will be statically instantiated. It contains all the structures * needed to maintain a cache of objects, with both LRU flushing behavior, and timed expiration of * objects. * This cache is entirely local to a JVM and does NOT have any locking and synchronization semantics * cross-JVM. That is handled at a higher level. */ public class GeneralCache { public static final String _rcsid = "@(#)$Id: GeneralCache.java 988245 2010-08-23 18:39:35Z kwright $"; // This table is for looking stuff up by object description protected ObjectRecordTable hashtable = new ObjectRecordTable(); // This table is for looking stuff up by cache key - hash table of hash tables protected InvalidationTable invalidationTable = new InvalidationTable(); // This table keeps the running count of each object class protected ObjectClassTable objectClassTable = new ObjectClassTable(); // This structure is the general expiration tree public ExpirationTree expirationTree = new ExpirationTree(); public GeneralCache() { } /** Locate an object in the cache, and return it if found. *@param objectDescription is the object's unique identifier. *@return the object if found, or null if not present in the cache. */ public synchronized Object lookup(Object objectDescription) { ObjectRecord o = hashtable.lookup(objectDescription); if (o == null) return null; return o.getObject(); } /** Get the creation time of an object in the cache. *@param objectDescription is the object's unique identifier. *@return the creation time, or -1 if object not found. */ public synchronized long getObjectCreationTime(Object objectDescription) { ObjectRecord o = hashtable.lookup(objectDescription); if (o == null) return -1L; return o.getCreationTime(); } /** Get the invalidation keys for an object in the cache. *@param objectDescription is the object's unique identifier. *@return the keys, or null if not found. */ public synchronized StringSet getObjectInvalidationKeys(Object objectDescription) { ObjectRecord o = hashtable.lookup(objectDescription); if (o == null) return null; return o.getKeys(); } /** Get the expiration time for an object in the cache. *@param objectDescription is the object's unique identifier. *@return the expiration time (-1L means none). */ public synchronized long getObjectExpirationTime(Object objectDescription) { ObjectRecord o = hashtable.lookup(objectDescription); if (o == null) return -1L; return o.getObjectExpiration(); } /** Delete a record from the cache. *@param objectDescription is the unique description. */ public synchronized void deleteObject(Object objectDescription) { ObjectRecord o = hashtable.lookup(objectDescription); if (o != null) deleteEntry(o); } /** Add a newly created object to the cache. Use ONLY for newly created objects! *@param objectDescription is the newly created object's unique description. *@param object is the newly created object itself. *@param keys are the invalidation keys for the newly created object. *@param timestamp is the creation timestamp for this object (used for cross-JVM invalidation) */ public synchronized void setObject(Object objectDescription, Object object, StringSet keys, long timestamp) { ObjectRecord record = new ObjectRecord(objectDescription,object,keys,timestamp); hashtable.add(record); // Make an entry in the invalidation hash invalidationTable.addKeys(keys,record); // Object has no expiration or class yet, so don't add it to the expiration tree, or to the object // class trees } /** Set an object's expiration time. *@param objectDescription is the object's unique description. *@param expirationTime is the object's new expiration time, in milliseconds since epoch. */ public synchronized void setObjectExpiration(Object objectDescription, long expirationTime) { // Find existing object ObjectRecord existing = hashtable.lookup(objectDescription); if (existing == null) return; if (existing.getObjectExpiration() != -1) { // Pull the object from the expiration tree expirationTree.removeEntry(existing); } // Set the new expiration existing.setObjectExpiration(expirationTime); if (expirationTime != -1) { //Put the object back into the expiration tree expirationTree.addEntry(existing); } } /** Set an object's class and maximum count. This will clean up extra objects * in a Least Recently Used fashion until the count is met. *@param objectDescription is the object's unique description. *@param objectClass is the object's "class", or grouping for the purposes of LRU. *@param maxCount is the maximum number of objects of the class to permit to * remain in the cache. */ public synchronized void setObjectClass(Object objectDescription, String objectClass, int maxCount) { // Lookup the existing object class ObjectRecord existing = hashtable.lookup(objectDescription); if (existing == null) return; if (existing.getObjectClass() != null) { // Pull the object from the object class expiration tree objectClassTable.removeEntry(existing); } // Set the new object class & LRU value existing.setObjectClass(objectClass); if (objectClass != null) { // Put the object into the object class expiration tree objectClassTable.addEntry(existing); if (maxCount >= 0) { // Now, clean up objects to meet the count while (objectClassTable.getCurrentMemberCount(objectClass) > maxCount) { ObjectRecord oldestRecord = objectClassTable.getOldestEntry(objectClass); // Delete this entry from all places it lives deleteEntry(oldestRecord); } } } } /** Invalidate a set of keys. This causes all objects that have any of the specified * keys as invalidation keys to be removed from the cache. *@param keys is the StringSet describing the keys to invalidate. */ public synchronized void invalidateKeys(StringSet keys) { Iterator enum2 = keys.getKeys(); while (enum2.hasNext()) { String invalidateKey = (String)enum2.next(); Iterator enum1 = invalidationTable.getObjectRecordsForKey(invalidateKey); while (enum1.hasNext()) { ObjectRecord record = (ObjectRecord)enum1.next(); hashtable.remove(record); // Remove from object class table if (record.getObjectClass() != null) { objectClassTable.removeEntry(record); } // Remove from expiration table if (record.getExpirationTime() >= 0) { expirationTree.removeEntry(record); } } // We do this last, because we are enumerating over something in here! invalidationTable.removeKey(invalidateKey); } } /** Expire all records that have older expiration times than that passed in. * @param expireTime is the time to compare against, in milliseconds since epoch. */ public void expireRecords(long expireTime) { while (true) { // Do the synchronizer inside the loop. Cleanup is slower, // but the cache does not get locked for long periods. synchronized (this) { // Get the oldest record, if any ObjectRecord x = expirationTree.getOldestEntry(); if (x == null) break; if (x.getExpirationTime() > expireTime) break; // Remove the entry deleteEntry(x); } } } /** Delete a record from the cache. NOTE WELL: This method cannot be used * if the data associated with the record is currently being processed with * an enumeration (for example), since it modifies the structures that the * enumeration is based on! *@param record is the object record. */ protected void deleteEntry(ObjectRecord record) { // Delete from the main cache hashtable.remove(record); // Delete from key hash invalidationTable.removeObjectRecord(record); // Remove from object class table if (record.getObjectClass() != null) { objectClassTable.removeEntry(record); } // Remove from expiration table if (record.getExpirationTime() >= 0) { expirationTree.removeEntry(record); } } /** This class represents a cached object. It has enough hooks to allow it * to live in all the various data structures the general cache maintains. */ protected class ObjectRecord { protected Object objectDescription; protected Object theObject; protected StringSet invalidationKeys; protected long creationTime; protected long expirationTime = -1; protected String objectClass = null; protected ObjectRecord prevLRU = null; protected ObjectRecord nextLRU = null; protected ObjectRecord sameExpirationPrev = null; protected ObjectRecord sameExpirationNext = null; public ObjectRecord(Object objectDescription, Object theObject, StringSet invalidationKeys, long creationTime) { this.creationTime = creationTime; this.objectDescription = objectDescription; this.theObject = theObject; this.invalidationKeys = invalidationKeys; } public long getCreationTime() { return creationTime; } public void setSameExpirationPrev(ObjectRecord x) { sameExpirationPrev = x; } public ObjectRecord getSameExpirationPrev() { return sameExpirationPrev; } public void setSameExpirationNext(ObjectRecord x) { sameExpirationNext = x; } public ObjectRecord getSameExpirationNext() { return sameExpirationNext; } public void setObjectExpiration(long expTime) { expirationTime = expTime; } public long getObjectExpiration() { return expirationTime; } public Object getObjectDescription() { return objectDescription; } public void setObjectClass(String className) { objectClass = className; } public String getObjectClass() { return objectClass; } public ObjectRecord getPrevLRU() { return prevLRU; } public ObjectRecord getNextLRU() { return nextLRU; } public void setPrevLRU(ObjectRecord prev) { prevLRU = prev; } public void setNextLRU(ObjectRecord next) { nextLRU = next; } public Object getObject() { return theObject; } public StringSet getKeys() { return invalidationKeys; } public long getExpirationTime() { return expirationTime; } public int hashCode() { return objectDescription.hashCode(); } public boolean equals(Object o) { if (!(o instanceof ObjectRecord)) return false; ObjectRecord record = (ObjectRecord)o; return objectDescription.equals(record.objectDescription); } } /** This class describes a table of object records, looked up * by the unique object description. */ protected class ObjectRecordTable { protected HashMap hashtable = new HashMap(); public ObjectRecordTable() { } public void add(ObjectRecord record) { hashtable.put(record.getObjectDescription(),record); } public void remove(Object objectDescription) { hashtable.remove(objectDescription); } public void remove(ObjectRecord record) { hashtable.remove(record.getObjectDescription()); } public ObjectRecord lookup(Object objectDescription) { return (ObjectRecord)hashtable.get(objectDescription); } } /** This class describes a table of invalidation keys, each of which points * to a set of object records. */ protected class InvalidationTable { protected HashMap hashtable = new HashMap(); public InvalidationTable() { } public void addKeys(StringSet keyset, ObjectRecord objectRecord) { Iterator enum1 = keyset.getKeys(); while (enum1.hasNext()) { String key = (String)enum1.next(); HashMap ht = (HashMap)hashtable.get(key); if (ht == null) { ht = new HashMap(); hashtable.put(key,ht); } ht.put(objectRecord,objectRecord); } } public Iterator getObjectRecordsForKey(String key) { HashMap ht = (HashMap)hashtable.get(key); if (ht == null) { ht = new HashMap(); hashtable.put(key,ht); } return ht.keySet().iterator(); } public void removeKey(String key) { hashtable.remove(key); } public void removeObjectRecord(ObjectRecord record) { // Get the keys StringSet keys = record.getKeys(); Iterator enum1 = keys.getKeys(); while (enum1.hasNext()) { String key = (String)enum1.next(); removeObjectRecordFromKey(key,record); } } public void removeObjectRecordFromKey(String key, ObjectRecord objectRecord) { HashMap ht = (HashMap)hashtable.get(key); if (ht == null) return; ht.remove(objectRecord); } } /** This class describes a set of object classes, each with its own LRU behavior. */ protected class ObjectClassTable { protected HashMap hashtable = new HashMap(); public ObjectClassTable() { } /** Call ONLY if there is no existing record in the object class table for this record */ public void addEntry(ObjectRecord record) { ObjectClassRecord x = (ObjectClassRecord)hashtable.get(record.getObjectClass()); if (x == null) { x = new ObjectClassRecord(); hashtable.put(record.getObjectClass(),x); } x.addEntry(record); } /** Call ONLY if there is known to be an existing record in the object class table */ public void removeEntry(ObjectRecord record) { ObjectClassRecord x = (ObjectClassRecord)hashtable.get(record.getObjectClass()); if (x == null) return; x.removeEntry(record); } public int getCurrentMemberCount(String objectClassName) { ObjectClassRecord x = (ObjectClassRecord)hashtable.get(objectClassName); if (x == null) return 0; return x.getCurrentMemberCount(); } public ObjectRecord getOldestEntry(String objectClassName) { ObjectClassRecord x = (ObjectClassRecord)hashtable.get(objectClassName); if (x == null) return null; return x.getOldestEntry(); } } /** This is a helper class for the ObjectClassTable. It maintains the data * for an individual object class. */ protected class ObjectClassRecord { protected int currentMemberCount = 0; protected ObjectRecord firstLRU = null; protected ObjectRecord lastLRU = null; public ObjectClassRecord() { } public int getCurrentMemberCount() { return currentMemberCount; } /** Call this ONLY if it is known that the entry exists in * the object class record!!! */ public void removeEntry(ObjectRecord x) { currentMemberCount--; // Patch up everything ObjectRecord prev = x.getPrevLRU(); ObjectRecord next = x.getNextLRU(); if (prev == null) firstLRU = next; else prev.setNextLRU(next); if (next == null) lastLRU = prev; else next.setPrevLRU(prev); x.setPrevLRU(null); x.setNextLRU(null); } /** Add a record to the end of the LRU list. * Call this ONLY if it is known that the entry does NOT * exist in the object class record!!! */ public void addEntry(ObjectRecord x) { currentMemberCount++; x.setNextLRU(null); x.setPrevLRU(lastLRU); if (lastLRU == null) firstLRU = x; else lastLRU.setNextLRU(x); lastLRU = x; } /** Find the first (oldest) entry, or null * if there is none. */ public ObjectRecord getOldestEntry() { return firstLRU; } } /** This class represents a timed expiration tree. Expiration * is used to order the nodes. */ protected class ExpirationTree { protected ExpirationTreeNode root = null; public ExpirationTree() { } /** This method MUST have the entry in the tree before * being called! */ public void removeEntry(ObjectRecord x) { // We may delete entries from a node, but we could very well delete the node // as well. Therefore, this method assumes that this might happen. // ExpirationTreeNode parent = null; boolean parentLesser = false; long expTime = x.getExpirationTime(); ExpirationTreeNode current = root; while (current != null) { long nodeExpTime = current.getExpirationTime(); if (expTime == nodeExpTime) { // Found the right node! // Delete the embedded record if (current.removeObjectRecord(x)) { // The node itself also needs to be removed! ExpirationTreeNode lesserSide = current.getLesser(); ExpirationTreeNode greaterSide = current.getGreater(); if (lesserSide == null && greaterSide == null) { // Just remove the node; no children setPointer(parent,parentLesser,null); } else if (lesserSide == null && greaterSide != null) { // Simple rearrangement setPointer(parent,parentLesser,greaterSide); } else if (lesserSide != null && greaterSide == null) { // Reverse simple rearrangement setPointer(parent,parentLesser,lesserSide); } else { // Full complexity // Here, we may have a choice: Move up the lesser child, or // move up the greater child. // In theory, this should depend on the depth difference of the two // sides, but since we don't keep this info, we'll just be arbitrary setPointer(parent,parentLesser,greaterSide); // Add the lesser side into the new node in this position addTreeToBranch(greaterSide,true,lesserSide); } } return; } if (expTime < nodeExpTime) { // go the lesser route parent = current; parentLesser = true; current = current.getLesser(); } else { // go the greater route parent = current; parentLesser = false; current = current.getGreater(); } } // Should never get here, because it means we did not find the record. } /** This method files a subtree (represented by toAdd) beneath a branch, which is represented by * the parent parameters. If parent is null, then the overall root of the tree is the start point. * If the parent is NOT null, then parentLesser describes whether the lesser branch is the one being modified. * The logic determines the shallowest legal placement of the toAdd node(s), and inserts them there. */ protected void addTreeToBranch(ExpirationTreeNode parent, boolean parentLesser, ExpirationTreeNode toAdd) { if (toAdd == null) return; long expTime = toAdd.getExpirationTime(); // Find the first node to consider ExpirationTreeNode current; if (parent == null) current = root; else { if (parentLesser) current = parent.getLesser(); else current = parent.getGreater(); } // Now, loop until we hit the end while (current != null) { long nodeExpTime = current.getExpirationTime(); if (expTime < nodeExpTime) { // Take the lesser route parent = current; parentLesser = true; current = current.getLesser(); } else { // Take the greater route parent = current; parentLesser = false; current = current.getGreater(); } } // Insert the subtree here setPointer(parent,parentLesser,toAdd); } /** This method MUST NOT have the entry in the tree already * before being called! */ public void addEntry(ObjectRecord x) { // Get the record expiration time, for convenience long expTime = x.getExpirationTime(); // These two variables keep track of the last link we examined, so we can know where to put // the new node, if required. ExpirationTreeNode previousNode = null; boolean lesser = false; // This is our current variable ExpirationTreeNode current = root; while (current != null) { long nodeExpTime = current.getExpirationTime(); if (nodeExpTime == expTime) { // Add to the current node current.addObjectRecord(x); return; } if (nodeExpTime > expTime) { // Go down the lesser branch previousNode = current; lesser = true; current = current.getLesser(); } else { // Go down the greater branch previousNode = current; lesser = false; current = current.getGreater(); } } // New node needs to be created. ExpirationTreeNode newNode = new ExpirationTreeNode(x); setPointer(previousNode,lesser,newNode); } protected void setPointer(ExpirationTreeNode parent, boolean isLesser, ExpirationTreeNode toAdd) { if (parent == null) root = toAdd; else { if (isLesser) parent.setLesser(toAdd); else parent.setGreater(toAdd); } } public ObjectRecord getOldestEntry() { // Look for the least node, and grab a record from it ExpirationTreeNode current = root; ExpirationTreeNode last = null; while (current != null) { last = current; current = current.getLesser(); } if (last == null) return null; return last.getOldest(); } } /** This class represents a node in the expiration tree. * The node has a pool of size at least one containing object records * with the same expiration date. */ protected class ExpirationTreeNode { protected ExpirationTreeNode lesser = null; protected ExpirationTreeNode greater = null; protected ObjectRecord firstSame = null; protected ObjectRecord lastSame = null; public ExpirationTreeNode(ObjectRecord record) { firstSame = record; lastSame = record; } public long getExpirationTime() { return firstSame.getExpirationTime(); } public ExpirationTreeNode getLesser() { return lesser; } public void setLesser(ExpirationTreeNode lesser) { this.lesser = lesser; } public ExpirationTreeNode getGreater() { return greater; } public void setGreater(ExpirationTreeNode greater) { this.greater = greater; } public void addObjectRecord(ObjectRecord x) { x.setSameExpirationNext(firstSame); firstSame.setSameExpirationPrev(x); firstSame = x; } /** Returns true if this removal was the last one (in which case the tree node is now * invalid, and should be removed from the tree) */ public boolean removeObjectRecord(ObjectRecord x) { // Patch up everything ObjectRecord prev = x.getSameExpirationPrev(); ObjectRecord next = x.getSameExpirationNext(); if (prev == null) firstSame = next; else prev.setSameExpirationNext(next); if (next == null) lastSame = prev; else next.setSameExpirationPrev(prev); x.setSameExpirationPrev(null); x.setSameExpirationNext(null); return (firstSame == null); } public ObjectRecord getOldest() { return lastSame; } } }