/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.cache.interceptors;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.buddyreplication.GravitateResult;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.marshall.MethodCall;
import org.jboss.cache.marshall.MethodCallFactory;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.TransactionEntry;
import org.jgroups.Address;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.RspFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* The Data Gravitator interceptor intercepts cache misses and attempts to
* gravitate data from other parts of the cluster.
* <p/>
* Only used if Buddy Replication is enabled. Also, the interceptor only kicks
* in if an {@link org.jboss.cache.config.Option} is passed in to force Data
* Gravitation for a specific invocation or if <b>autoDataGravitation</b> is
* set to <b>true</b> when configuring Buddy Replication.
* <p/>
* See the JBoss Cache User Guide for more details on configuration options.
* There is a section dedicated to Buddy Replication in the Replication
* chapter.
*
* @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
*/
public class DataGravitatorInterceptor extends BaseRpcInterceptor
{
private BuddyManager buddyManager;
private boolean syncCommunications = false;
private Map<GlobalTransaction, MethodCall> transactionMods = new ConcurrentHashMap<GlobalTransaction, MethodCall>();
@Override
public void setCache(CacheSPI cache)
{
super.setCache(cache);
this.buddyManager = cache.getBuddyManager();
syncCommunications = configuration.getCacheMode() == Configuration.CacheMode.REPL_SYNC || configuration.getCacheMode() == Configuration.CacheMode.INVALIDATION_SYNC;
}
public DataGravitatorInterceptor()
{
initLogger();
}
@Override
protected boolean skipMethodCall(InvocationContext ctx)
{
return MethodDeclarations.isBlockUnblockMethod(ctx.getMethodCall().getMethodId()) ||
ctx.getOptionOverrides().isSkipDataGravitation();
}
@Override
protected Object handleGetChildrenNamesMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handleGetDataMapMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handleExistsMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handleGetKeysMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handleGetKeyValueMethod(InvocationContext ctx, Fqn fqn, Object key, boolean sendNodeEvent) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handleGetNodeMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleGetMethod(ctx, fqn);
}
@Override
protected Object handlePrepareMethod(InvocationContext ctx, GlobalTransaction gtx, List modification, Address coordinator, boolean onePhaseCommit) throws Throwable
{
try
{
Object returnValue = nextInterceptor(ctx);
doPrepare(ctx.getGlobalTransaction(), ctx);
return returnValue;
}
catch (Throwable throwable)
{
transactionMods.remove(ctx.getGlobalTransaction());
throw throwable;
}
}
@Override
protected Object handleOptimisticPrepareMethod(InvocationContext ctx, GlobalTransaction gtx, List modifications, Map data, Address address, boolean onePhaseCommit) throws Throwable
{
return handlePrepareMethod(ctx, gtx, modifications, address, onePhaseCommit);
}
@Override
protected Object handleRollbackMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
try
{
transactionMods.remove(ctx.getGlobalTransaction());
return nextInterceptor(ctx);
}
catch (Throwable throwable)
{
transactionMods.remove(ctx.getGlobalTransaction());
throw throwable;
}
}
@Override
protected Object handleCommitMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
try
{
doCommit(ctx.getGlobalTransaction(), ctx);
transactionMods.remove(ctx.getGlobalTransaction());
return nextInterceptor(ctx);
}
catch (Throwable throwable)
{
transactionMods.remove(ctx.getGlobalTransaction());
throw throwable;
}
}
private Object handleGetMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
if (isGravitationEnabled(ctx))
{
// test that the Fqn being requested exists locally in the cache.
if (trace) log.trace("Checking local existence of fqn " + fqn);
if (BuddyManager.isBackupFqn(fqn))
{
log.info("Is call for a backup Fqn, not performing any gravitation. Direct calls on internal backup nodes are *not* supported.");
}
else
{
if (peekNode(ctx, fqn, false, false, false) == null)
{
if (trace) log.trace("Gravitating from local backup tree");
BackupData data = localBackupGet(fqn, ctx);
if (data == null)
{
if (trace) log.trace("Gravitating from remote backup tree");
// gravitate remotely.
data = remoteBackupGet(fqn);
}
if (data != null)
{
// create node locally so I don't gravitate again
// when I do the put() call to the cluster!
//createNode(data.backupData, true);
// Make sure I replicate to my buddies.
if (trace)
log.trace("Passing the put call locally to make sure state is persisted and ownership is correctly established.");
createNode(ctx, data.backupData, false);
// very strange, the invocation contexts get twisted up here, and will need preservation.
// a bit crappy and hacky, all will be solved when we move to JBoss AOP in 2.1.0
//ctx.setMethodCall(m);
// Clean up the other nodes
cleanBackupData(data, ctx.getGlobalTransaction(), ctx);
}
}
else
{
if (trace) log.trace("No need to gravitate; have this already.");
}
}
}
else
{
if (trace)
{
log.trace("Suppressing data gravitation for this call.");
}
}
return nextInterceptor(ctx);
}
private boolean isGravitationEnabled(InvocationContext ctx)
{
boolean enabled = ctx.isOriginLocal();
if (enabled)
{
if (!buddyManager.isAutoDataGravitation())
{
enabled = ctx.getOptionOverrides().getForceDataGravitation();
}
}
return enabled;
}
private void doPrepare(GlobalTransaction gtx, InvocationContext ctx) throws Throwable
{
MethodCall cleanup = transactionMods.get(gtx);
if (trace) log.trace("Broadcasting prepare for cleanup ops " + cleanup);
if (cleanup != null)
{
MethodCall prepare;
List<MethodCall> mods = new ArrayList<MethodCall>(1);
mods.add(cleanup);
if (configuration.isNodeLockingOptimistic())
{
prepare = MethodCallFactory.create(MethodDeclarations.optimisticPrepareMethod_id, gtx, mods, null, cache.getLocalAddress(), false);
}
else
{
prepare = MethodCallFactory.create(MethodDeclarations.prepareMethod_id, gtx, mods, cache.getLocalAddress(), syncCommunications);
}
replicateCall(ctx, getMembersOutsideBuddyGroup(), prepare, syncCommunications, ctx.getOptionOverrides());
}
else
{
if (trace) log.trace("Nothing to broadcast in prepare phase for gtx " + gtx);
}
}
private void doCommit(GlobalTransaction gtx, InvocationContext ctx) throws Throwable
{
if (transactionMods.containsKey(gtx))
{
if (trace) log.trace("Broadcasting commit for gtx " + gtx);
replicateCall(ctx, getMembersOutsideBuddyGroup(), MethodCallFactory.create(MethodDeclarations.commitMethod_id, gtx), syncCommunications, ctx.getOptionOverrides(), true, true);
}
else
{
if (trace) log.trace("Nothing to broadcast in commit phase for gtx " + gtx);
}
}
private List<Address> getMembersOutsideBuddyGroup()
{
List<Address> members = new ArrayList<Address>(cache.getMembers());
members.remove(cache.getLocalAddress());
members.removeAll(buddyManager.getBuddyAddresses());
return members;
}
private BackupData remoteBackupGet(Fqn name) throws Exception
{
BackupData result = null;
GravitateResult gr = gravitateData(name);
if (gr.isDataFound())
{
if (trace)
{
log.trace("Got response " + gr);
}
result = new BackupData(name, gr);
}
return result;
}
private void cleanBackupData(BackupData backup, GlobalTransaction gtx, InvocationContext ctx) throws Throwable
{
MethodCall cleanup = MethodCallFactory.create(MethodDeclarations.dataGravitationCleanupMethod_id, backup.primaryFqn, backup.backupFqn);
if (trace) log.trace("Performing cleanup on [" + backup.primaryFqn + "]");
if (gtx == null)
{
// broadcast removes
// remove main Fqn
if (trace) log.trace("Performing cleanup on [" + backup.backupFqn + "]");
// remove backup Fqn
replicateCall(ctx, cache.getMembers(), cleanup, syncCommunications, ctx.getOptionOverrides(), false, false);
}
else
{
if (trace)
{
log.trace("Data gravitation performed under global transaction " + gtx + ". Not broadcasting cleanups until the tx commits. Adding to tx mod list instead.");
}
transactionMods.put(gtx, cleanup);
TransactionEntry te = getTransactionEntry(gtx);
te.addModification(cleanup);
}
}
private GravitateResult gravitateData(Fqn fqn) throws Exception
{
if (trace)
{
log.trace("cache=" + cache.getLocalAddress() + "; requesting data gravitation for Fqn " + fqn);
}
List<Address> mbrs = cache.getMembers();
Boolean searchSubtrees = (buddyManager.isDataGravitationSearchBackupTrees() ? Boolean.TRUE : Boolean.FALSE);
MethodCall dGrav = MethodCallFactory.create(MethodDeclarations.dataGravitationMethod_id, fqn, searchSubtrees);
// doing a GET_ALL is crappy but necessary since JGroups' GET_FIRST could return null results from nodes that do
// not have either the primary OR backup, and stop polling other valid nodes.
List resps = cache.getRPCManager().callRemoteMethods(mbrs, dGrav, GroupRequest.GET_ALL, true, buddyManager.getBuddyCommunicationTimeout(), new ResponseValidityFilter(mbrs, cache.getLocalAddress()), false);
if (trace)
{
log.trace("got responses " + resps);
}
if (resps == null)
{
if (mbrs.size() > 1) log.error("No replies to call " + dGrav);
return GravitateResult.noDataFound();
}
// test for and remove exceptions
GravitateResult result = GravitateResult.noDataFound();
for (Object o : resps)
{
if (o instanceof Throwable)
{
if (log.isDebugEnabled())
{
log.debug("Found remote Throwable among responses - removing from responses list", (Exception) o);
}
}
else if (o != null)
{
result = (GravitateResult) o;
if (result.isDataFound())
{
break;
}
}
else if (!configuration.isUseRegionBasedMarshalling())
{
// Null is OK if we are using region based marshalling; it
// is what is returned if a region is inactive. Otherwise
// getting a null is an error condition
log.error("Unexpected null response to call " + dGrav + ".");
}
}
return result;
}
@SuppressWarnings("unchecked")
private void createNode(InvocationContext ctx, List<NodeData> nodeData, boolean localOnly) throws CacheException
{
for (NodeData data : nodeData)
{
if (localOnly)
{
// if (cache.peek(data.getFqn(), false) == null)
if (peekNode(ctx, data.getFqn(), false, false, false) == null)
{
createNodesLocally(data.getFqn(), data.getAttributes());
}
}
else
{
cache.put(data.getFqn(), data.getAttributes());
}
}
}
@SuppressWarnings("unchecked")
private void createNodesLocally(Fqn<?> fqn, Map<?, ?> data) throws CacheException
{
int treeNodeSize;
if ((treeNodeSize = fqn.size()) == 0) return;
NodeSPI n = cache.getRoot();
for (int i = 0; i < treeNodeSize; i++)
{
Object child_name = fqn.get(i);
NodeSPI child_node = n.addChildDirect(new Fqn<Object>(child_name));
if (child_node == null)
{
if (trace)
{
log.trace("failed to find or create child " + child_name + " of node " + n.getFqn());
}
return;
}
if (i == treeNodeSize - 1)
{
// set data
child_node.putAllDirect(data);
}
n = child_node;
}
}
private TransactionEntry getTransactionEntry(GlobalTransaction gtx)
{
return cache.getTransactionTable().get(gtx);
}
private BackupData localBackupGet(Fqn fqn, InvocationContext ctx) throws CacheException
{
GravitateResult result = cache.gravitateData(fqn, true);// a "local" gravitation
boolean found = result.isDataFound();
BackupData data = null;
if (found)
{
Fqn backupFqn = result.getBuddyBackupFqn();
data = new BackupData(fqn, result);
// now the cleanup
if (buddyManager.isDataGravitationRemoveOnFind())
{
// Remove locally only; the remote call will
// be broadcast later
ctx.getOptionOverrides().setCacheModeLocal(true);
cache.removeNode(backupFqn);
}
else
{
cache.evict(backupFqn, true);
}
}
return data;
}
private static class BackupData
{
Fqn primaryFqn;
Fqn backupFqn;
List<NodeData> backupData;
public BackupData(Fqn primaryFqn, GravitateResult gr)
{
this.primaryFqn = primaryFqn;
this.backupFqn = gr.getBuddyBackupFqn();
this.backupData = gr.getNodeData();
}
}
public static class ResponseValidityFilter implements RspFilter
{
private int numValidResponses = 0;
private List<Address> pendingResponders;
public ResponseValidityFilter(List<Address> expected, Address localAddress)
{
// so for now I used a list to keep it consistent
this.pendingResponders = new ArrayList<Address>(expected);
// We'll never get a response from ourself
this.pendingResponders.remove(localAddress);
}
public boolean isAcceptable(Object object, Address address)
{
pendingResponders.remove(address);
if (object instanceof GravitateResult)
{
GravitateResult response = (GravitateResult) object;
if (response.isDataFound()) numValidResponses++;
}
// always return true to make sure a response is logged by the JGroups RpcDispatcher.
return true;
}
public boolean needMoreResponses()
{
return numValidResponses < 1 && pendingResponders.size() > 0;
}
}
}