/*
* JBoss, Home of Professional Open Source.
* Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.cache.interceptors;
import org.jboss.cache.CacheException;
import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InternalNode;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.commands.read.GetChildrenNamesCommand;
import org.jboss.cache.commands.read.GetDataMapCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetKeysCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.tx.RollbackCommand;
import org.jboss.cache.commands.write.ClearDataCommand;
import org.jboss.cache.commands.write.MoveCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutForExternalReadCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.commands.write.RemoveKeyCommand;
import org.jboss.cache.commands.write.RemoveNodeCommand;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.Configuration.NodeLockingScheme;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.interceptors.base.JmxStatsCommandInterceptor;
import org.jboss.cache.jmx.annotations.ManagedAttribute;
import org.jboss.cache.jmx.annotations.ManagedOperation;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.mvcc.MVCCNodeHelper;
import org.jboss.cache.mvcc.NullMarkerNode;
import org.jboss.cache.notifications.Notifier;
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 7610 2009-01-29 12:43:31Z manik.surtani@jboss.com $
*/
public class CacheLoaderInterceptor extends JmxStatsCommandInterceptor
{
private long cacheLoads = 0;
private long cacheMisses = 0;
private CacheLoaderManager clm;
protected TransactionTable txTable = null;
protected CacheLoader loader;
protected DataContainer dataContainer;
protected Notifier notifier;
protected boolean isActivation = false;
// protected boolean usingVersionedInvalidation = false;
protected MVCCNodeHelper helper;
/**
* 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,
DataContainer dataContainer, Notifier notifier, MVCCNodeHelper helper)
{
this.txTable = txTable;
this.clm = clm;
// CacheMode mode = configuration.getCacheMode();
// usingVersionedInvalidation = mode.isInvalidation();
this.dataContainer = dataContainer;
this.notifier = notifier;
this.helper = helper;
}
@Start
protected void startInterceptor()
{
loader = clm.getCacheLoader();
}
@Override
public Object visitPutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
{
if (command.getFqn() != null)
{
if (command.isErase())
{
replace(ctx, command.getFqn());
}
else
{
loadIfNeeded(ctx, command.getFqn(), null, true, true, false, false, false, false, true);
}
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
{
if (command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, useCacheStore, !useCacheStore, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitPutForExternalReadCommand(InvocationContext ctx, PutForExternalReadCommand command) throws Throwable
{
return visitPutKeyValueCommand(ctx, command);
}
@Override
public Object visitMoveCommand(InvocationContext ctx, MoveCommand command) throws Throwable
{
if (command.getFqn() != null)
{
if (command.getTo() != null)
{
loadIfNeeded(ctx, command.getTo(), null, false, false, true, false, true, false, true);
}
loadIfNeeded(ctx, command.getFqn(), null, false, false, true, true, true, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
{
if (command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, false, true, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
{
if (command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), null, false, false, true, false, false, true, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitGetChildrenNamesCommand(InvocationContext ctx, GetChildrenNamesCommand command) throws Throwable
{
Fqn fqn = command.getFqn();
if (fqn != null)
{
loadIfNeeded(ctx, fqn, null, false, false, false, false, false, true, true);
loadChildren(fqn, dataContainer.peekInternalNode(fqn, true), false, false, ctx);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitGetKeysCommand(InvocationContext ctx, GetKeysCommand command) throws Throwable
{
if (command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), null, true, false, true, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitGetDataMapCommand(InvocationContext ctx, GetDataMapCommand command) throws Throwable
{
if (command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), null, true, false, true, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitRollbackCommand(InvocationContext ctx, RollbackCommand command) throws Throwable
{
// clean up nodesCreated map
if (trace) log.trace("Removing temporarily created nodes");
// this needs to be done in reverse order.
List list = ctx.getTransactionContext().getDummyNodesCreatedByCacheLoader();
if (list != null && list.size() > 0)
{
ListIterator i = list.listIterator(list.size());
while (i.hasPrevious())
{
Fqn fqn = (Fqn) i.previous();
try
{
dataContainer.evict(fqn, false);
}
catch (CacheException e)
{
if (trace) log.trace("Unable to evict node " + fqn, e);
}
}
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitRemoveNodeCommand(InvocationContext ctx, RemoveNodeCommand command) throws Throwable
{
if (configuration.getNodeLockingScheme() == NodeLockingScheme.OPTIMISTIC && command.getFqn() != null)
{
loadIfNeeded(ctx, command.getFqn(), null, false, false, false, false, false, false, false);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitRemoveKeyCommand(InvocationContext ctx, RemoveKeyCommand command) throws Throwable
{
if (command.getFqn() != null && !useCacheStore)
{
loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, false, false, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
@Override
public Object visitClearDataCommand(InvocationContext ctx, ClearDataCommand command) throws Throwable
{
Fqn fqn = command.getFqn();
if (fqn != null && !useCacheStore)
{
loadIfNeeded(ctx, fqn, null, false, true, false, false, false, false, true);
}
return invokeNextInterceptor(ctx, command);
}
private void replace(InvocationContext ctx, Fqn fqn) throws InterruptedException
{
NodeSPI n = helper.wrapNodeForReading(ctx, fqn, true);
if (n instanceof NullMarkerNode)
{
ctx.getLookedUpNodes().remove(fqn);
}
n = helper.wrapNodeForWriting(ctx, fqn, true, true, true, false, false);
n.setDataLoaded(true);
}
private void loadIfNeeded(InvocationContext ctx, Fqn fqn, Object key, boolean allKeys, boolean initNode, boolean acquireWriteLock, boolean recursive, boolean isMove, boolean bypassLoadingData, boolean shouldLoadIfNodeIsNull) throws Throwable
{
NodeSPI n = helper.wrapNodeForReading(ctx, fqn, true);
if (n instanceof NullMarkerNode)
{
ctx.getLookedUpNodes().remove(fqn);
n = null;
}
boolean mustLoad = mustLoad(fqn, n, key, allKeys || isMove, shouldLoadIfNodeIsNull);
if (trace) log.trace("load element " + fqn + " mustLoad=" + mustLoad);
if (mustLoad)
{
if (acquireWriteLock || initNode)
{
boolean isNew = n == null;
n = helper.wrapNodeForWriting(ctx, fqn, true, false, true, false, true); // won't create any nodes but will acquire locks.
if (isNew && n != null) n.setDataLoaded(false);
}
// This is really convoluted
if (n == null || !n.isDeleted())
{
boolean exists;
Map nodeData = null;
if (bypassLoadingData)
{
exists = loader.exists(fqn);
}
else
{
nodeData = loadData(ctx, fqn);
exists = nodeData != null;
}
if (n == null && exists)
{
// just create a dummy node in memory
n = helper.wrapNodeForWriting(ctx, fqn, true, true, true, false, false);
n.setDataLoaded(false);
}
if (nodeData != null && n != null)
{
setNodeState(ctx, fqn, n, nodeData);
}
}
}
// The complete list of children aren't known without loading them
if (recursive && n != null)
{
loadChildren(fqn, n.getDelegationTarget(), recursive, isMove, ctx);
}
}
/**
* Load the children.
*
* @param node may be null if the node was not found.
* @param ctxt
*/
private void loadChildren(Fqn fqn, InternalNode node, boolean recursive, boolean isMove, InvocationContext ctxt) throws Throwable
{
if (node != null && node.isChildrenLoaded())
{
if (trace) log.trace("Children already loaded!");
return;
}
Set childrenNames;
try
{
childrenNames = loader.getChildrenNames(fqn);
}
catch (Exception e)
{
if (log.isInfoEnabled()) log.info("Cache loader was unable to load state", e);
// return!
return;
}
if (trace)
{
log.trace("load children " + fqn + " children=" + childrenNames);
}
// For getChildrenNames null means no children
if (childrenNames == null)
{
if (node != null)
{
if (useCacheStore)
{
node.removeChildren();
}
node.setChildrenLoaded(true);
}
return;
}
// Create if node had not been created already
if (node == null)
{
NodeSPI temp = helper.wrapNodeForWriting(ctxt, fqn, true, true, true, false, false);
node = temp.getDelegationTarget();
}
// Create one DataNode per child, mark as UNINITIALIZED
for (Object name : childrenNames)
{
Fqn childFqn = Fqn.fromRelativeElements(fqn, name);
// create child if it didn't exist
NodeSPI child = helper.wrapNodeForWriting(ctxt, childFqn, true, true, true, false, false);
if (child.isCreated()) child.setDataLoaded(false);
if ((isMove || isActivation) && recursive)
{
// load data for children as well!
child.setInternalState(loadData(ctxt, child.getFqn()));
child.setDataLoaded(true);
}
if (recursive)
{
loadChildren(child.getFqn(), child.getDelegationTarget(), true, isMove, ctxt);
}
}
node.setChildrenLoaded(true);
}
private boolean mustLoad(Fqn fqn, NodeSPI n, Object key, boolean allKeys, boolean shouldLoadIfNodeIsNull)
{
if (n == null)
{
if (trace) log.trace("Node [" + fqn + "] is null in memory. Must load? " + shouldLoadIfNodeIsNull);
return shouldLoadIfNodeIsNull;
}
// check this first!!!
if (!n.isValid()) // && configuration.getNodeLockingScheme().isVersionedScheme())
{
// attempt to load again; this only happens if we have tombstones lying around, or we are using invalidation.
if (trace)
{
log.trace("Loading node [" + fqn + "] 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 [" + fqn + "], key requested is null");
return false;
}
if (n.containsKeyDirect(key))
{
if (trace) log.trace("Don't load [" + fqn + "], already have necessary key in memory");
return false;
}
}
if (!n.isDataLoaded())
{
if (trace) log.trace("Must load node [" + fqn + "], uninitialized");
return true;
}
if (trace) log.trace("Don't load node [" + fqn + "], by default");
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 void setNodeState(InvocationContext ctx, Fqn fqn, NodeSPI n, Map nodeData) throws Exception
{
if (trace) log.trace("setNodeState node is " + n);
if (nodeData != null)
{
notifier.notifyNodeLoaded(fqn, true, Collections.emptyMap(), ctx);
if (isActivation)
{
notifier.notifyNodeActivated(fqn, true, Collections.emptyMap(), ctx);
}
n.setInternalState(nodeData);
// set this node as valid?
// if (usingVersionedInvalidation) n.setValid(true, false);
n.setValid(true, false);
notifier.notifyNodeLoaded(fqn, false, nodeData, ctx);
if (isActivation)
{
notifier.notifyNodeActivated(fqn, false, nodeData, ctx);
}
}
if (!n.isDataLoaded())
{
if (trace) log.trace("Setting dataLoaded to true");
n.setDataLoaded(true);
}
}
private Map loadData(InvocationContext ctx, Fqn fqn) throws Exception
{
if (trace) log.trace("Attempting to load data for " + fqn);
Map nodeData = loader.get(fqn);
boolean nodeExists = (nodeData != null);
if (trace) log.trace("Node " + fqn + " exists? " + nodeExists);
if (nodeExists) recordNodeLoaded(ctx, fqn);
if (getStatisticsEnabled())
{
if (nodeExists)
{
cacheLoads++;
}
else
{
cacheMisses++;
}
}
return nodeData;
}
@ManagedAttribute(description = "number of cache loader node loads")
public long getCacheLoaderLoads()
{
return cacheLoads;
}
@ManagedAttribute(description = "number of cache loader node misses")
public long getCacheLoaderMisses()
{
return cacheMisses;
}
@ManagedOperation
@Override
public void resetStatistics()
{
cacheLoads = 0;
cacheMisses = 0;
}
@ManagedOperation
@Override
public Map<String, Object> dumpStatistics()
{
Map<String, Object> retval = new HashMap<String, Object>();
retval.put("CacheLoaderLoads", cacheLoads);
retval.put("CacheLoaderMisses", cacheMisses);
return retval;
}
protected void recordNodeLoaded(InvocationContext ctx, Fqn fqn)
{
// this is a no-op. Only used by subclasses.
}
}