/* $Id: CacheManager.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.nio.charset.StandardCharsets; import java.util.*; import org.apache.manifoldcf.core.system.Logging; import org.apache.manifoldcf.core.system.ManifoldCF; import java.io.*; /** This class implements the cache manager interface, and provides generic cache management * services. See the interface for a description of how the services work. However, since this * service requires the lock manager, and there is one lock manager per thread, there will * be one of these service instances per thread as well. */ public class CacheManager implements ICacheManager { public static final String _rcsid = "@(#)$Id: CacheManager.java 988245 2010-08-23 18:39:35Z kwright $"; protected final static String cacheLockPrefix = "_Cache_"; protected ILockManager lockManager; protected static GeneralCache cache = new GeneralCache(); // This is the hash mapping transaction id's to CacheTransactionHandle objects. // It is thread specific because transactions are thread local. protected HashMap transactionHash = new HashMap(); public CacheManager(IThreadContext context) throws ManifoldCFException { lockManager = LockManagerFactory.make(context); } /** Locate or create a set of objects in the cached object pool, and/or destroy and invalidate * the same or other objects. * * For each "locate" object * described below, one of two things will happen: the execObject's exists() method * will be called (if the object exists in the cache already), or the execObject's create*() * family of methods will be called. When all objects have been declared to the execObject, * finally the execute() method will be called. In both cases, all objects are considered * to be locked, and cannot be invalidated until the lock is cleared (after the end of the called method). * * For the invalidation descriptions, the execObject's destroy() method will be called instead. * The object will be invalidated from the cache at the end of the findObjectsAndExecute() operation. * * It is perfectly legal to include the same object in both the locate set and the invalidate set! * * If an error occurs during object creation, the execute() method will NOT be called. If the * execute() method has an error, the objects that were created will still be recorded in the cache. * * @param locateObjectDescriptions is a set of description objects that uniquely describe the objects needed. * @param invalidateKeys is the set of keys to invalidate at the end of the execution. * @param execObject is the cache execution object whose create() or execute() methods will * be called. * @param transactionID is the current transaction identifier, or null. Objects created within this call * will be associated with this transaction; they will be purged from the cache should the transaction * be rolled back. */ public void findObjectsAndExecute(ICacheDescription[] locateObjectDescriptions, StringSet invalidateKeys, ICacheExecutor execObject, String transactionID) throws ManifoldCFException { // There is some clever engineering involved here. The lock manager is used to control synchronization // around usage and invalidation of objects. However, there is ANOTHER lock condition that needs to be // managed as well in this code. Specifically, in a single JVM, only one thread should be allowed to // create an object at a time. // // This is not a true "lock", because it doesn't cross JVM boundaries. However, it is not brute-force // like a synchronizer either (a synchronizer would single-thread all object creation and usage). // Instead, we need to create a specific object for every object being located. Then, a synchronizer // on that object can insure that only one thread per JVM is allowed to create a given object at a time, // and the others see an "existing" object instead. // // The basic logic is that the sync object is looked up from a global pool in a synchronized way, and if // found indicates that a creation is in progress on the corresponding object. The thread must then wait // until the synchronizer object is released before continuing. Since true locks have already been thrown, // it is guaranteed that the object cannot be destroyed, and the released threads can refind the object // and use it as "existing". Basically, it's a runt version of the lock manager. ICacheHandle handle = enterCache(locateObjectDescriptions,invalidateKeys,transactionID); try { // Ok, we locked everything we need. // Now we need to create what is not yet there. // First, call create() or exists() on the "locate" objects (and also update the // LRU times) if (locateObjectDescriptions != null) { // We are going to go through the locate object list twice; once to see // what we need to create, and a second time after we do the creation. // Accumulate non-cached objects here. HashMap allObjects = new HashMap(); int i; ICacheCreateHandle createHandle = enterCreateSection(handle); try { // Now, go through the objects again, and see which ones we need // to create. The ones with null cache sets will always need creation. ArrayList createList = new ArrayList(); i = 0; while (i < locateObjectDescriptions.length) { ICacheDescription objectDescription = locateObjectDescriptions[i++]; StringSet set = objectDescription.getObjectKeys(); if (set == null) createList.add(objectDescription); else { Object o = lookupObject(createHandle,objectDescription); if (o == null) createList.add(objectDescription); else allObjects.put(objectDescription,o); } } // Perform the create operation ICacheDescription[] createDescriptions = new ICacheDescription[createList.size()]; i = 0; while (i < createList.size()) { createDescriptions[i] = (ICacheDescription)createList.get(i); i++; } Object[] createdObjects = execObject.create(createDescriptions); if (createdObjects == null) return; // Loop through the returned objects, and enter them into the cache i = 0; while (i < createdObjects.length) { saveObject(createHandle,createDescriptions[i],createdObjects[i]); allObjects.put(createDescriptions[i],createdObjects[i]); i++; } } finally { leaveCreateSection(createHandle); } i = 0; while (i < locateObjectDescriptions.length) { ICacheDescription objectDescription = locateObjectDescriptions[i++]; Object o = allObjects.get(objectDescription); // It already exists - send it to the executor execObject.exists(objectDescription,o); } } // Now, call the execute method if (execObject != null) execObject.execute(); invalidateKeys(handle); } finally { leaveCache(handle); } } /** Second way of doing cache management. * Basically, this approach breaks the findObjectsAndExecute() method down into bite-sized chunks. * The goal is additional flexibility, and an executor object does not need to be defined. * Pretty much everything else is identical. The objects returned by the manager methods * provide context control and enforce the proper ordering and nesting. * This method enters the cacher and builds a cache handle. Once a cache handle is * returned, the calling code MUST, under all circumstances, leave the cache using the * same handle. * @param locateObjectDescriptions is a set of description objects that uniquely describe the objects needed. * May be null if no objects are desired. * @param invalidateKeys are the keys to invalidate after successful execution. May be null. * @param transactionID is the current transaction identifier, or null. Objects created within this block * will be associated with this transaction; they will be purged from the cache should the transaction * be rolled back. * @return a cache handle. */ public ICacheHandle enterCache(ICacheDescription[] locateObjectDescriptions, StringSet invalidateKeys, String transactionID) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); if (locateObjectDescriptions != null) { sb.append("{"); int i = 0; while (i < locateObjectDescriptions.length) { if (i > 0) sb.append(","); sb.append(locateObjectDescriptions[i].getCriticalSectionName()); i++; } sb.append("}"); } else sb.append("NULL"); StringBuilder inv = new StringBuilder(); if (invalidateKeys != null) { inv.append("{"); boolean isFirst = true; Iterator iter = invalidateKeys.getKeys(); while (iter.hasNext()) { if (isFirst) isFirst = false; else inv.append(","); inv.append((String)iter.next()); } inv.append("}"); } else inv.append("NULL"); Logging.cache.debug("Entering cacher; objects = "+sb.toString()+" invalidate keys = "+inv.toString()); } // First, throw read locks for the locate objects and write locks for invalidate objects. // To do the read locks, we need to go through the locate objects and get their cache keys. // This must be done prior to the object even being created. StringSetBuffer readLockTable = new StringSetBuffer(); if (locateObjectDescriptions != null) { int i = 0; while (i < locateObjectDescriptions.length) { ICacheDescription objectDescription = locateObjectDescriptions[i++]; // Get the keys StringSet keys = objectDescription.getObjectKeys(); if (keys != null) { readLockTable.add(keys); } } } // Convert readLockTable to a StringSet StringSet readKeys = new StringSet(readLockTable); // The locks we will actually throw depend on whether or not we are in a transaction. The // transaction filters the locks that are needed; locks are preserved within the transaction. // The cache handle itself also does not need the same data - in a transaction, readLocks and writeLocks // passed to the handle will be null (because we don't want them to be freed until the transaction exits). CacheHandle ch; if (transactionID == null) { String[] writeLocks = null; if (invalidateKeys != null) writeLocks = invalidateKeys.getArray(cacheLockPrefix); String[] readLocks = readKeys.getArray(cacheLockPrefix); ch = new CacheHandle(readLocks,writeLocks,locateObjectDescriptions,invalidateKeys,transactionID); Logging.lock.debug("Starting cache outside transaction"); lockManager.enterLocks(readLocks,null,writeLocks); Logging.lock.debug(" Done starting cache"); } else { CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID); if (handle == null) { ManifoldCFException ex = new ManifoldCFException("Illegal transaction ID: '"+transactionID+"'",ManifoldCFException.GENERAL_ERROR); Logging.cache.error(Thread.currentThread().toString()+": enterCache: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex); throw ex; } if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Starting cache in transaction "+transactionID); ch = new CacheHandle(null,null,locateObjectDescriptions,invalidateKeys,transactionID); StringSet newReadLocks = handle.getRemainingReadLocks(readKeys,invalidateKeys); StringSet newWriteLocks = handle.getRemainingWriteLocks(readKeys,invalidateKeys); lockManager.enterLocks(newReadLocks.getArray(cacheLockPrefix),null,newWriteLocks.getArray(cacheLockPrefix)); handle.addLocks(newReadLocks,newWriteLocks); // Logging.lock.debug(" Done starting cache"); } if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Successfully entered cacher; handle = "+ch.toString()); } return ch; } /** Enter a creation critical section. This insures that only one thread is * creating the specified objects at a time. This MUST be paired with * a leaveCreateSection() method call, whatever happens. *@param handle is the cache handle. */ public ICacheCreateHandle enterCreateSection(ICacheHandle handle) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Entering cache create section; cache handle = "+handle.toString()); } // Since we are inside the locks, any existing object cannot be invalidated; // however, it is possible that more than one thread can attempt to perform // object creation for the same object. The only way to prevent this is to // throw a multiple critical section writelock on all the objects. This will // stop multiple creation. ICacheDescription[] locateObjectDescriptions = handle.getObjectDescriptions(); if (locateObjectDescriptions == null) throw new ManifoldCFException("Can't enter create section without objects to create", ManifoldCFException.GENERAL_ERROR); int i = 0; ArrayList writeCriticalSectionArray = new ArrayList(); while (i < locateObjectDescriptions.length) { ICacheDescription objectDescription = locateObjectDescriptions[i++]; StringSet set = objectDescription.getObjectKeys(); // If this object is uncached, we do NOT try to synchronize creation, since it is // a waste of time to do that. Multiple objects will need to be created anyhow. if (set != null) writeCriticalSectionArray.add(objectDescription.getCriticalSectionName()); } // Make it into an array. String[] writeCriticalSections = new String[writeCriticalSectionArray.size()]; i = 0; while (i < writeCriticalSectionArray.size()) { writeCriticalSections[i] = (String)writeCriticalSectionArray.get(i); i++; } CacheCreateHandle ch = new CacheCreateHandle(writeCriticalSections,handle.getTransactionID()); // Enter critical section (but only based on the objects we intend to cache). lockManager.enterCriticalSections(null,null,writeCriticalSections); if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Successfully entered cache create section for handle = "+ handle.toString()+"; section handle = "+ch.toString()); } return ch; } /** Lookup an object. Returns null if object not found. If it is found, * object's LRU and expiration info are updated. The objectDescription passed * MUST be one of the ones specified in the enclosing enterCache() method. *@param handle is the handle to use for the create. *@param objectDescription is the description of the object to look up. *@return the object, or null if not found. */ public Object lookupObject(ICacheCreateHandle handle, ICacheDescription objectDescription) throws ManifoldCFException { if (handle == null) throw new ManifoldCFException("Can't do lookup outside of create section", ManifoldCFException.GENERAL_ERROR); if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Looking up object in section handle = "+handle.toString()+ "; object name = '"+objectDescription.getCriticalSectionName()+"'"); } StringSet set = objectDescription.getObjectKeys(); // If no cache keys, it can't be cached, so don't check! if (set == null) return null; // If this is in a transaction, we must look at the local cached copy first. // In fact, we walk back through the chain of parent transactions until we find it, // or until the cache keys are invalid against the transaction's invalidation keys. String transactionID = handle.getTransactionID(); if (transactionID != null) { CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID); if (transactionHandle == null) { ManifoldCFException ex = new ManifoldCFException("Illegal transaction id",ManifoldCFException.GENERAL_ERROR); Logging.cache.error(Thread.currentThread().toString()+": lookupObject: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex); throw ex; } while (transactionHandle != null) { // Look for the object Object q = transactionHandle.lookupObject(objectDescription); if (q != null) { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' found in transaction cache"); } return q; } // See if we can look at the parent if (transactionHandle.checkCacheKeys(set)) return null; transactionHandle = transactionHandle.getParentTransaction(); } // If nothing stops us, look in the global cache too } Object o = cache.lookup(objectDescription); if (o == null) return null; if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' exists locally; checking if local copy is valid"); } // See if the object's attached expiration is before the current time. long expireTime = cache.getObjectExpirationTime(objectDescription); if (expireTime != -1L && expireTime <= handle.getLookupTime()) { // Blow away the entry in cache, since it has expired cache.deleteObject(objectDescription); return null; } // Before we conclude that the object is found, if we are on a multi-JVM environment we MUST check // the object's timestamp!!! We check it against the invalidation key file timestamps for the object. long createTime = cache.getObjectCreationTime(objectDescription); StringSet keys = cache.getObjectInvalidationKeys(objectDescription); Iterator iter = keys.getKeys(); while (iter.hasNext()) { String key = (String)iter.next(); if (hasExpired(key,createTime)) { // Blow away the entry in cache, since it has expired cache.deleteObject(objectDescription); return null; } } // System.out.println("Found object: "+objectDescription.getCriticalSectionName()); if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' is valid; resetting local expiration"); } // Update the expiration time for this object. resetObjectExpiration(objectDescription,handle.getLookupTime()); return o; } /** Check if object has expired (by looking at file system). *@param key is the invalidation key. *@param createTime is the creation time. *@return true if expired, false otherwise. */ protected boolean hasExpired(String key, long createTime) throws ManifoldCFException { long createdDate = readSharedData(key); if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(" Checking whether our cached copy of object with key = "+key+" has been invalidated. It has create time "+new Long(createTime).toString()+", and the last change is "+new Long(createdDate).toString()); } if (createdDate == 0L) return false; return createdDate >= createTime; } /** Set object's expiration and LRU. *@param objectDescription is the description object. *@param currentTime is the current time in milliseconds since epoch. */ protected void resetObjectExpiration(ICacheDescription objectDescription, long currentTime) { // Update the expiration time for this object. long expireInterval = objectDescription.getObjectExpirationTime(currentTime); cache.setObjectExpiration(objectDescription,expireInterval); // Update LRU and max counts. This also flushes the cache to meet the criteria. ICacheClass objectClass = objectDescription.getObjectClass(); if (objectClass != null) cache.setObjectClass(objectDescription,objectClass.getClassName(),objectClass.getMaxLRUCount()); else cache.setObjectClass(objectDescription,null,Integer.MAX_VALUE); } /** Save a newly created object. The object MUST be one of those identified in the * enterCache() method. *@param handle is the create handle. *@param objectDescription is the object description. *@param object is the object. */ public void saveObject(ICacheCreateHandle handle, ICacheDescription objectDescription, Object object) throws ManifoldCFException { if (handle == null) throw new ManifoldCFException("Can't do save outside of create section", ManifoldCFException.GENERAL_ERROR); if (Logging.cache.isDebugEnabled()) { StringSet ks = objectDescription.getObjectKeys(); StringBuilder sb = new StringBuilder(); if (ks != null) { sb.append("{"); Iterator iter = ks.getKeys(); boolean isFirst = true; while (iter.hasNext()) { if (isFirst) isFirst = false; else sb.append(","); sb.append((String)iter.next()); } sb.append("}"); } else sb.append("NULL"); Logging.cache.debug("Saving new object in section handle = "+handle.toString()+ "; object description = '"+objectDescription.getCriticalSectionName()+ "; cache keys = "+sb.toString()); } StringSet keys = objectDescription.getObjectKeys(); if (keys != null) { String transactionID = handle.getTransactionID(); if (transactionID == null) { cache.setObject(objectDescription,object,keys,handle.getLookupTime()); // Update the expiration time for this object. resetObjectExpiration(objectDescription,handle.getLookupTime()); } else { // Put it into the transaction object // Expiration and LRU don't count here; they get applied when the object goes into // the global cache. CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID); if (transactionHandle == null) { ManifoldCFException ex = new ManifoldCFException("Bad transaction handle",ManifoldCFException.GENERAL_ERROR); Logging.cache.error(Thread.currentThread().toString()+": saveObject: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex); throw ex; } transactionHandle.saveObject(objectDescription,object,keys); } } } /** Leave the create section. *@param handle is the handle created by the corresponding enterCreateSection() method. */ public void leaveCreateSection(ICacheCreateHandle handle) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Leaving cache create section; section handle = "+handle.toString()); } lockManager.leaveCriticalSections(null,null,handle.getCriticalSectionNames()); } /** Invalidate keys. The keys invalidated are what got passed to the enterCache() method. *@param handle is the cache handle. Does nothing if a null set of keys was passed in. */ public void invalidateKeys(ICacheHandle handle) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Invalidating keys; handle = "+handle.toString()); } StringSet invalidateKeys = handle.getInvalidationKeys(); // Get the transaction handle in effect String transactionID = handle.getTransactionID(); if (transactionID == null) { performInvalidation(invalidateKeys); } else { CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID); if (transactionHandle == null) { ManifoldCFException ex = new ManifoldCFException("Bad transaction ID!",ManifoldCFException.GENERAL_ERROR); Logging.cache.error(Thread.currentThread().toString()+": invalidateKeys: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex); throw ex; } // Do the invalidation right in the transaction handle. transactionHandle.invalidateKeys(invalidateKeys); } } /** Perform an invalidation. Assume all appropriate locks are in place. *@param keys is the set of keys to invalidate. */ protected void performInvalidation(StringSet keys) throws ManifoldCFException { // Finally, perform the invalidation. if (keys != null) { long invalidationTime = System.currentTimeMillis(); // Loop through all keys Iterator iter = keys.getKeys(); while (iter.hasNext()) { String keyName = (String)iter.next(); if (Logging.cache.isDebugEnabled()) Logging.cache.debug(" Invalidating key = "+keyName+" as of time = "+new Long(invalidationTime).toString()); writeSharedData(keyName,invalidationTime); } cache.invalidateKeys(keys); } } /** Leave the cache. Must be paired with enterCache, above. *@param handle is the handle of the cache we are leaving. */ public void leaveCache(ICacheHandle handle) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Leaving cacher; handle = "+handle.toString()); } lockManager.leaveLocks(handle.getReadLockStrings(),null,handle.getWriteLockStrings()); } // The following methods are used to communicate transaction information to the cache. /** Begin a cache transaction. * This keeps track of the relationship between objects cached within transactions. *@param startingTransactionID is the id of the transaction that is starting. *@param enclosingTransactionID is the id of the transaction that is in effect, or null. */ public void startTransaction(String startingTransactionID, String enclosingTransactionID) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(Thread.currentThread().toString()+": Starting transaction: "+startingTransactionID+": "+this.toString()+": "+transactionHash.toString()); } CacheTransactionHandle parent; if (enclosingTransactionID == null) parent = null; else { parent = (CacheTransactionHandle)transactionHash.get(enclosingTransactionID); if (parent == null) { ManifoldCFException ex = new ManifoldCFException("Illegal parent transaction ID: "+enclosingTransactionID, ManifoldCFException.GENERAL_ERROR); Logging.cache.error(Thread.currentThread().toString()+": startTransaction: "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex); throw ex; } } transactionHash.put(startingTransactionID,new CacheTransactionHandle(parent)); if (Logging.cache.isDebugEnabled()) { Logging.cache.debug("Successfully created transaction: "+startingTransactionID+": "+this.toString()+": "+transactionHash.toString()); } } /** Commit a cache transaction. * This method MUST be called when a transaction successfully ends, or open locks will not be closed!!! * All cache activity that has taken place inside the transaction will be resolved, and the cache locks * held open will be released. */ public void commitTransaction(String transactionID) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(Thread.currentThread().toString()+": Committing transaction: "+transactionID+": "+this.toString()+": "+transactionHash.toString()); } CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID); if (handle == null) throw new ManifoldCFException("Cache manager: commit transaction without start!",ManifoldCFException.GENERAL_ERROR); // First, move all the locally cached entries into the global cache. // This is safe to do because we know that the transaction belongs to a single thread. CacheTransactionHandle parentTransaction = handle.getParentTransaction(); Iterator iter = handle.getCurrentObjects(); StringSet invalidationKeys = handle.getInvalidationKeys(); if (parentTransaction == null) { // It will be folded into the main cache! // First, trigger invalidation. performInvalidation(invalidationKeys); long currentTime = System.currentTimeMillis(); // Now, copy the objects into the main cache while (iter.hasNext()) { ICacheDescription desc = (ICacheDescription)iter.next(); Object o = handle.lookupObject(desc); // System.out.println("Moving object to main cache: "+desc.getCriticalSectionName()); cache.setObject(desc,o,desc.getObjectKeys(),currentTime); // Now, set expiration and LRU // Update the expiration time for this object. resetObjectExpiration(desc,currentTime); } // End all of the locks. // We always end the write locks before the read locks. if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Ending transaction write locks for transaction "+transactionID); lockManager.leaveLocks(null,null,handle.getWriteLocks().getArray(cacheLockPrefix)); if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Ending transaction read locks for transaction "+transactionID); lockManager.leaveLocks(handle.getReadLocks().getArray(cacheLockPrefix),null,null); if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Done ending "+transactionID); } else { // Copy the invalidation etc. into the wrapping transaction. // Invalidate the parent cache entries parentTransaction.invalidateKeys(invalidationKeys); // Copy the objects from child to parent while (iter.hasNext()) { ICacheDescription desc = (ICacheDescription)iter.next(); Object o = handle.lookupObject(desc); parentTransaction.saveObject(desc,o,desc.getObjectKeys()); } // Transfer all locks to parent from child parentTransaction.addLocks(handle.getReadLocks(),handle.getWriteLocks()); } // Finally, remove the transaction from the hash. transactionHash.remove(transactionID); } /** Roll back a cache transaction. * This method releases all objects cached against the ending transaction ID, and releases all locks * held for the transaction. */ public void rollbackTransaction(String transactionID) throws ManifoldCFException { if (Logging.cache.isDebugEnabled()) { Logging.cache.debug(Thread.currentThread().toString()+": Rolling back transaction: "+transactionID+": "+this.toString()+": "+transactionHash.toString()); } CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID); if (handle == null) throw new ManifoldCFException("Cache manager: rollback transaction without start!",ManifoldCFException.GENERAL_ERROR); // End all of the locks // We always end the write locks before the read locks. if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Ending rollback write locks for transaction "+transactionID); lockManager.leaveLocks(null,null,handle.getWriteLocks().getArray(cacheLockPrefix)); if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Ending rollback read locks for transaction "+transactionID); lockManager.leaveLocks(handle.getReadLocks().getArray(cacheLockPrefix),null,null); if (Logging.lock.isDebugEnabled()) Logging.lock.debug("Done rolling back "+transactionID); // Now, get rid of the transaction entry, because we just want to chuck what we filed there. transactionHash.remove(transactionID); } /** Timed invalidation. Call this periodically to get rid of all objects that have expired. *@param currentTimestamp is the current time in milliseconds since epoch. */ public void expireObjects(long currentTimestamp) throws ManifoldCFException { // This is a local JVM operation; we will not need to do any locks. We just // need to blow expired objects from the cache. cache.expireRecords(currentTimestamp); } // Protected methods and classes /** Read an invalidation file contents. *@param key is the cache key name. *@return the invalidation time, or 0 if none. */ protected long readSharedData(String key) throws ManifoldCFException { // Read cache resource byte[] cacheResourceData = lockManager.readData("cache-"+key); if (cacheResourceData == null) return 0L; String expiration = new String(cacheResourceData, StandardCharsets.UTF_8); return new Long(expiration).longValue(); } /** Write the invalidation file contents. *@param key is the cache key name. *@param value is the invalidation timestamp. */ protected void writeSharedData(String key, long value) throws ManifoldCFException { if (value == 0L) lockManager.writeData(key,null); else { lockManager.writeData("cache-"+key,Long.toString(value).getBytes(StandardCharsets.UTF_8)); } } /** This is the local implementation of ICacheHandle. */ protected class CacheHandle implements ICacheHandle { // Member variables protected String[] readLocks; protected String[] writeLocks; protected ICacheDescription[] objectDescriptions; protected StringSet invalidationKeys; protected String transactionID; public CacheHandle(String[] readLocks, String[] writeLocks, ICacheDescription[] descriptions, StringSet invalidationKeys, String transactionID) { this.readLocks = readLocks; this.writeLocks = writeLocks; this.objectDescriptions = descriptions; this.invalidationKeys = invalidationKeys; this.transactionID = transactionID; } /** Get the read lock strings. *@return read lock string array. */ public String[] getReadLockStrings() { return readLocks; } /** Get the write lock strings. *@return write lock string array. */ public String[] getWriteLockStrings() { return writeLocks; } /** Get the set of object descriptions. *@return the object descriptions. */ public ICacheDescription[] getObjectDescriptions() { return objectDescriptions; } /** Get the invalidation keys. *@return the invalidation key set. */ public StringSet getInvalidationKeys() { return invalidationKeys; } /** Get the transaction ID. *@return the transaction ID. */ public String getTransactionID() { return transactionID; } } /** This is the local implementation of ICacheCreateHandle */ protected class CacheCreateHandle implements ICacheCreateHandle { // Member variables protected String[] criticalSectionNames; protected long theTime; protected String transactionID; public CacheCreateHandle(String[] criticalSectionNames, String transactionID) { this.criticalSectionNames = criticalSectionNames; theTime = System.currentTimeMillis(); this.transactionID = transactionID; } /** Get the critical section names. *@return the critical section names. */ public String[] getCriticalSectionNames() { return criticalSectionNames; } /** Get create start time. *@return the time in milliseconds since epoch. */ public long getLookupTime() { return theTime; } /** Get the transaction ID. *@return the transaction ID. */ public String getTransactionID() { return transactionID; } } /** This is the class the cache manager uses to keep track of transaction * relationships and data. Since a transaction is local to a thread, this * class is local to a specific thread as well; it is thus referenced by a thread-local * hash table. */ protected class CacheTransactionHandle { // Member variables protected CacheTransactionHandle parentTransaction; // The cache in here must also be flushable via cache keys; the // objects are therefore in a cache key section as well as in a "lookup" section. /** This is the object hash (key is the description object, value is the stored object). */ protected HashMap objectHash = new HashMap(); /** This is the cache key map (key is the cache key, value is a hashmap containing the * object descriptions to be invalidated). */ protected HashMap cacheKeyMap = new HashMap(); /** This is the set of cache key read locks that are currently held by this transaction. */ protected HashMap cacheKeyReadLocks = new HashMap(); /** This is the set of cache key write locks that are currently held by this transaction. */ protected HashMap cacheKeyWriteLocks = new HashMap(); /** This is the current set of keys to invalidate if the transaction should be committed. */ protected StringSetBuffer invalidationKeys = new StringSetBuffer(); /** Constructor. *@param parentTransaction is the parent transaction identifier, or null if none. */ public CacheTransactionHandle(CacheTransactionHandle parentTransaction) { this.parentTransaction = parentTransaction; } /** Get parent transaction. *@return the parent transaction. */ public CacheTransactionHandle getParentTransaction() { return parentTransaction; } /** Get the set of write locks to close on exit from this transaction. *@return the write lock names as an array. */ public StringSet getWriteLocks() { return new StringSet(cacheKeyWriteLocks); } /** Get the set of read locks to close on exit from this transaction. *@return the read lock names as an array. */ public StringSet getReadLocks() { return new StringSet(cacheKeyReadLocks); } /** Get the current invalidation keys. *@return the keys as a stringset */ public StringSet getInvalidationKeys() { return new StringSet(invalidationKeys); } /** Look for an object in cache. *@param descriptionObject is the cache description object. *@return the object, if found, or null if not found. */ public Object lookupObject(ICacheDescription descriptionObject) { return objectHash.get(descriptionObject); } /** Save an object in cache. *@param descriptionObject is the description. *@param object is the object to save. *@param cacheKeys are the cache keys. */ public void saveObject(ICacheDescription descriptionObject, Object object, StringSet cacheKeys) { // Put into main hash objectHash.put(descriptionObject,object); // Put into the cache key hashes Iterator iter = cacheKeys.getKeys(); while (iter.hasNext()) { String key = (String)iter.next(); HashMap thisHash = (HashMap)cacheKeyMap.get(key); if (thisHash == null) { thisHash = new HashMap(); cacheKeyMap.put(key,thisHash); } thisHash.put(descriptionObject,descriptionObject); } } /** Invalidate objects. *@param keys is the set of keys to invalidate. */ public void invalidateKeys(StringSet keys) { if (keys == null) return; // Merge these keys into the invalidation set invalidationKeys.add(keys); // Now, bump stuff from the cache Iterator iter = keys.getKeys(); while (iter.hasNext()) { String keyName = (String)iter.next(); HashMap x = (HashMap)cacheKeyMap.get(keyName); if (x != null) { // Need to flush the stated items Iterator iter2 = x.keySet().iterator(); while (iter2.hasNext()) { ICacheDescription desc = (ICacheDescription)iter2.next(); objectHash.remove(desc); } cacheKeyMap.remove(keyName); } } } /** See if cache keys intersect with invalidation keys. *@param cacheKeys is the set of cache keys that describe an object. *@return true if these keys have been invalidated in this transaction */ public boolean checkCacheKeys(StringSet cacheKeys) { return invalidationKeys.contains(cacheKeys); } /** Come up with the set of read locks we still need to throw. *@param cacheKeys is the set of cache keys we need to have read locks for. *@param keys is the set of invalidation keys we need to have write locks for. *@return the array of read locks we still need to throw. */ public StringSet getRemainingReadLocks(StringSet cacheKeys, StringSet keys) { StringSetBuffer accumulator = new StringSetBuffer(); if (cacheKeys != null) { Iterator iter = cacheKeys.getKeys(); while (iter.hasNext()) { String cacheKey = (String)iter.next(); // If this already has a lock, of any kind, continue if (keys != null && keys.contains(cacheKey)) continue; // Look back through the whole parent chain to see whether we need to throw // this lock. CacheTransactionHandle ptr = this; boolean found = false; while (ptr != null) { if (ptr.cacheKeyReadLocks.get(cacheKey) != null || ptr.cacheKeyWriteLocks.get(cacheKey) != null) { found = true; break; } ptr = ptr.parentTransaction; } if (found) continue; accumulator.add(cacheKey); } } return new StringSet(accumulator); } /** Come up with the set of write locks we still need to throw. *@param cacheKeys is the set of cache keys we need to have read locks for. *@param keys is the set of invalidation keys we need to have write locks for. *@return the array of write locks we still need to throw. */ public StringSet getRemainingWriteLocks(StringSet cacheKeys, StringSet keys) throws ManifoldCFException { // If any of these keys are read locks but not yet write locks, we throw an exception! // (There is currently no ability to promote a read lock to a write lock.) StringSetBuffer accumulator = new StringSetBuffer(); if (keys != null) { Iterator iter = keys.getKeys(); while (iter.hasNext()) { String invKey = (String)iter.next(); // Look back through the whole parent chain to see whether we need to throw // this lock. CacheTransactionHandle ptr = this; boolean found = false; while (ptr != null) { // Write check first! Then, read check. if (ptr.cacheKeyWriteLocks.get(invKey) != null) { found = true; break; } // Upgrade lock attempts are now permitted. if (ptr.cacheKeyReadLocks.get(invKey) != null) break; ptr = ptr.parentTransaction; } if (found) continue; accumulator.add(invKey); } } return new StringSet(accumulator); } /** Add to the set of locks that are open. *@param thrownReadLocks is the set of read locks. *@param thrownWriteLocks is the set of write locks. */ public void addLocks(StringSet thrownReadLocks, StringSet thrownWriteLocks) { // Some of the write locks will be upgrades. In this case, the same lock // might appear BOTH as read and as write. I hope the lock manager does the // right thing in this case! // In any case, at worst we will have one write lock and one read lock for the // same lock name, and never a write lock then a read lock, so we should be able to disambiguate if // needed. Iterator iter = thrownReadLocks.getKeys(); while (iter.hasNext()) { String x = (String)iter.next(); cacheKeyReadLocks.put(x,x); } iter = thrownWriteLocks.getKeys(); while (iter.hasNext()) { String x = (String)iter.next(); cacheKeyWriteLocks.put(x,x); } } /** Get all existing object descriptions. *@return an iterator of ICacheDescription objects. */ public Iterator getCurrentObjects() { return objectHash.keySet().iterator(); } } }