package org.jboss.cache.interceptors;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.Node;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.config.Configuration;
import static org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.lock.NodeLock;
import org.jboss.cache.marshall.MethodCall;
import org.jboss.cache.marshall.MethodCallFactory;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.TransactionEntry;
import org.jboss.cache.transaction.TransactionTable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Loads nodes that don't exist at the time of the call into memory from the CacheLoader
*
* @author Bela Ban
* @version $Id: CacheLoaderInterceptor.java 4986 2008-01-04 16:55:01Z manik.surtani@jboss.com $
*/
public class CacheLoaderInterceptor extends MethodDispacherInterceptor implements CacheLoaderInterceptorMBean
{
private long m_cacheLoads = 0;
private long m_cacheMisses = 0;
private TransactionTable txTable = null;
protected boolean isActivation = false;
protected CacheLoader loader;
protected CacheLoaderManager clm;
protected boolean usingOptimisticInvalidation = false;
public CacheLoaderInterceptor()
{
initLogger();
}
/**
* True if CacheStoreInterceptor is in place.
* This allows us to skip loading keys for remove(Fqn, key) and put(Fqn, key).
* It also affects removal of node data and listing children.
*/
protected boolean useCacheStore = true;
@Inject
protected void injectDependencies(TransactionTable txTable, CacheLoaderManager clm, Configuration configuration)
{
this.txTable = txTable;
this.clm = clm;
CacheMode mode = configuration.getCacheMode();
usingOptimisticInvalidation = configuration.isNodeLockingOptimistic() && mode.isInvalidation();
}
@Start
protected void startInterceptor()
{
loader = clm.getCacheLoader();
}
@Override
protected Object handlePutDataEraseMethod(InvocationContext ctx, GlobalTransaction gt, Fqn fqn, Map newData, boolean createUndoOps, boolean eraseContents) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, true, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handlePutDataMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Map data, boolean createUndoOps) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, true, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handlePutForExternalReadMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Object key, Object value) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, key, false, useCacheStore, !useCacheStore, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handlePutKeyValueMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Object key, Object value, boolean createUndoOps) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, key, false, useCacheStore, !useCacheStore, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleMoveMethod(InvocationContext ctx, Fqn from, Fqn to) throws Throwable
{
if (from != null)
{
if (to != null)
{
loadIfNeeded(ctx, to, null, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, true, false);
}
loadIfNeeded(ctx, from, null, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), true, true, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleAddChildMethod(InvocationContext ctx, GlobalTransaction tx, Fqn parentFqn, Object childName, Node cn, boolean createUndoOps) throws Throwable
{
if (parentFqn != null)
{
loadIfNeeded(ctx, parentFqn, null, false, false, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleGetKeyValueMethod(InvocationContext ctx, Fqn fqn, Object key, boolean sendNodeEvent) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, key, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleGetNodeMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, !usingOptimisticInvalidation);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleGetChildrenNamesMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, true);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleReleaseAllLocksMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handlePrintMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleGetKeysMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, true, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleGetDataMapMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, true, false, true, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleRollbackMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
// clean up nodesCreated map
if (trace) log.trace("Removing temporarily created nodes from treecache");
// this needs to be done in reverse order.
List list = getTransactionEntry(ctx).getDummyNodesCreatedByCacheLoader();
if (list != null && list.size() > 0)
{
ListIterator i = list.listIterator(list.size());
while (i.hasPrevious())
{
Fqn fqn = (Fqn) i.previous();
try
{
cache.evict(fqn, false);
}
catch (CacheException e)
{
if (trace) log.trace("Unable to evict node " + fqn, e);
}
}
}
return nextInterceptor(ctx);
}
@Override
protected Object handleRemoveNodeMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, boolean createUndoOps) throws Throwable
{
if (cache.getConfiguration().isNodeLockingOptimistic() && fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleRemoveKeyMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Object key, boolean createUndoOps) throws Throwable
{
if (fqn != null && !useCacheStore)
{
loadIfNeeded(ctx, fqn, key, false, false, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
@Override
protected Object handleRemoveDataMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, boolean createUndoOps) throws Throwable
{
if (fqn != null && !useCacheStore)
{
loadIfNeeded(ctx, fqn, null, false, true, false, ctx.getMethodCall(), getTransactionEntry(ctx), false, false, false);
}
return nextInterceptor(ctx);
}
private void loadIfNeeded(InvocationContext ctx, Fqn fqn, Object key, boolean allKeys, boolean initNode, boolean acquireLock, MethodCall m, TransactionEntry entry, boolean recursive, boolean isMove, boolean bypassLoadingData) throws Throwable
{
NodeSPI n = peekNode(ctx, fqn, false, true, true);
boolean mustLoad = mustLoad(n, key, allKeys);
if (trace)
{
log.trace("load element " + fqn + " mustLoad=" + mustLoad);
}
if (mustLoad)
{
if (initNode)
{
n = createTempNode(fqn, entry);
}
// Only attempt to acquire this lock if we need to - i.e., if
// the lock hasn't already been acquired by the Lock
// interceptor. CRUD methods (put, remove) would have acquired
// this lock - even if the node is not in memory and needs to be
// loaded. Non-CRUD methods (put) would NOT have acquired this
// lock so if we are to load the node from cache loader, we need
// to acquire a write lock here. as a 'catch-all', DO NOT
// attempt to acquire a lock here *anyway*, even for CRUD
// methods - this leads to a deadlock when you have threads
// simultaneously trying to create a node. See
// org.jboss.cache.loader.deadlock.ConcurrentCreationDeadlockTest
// - Manik Surtani (21 March 2006)
if (acquireLock)
{
lock(fqn, NodeLock.LockType.WRITE, false);// non-recursive for now
}
// if (!initNode && !wasRemovedInTx(fqn, ctx.getGlobalTransaction()))
if (!wasRemovedInTx(fqn, ctx.getGlobalTransaction()))
{
if (bypassLoadingData)
{
if (n == null && loader.exists(fqn))
{
// just create a dummy node in memory
n = createTempNode(fqn, entry);
}
}
else
{
n = loadNode(ctx, fqn, n, entry);
}
}
}
// The complete list of children aren't known without loading them
if (recursive || m.getMethodId() == MethodDeclarations.getChildrenNamesMethodLocal_id)
{
loadChildren(fqn, n, recursive, isMove);
}
}
/**
* Load the children.
*
* @param node may be null if the node was not found.
*/
private void loadChildren(Fqn fqn, NodeSPI node, boolean recursive, boolean isMove) throws Throwable
{
if (node != null && node.isChildrenLoaded())
{
if (trace) log.trace("Children already loaded!");
return;
}
Set children_names = loader.getChildrenNames(fqn);
if (trace)
{
log.trace("load children " + fqn + " children=" + children_names);
}
// For getChildrenNames null means no children
if (children_names == null)
{
if (node != null)
{
if (useCacheStore)
{
node.removeChildrenDirect();//getChildrenMapDirect().clear();
}
node.setChildrenLoaded(true);
}
return;
}
// Create if node had not been created already
if (node == null)
{
node = createNodes(fqn, null);// dont care about local transactions
}
// Create one DataNode per child, mark as UNINITIALIZED
for (Object name : children_names)
{
Fqn child_fqn = new Fqn(name);// this is a RELATIVE Fqn!!
// create child if it didn't exist
NodeSPI child = node.addChildDirect(child_fqn);
if ((isMove || isActivation) && recursive)
{
// load data for children as well!
child.setInternalState(loader.get(child.getFqn()));
child.setDataLoaded(true);
}
// why are we doing this?!??
// else
// {
// child.setDataLoaded(false);
// }
if (recursive)
{
loadChildren(child.getFqn(), child, true, isMove);
}
}
lock(fqn, recursive ? NodeLock.LockType.WRITE : NodeLock.LockType.READ, true);// recursive=true: lock entire subtree
node.setChildrenLoaded(true);
}
private boolean mustLoad(NodeSPI n, Object key, boolean allKeys)
{
if (n == null)
{
if (trace) log.trace("must load, node null");
return true;
}
// check this first!!!
if (!n.isValid() && cache.getConfiguration().isNodeLockingOptimistic())
{
// attempt to load again; this only happens if we have tombstones lying around, or we are using invalidation.
if (trace) log.trace("loading again from cache loader since in-memory node is marked as invalid");
return true;
}
// JBCACHE-1172 Skip single-key optimization if request needs all keys
if (!allKeys)
{
// if we are not looking for a specific key don't bother loading!
if (key == null)
{
if (trace) log.trace("don't load, key requested is null");
return false;
}
if (n.getKeysDirect().contains(key))
{
if (trace) log.trace("don't load, already have necessary key in memory");
return false;
}
}
if (!n.isDataLoaded())
{
if (trace) log.trace("must Load, uninitialized");
return true;
}
return false;
}
public long getCacheLoaderLoads()
{
return m_cacheLoads;
}
public long getCacheLoaderMisses()
{
return m_cacheMisses;
}
@Override
public void resetStatistics()
{
m_cacheLoads = 0;
m_cacheMisses = 0;
}
@Override
public Map<String, Object> dumpStatistics()
{
Map<String, Object> retval = new HashMap<String, Object>();
retval.put("CacheLoaderLoads", m_cacheLoads);
retval.put("CacheLoaderMisses", m_cacheMisses);
return retval;
}
protected void lock(Fqn fqn, NodeLock.LockType lock_type, boolean recursive) throws Throwable
{
if (configuration.isNodeLockingOptimistic()) return;
MethodCall m = MethodCallFactory.create(MethodDeclarations.lockMethodLocal_id,
fqn, lock_type, recursive);
// hacky
cache.getInterceptorChain().get(0).invoke(InvocationContext.fromMethodCall(m));
// nextInterceptor(m);
}
private TransactionEntry getTransactionEntry(InvocationContext ctx)
{
GlobalTransaction gtx = ctx.getGlobalTransaction();
if (gtx != null)
{
return txTable.get(gtx);
}
return null;
}
/**
* Returns true if the FQN or parent was removed during the current
* transaction.
* This is O(N) WRT to the number of modifications so far.
*/
private boolean wasRemovedInTx(Fqn fqn, GlobalTransaction t)
{
if (t == null)
{
return false;
}
TransactionEntry entry = txTable.get(t);
for (MethodCall m : entry.getCacheLoaderModifications())
{
if (m.getMethodId() == MethodDeclarations.removeNodeMethodLocal_id
&& fqn.isChildOrEquals((Fqn) m.getArgs()[1]))
{
return true;
}
}
return false;
}
/**
* Loads a node from disk; if it exists creates parent TreeNodes.
* If it doesn't exist on disk but in memory, clears the
* uninitialized flag, otherwise returns null.
*/
private NodeSPI loadNode(InvocationContext ctx, Fqn fqn, NodeSPI n, TransactionEntry entry) throws Exception
{
if (trace) log.trace("loadNode " + fqn);
Map nodeData = loadData(fqn);
if (nodeData != null)
{
if (trace) log.trace("Node data is not null, loading");
cache.getNotifier().notifyNodeLoaded(fqn, true, Collections.emptyMap(), ctx);
if (isActivation)
{
cache.getNotifier().notifyNodeActivated(fqn, true, Collections.emptyMap(), ctx);
}
n = createNodes(fqn, entry);
// n.clearDataDirect();
n.setInternalState(nodeData);
// set this node as valid?
if (usingOptimisticInvalidation) n.setValid(true, false);
cache.getNotifier().notifyNodeLoaded(fqn, false, nodeData, ctx);
if (isActivation)
{
cache.getNotifier().notifyNodeActivated(fqn, false, nodeData, ctx);
}
}
if (n != null && !n.isDataLoaded())
{
if (trace) log.trace("Setting dataLoaded to true");
n.setDataLoaded(true);
}
return n;
}
/**
* Creates a new memory node in preparation for storage.
*/
private NodeSPI createTempNode(Fqn fqn, TransactionEntry entry) throws Exception
{
NodeSPI n = createNodes(fqn, entry);
n.setDataLoaded(false);
if (trace)
{
log.trace("createTempNode n " + n);
}
return n;
}
private NodeSPI createNodes(Fqn fqn, TransactionEntry entry) throws Exception
{
Fqn tmp_fqn = Fqn.ROOT;
int size = fqn.size();
// root node
NodeSPI n = cache.getRoot();
for (int i = 0; i < size; i++)
{
Object child_name = fqn.get(i);
tmp_fqn = new Fqn(tmp_fqn, child_name);
NodeSPI child_node = findChild(n, child_name);
boolean last = (i == size - 1);
if (child_node == null)
{
if (last)
{
child_node = n.addChildDirect(new Fqn(child_name));
child_node.setDataLoaded(true);
}
else
{
child_node = n.addChildDirect(new Fqn(child_name));
child_node.setDataLoaded(false);
}
if (entry != null)
{
entry.loadUninitialisedNode(tmp_fqn);
}
}
n = child_node;
}
return n;
}
private NodeSPI findChild(NodeSPI node, Object child_name)
{
Map children = node.getChildrenMapDirect();
if (children == null) return null;
return (NodeSPI) children.get(child_name);
}
private Map loadData(Fqn fqn) throws Exception
{
Map nodeData = loader.get(fqn);
boolean nodeExists = (nodeData != null);
if (trace) log.trace("nodeExists " + nodeExists);
if (configuration.getExposeManagementStatistics() && getStatisticsEnabled())
{
if (nodeExists)
{
m_cacheLoads++;
}
else
{
m_cacheMisses++;
}
}
return nodeData;
}
}