/*
* 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 static org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.optimistic.DataVersioningException;
import org.jboss.cache.optimistic.DefaultDataVersion;
import org.jboss.cache.optimistic.TransactionWorkspace;
import org.jboss.cache.optimistic.WorkspaceNode;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jgroups.Address;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Validates the data in the {@link TransactionWorkspace} against data in the underlying data structure
* (versions only) and then applies changes to the underlying data structure. This is only triggered when commit,
* rollback or prepare method calls are encountered. Other method calls are directly passed up the interceptor chain,
* untouched. Note that prepare/commit/rollbacks are <b>not</b> passed up the interceptor chain after being processed.
* <p/>
* When preparting, this interceptor does nothing more than validate versions.
* The validation scheme used is based on the {@link org.jboss.cache.optimistic.DataVersion} implementation used.
* {@link org.jboss.cache.optimistic.DataVersion#newerThan(org.jboss.cache.optimistic.DataVersion)} is used to determine
* whether the version of one instance is newer than the version of another. It is up to the {@link org.jboss.cache.optimistic.DataVersion}
* implementation to deal with attempting to compare incompatible version types (and potentially throwing {@link org.jboss.cache.optimistic.DataVersioningException}s.
* <p/>
* Upon successful commit, changes in the workspace are applied to the underlying data structure in the cache.
* <p/>
* On rollback clears the nodes in the workspace and leaves the underlying data structure untouched.
*
* @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
* @author Steve Woodcock (<a href="mailto:stevew@jofti.com">stevew@jofti.com</a>)
*/
public class OptimisticValidatorInterceptor extends OptimisticInterceptor
{
private boolean useTombstones;
public OptimisticValidatorInterceptor()
{
initLogger();
}
@Override
public void setCache(CacheSPI cache)
{
super.setCache(cache);
CacheMode mode = cache.getConfiguration().getCacheMode();
useTombstones = (mode == CacheMode.INVALIDATION_ASYNC) || (mode == CacheMode.INVALIDATION_SYNC);
}
@Override
protected Object handleOptimisticPrepareMethod(InvocationContext ctx, GlobalTransaction gtx, List modifications, Map data, Address address, boolean onePhaseCommit) throws Throwable
{
TransactionWorkspace workspace = getTransactionWorkspace(getGlobalTransaction(ctx));
// There is no guarantee that this collection is in any order!
Collection<WorkspaceNode> nodes = workspace.getNodes().values();
//we ought to have all necessary locks here so lets try and validate
if (log.isDebugEnabled()) log.debug("Validating " + nodes.size() + " nodes.");
for (WorkspaceNode workspaceNode : nodes)
{
if (workspaceNode.isDirty())
{
Fqn fqn = workspaceNode.getFqn();
if (trace) log.trace("Validating version for node [" + fqn + "]");
NodeSPI underlyingNode;
underlyingNode = peekNode(ctx, fqn, false, true, true);
// if this is a newly created node then we expect the underlying node to be null.
// also, if the node has been deleted in the WS and the underlying node is null, this *may* be ok ... will test again later when comparing versions
// if not, we have a problem...
if (underlyingNode == null && !workspaceNode.isCreated() && !workspaceNode.isDeleted())
{
throw new DataVersioningException("Underlying node for " + fqn + " is null, and this node wasn't newly created in this transaction! We have a concurrent deletion event.");
}
// needs to have been created AND modified - we allow concurrent creation if no data is put into the node
if (underlyingNode != null && underlyingNode.isValid() && workspaceNode.isCreated() && workspaceNode.isModified())
{
throw new DataVersioningException("Transaction attempted to create " + fqn + " anew. It has already been created since this transaction started, by another (possibly remote) transaction. We have a concurrent creation event.");
}
if (underlyingNode != null && !underlyingNode.isValid())
{
// we havea tombstone
if (!workspaceNode.isCreated() && !workspaceNode.isDeleted())
throw new DataVersioningException("Underlying node doesn't exist but a tombstone does; workspace node should be marked as created!");
if (underlyingNode.getVersion().newerThan(workspaceNode.getVersion()))
{
// we have an out of date node here
throw new DataVersioningException("Version mismatch for node " + fqn + ": underlying node with version " + workspaceNode.getNode().getVersion() + " is newer than workspace node, with version " + workspaceNode.getVersion());
}
}
if (!workspaceNode.isCreated() && (workspaceNode.isDeleted() || workspaceNode.isModified()))
{
// if the real node is null, throw a DVE
if (underlyingNode == null)
{
// but not if the WSN has also been deleted
if (!workspaceNode.isDeleted())
throw new DataVersioningException("Unable to compare versions since the underlying node has been deleted by a concurrent transaction!");
else if (trace)
log.trace("The data node [" + fqn + "] is null, but this is ok since the workspace node is marked as deleted as well");
}
// if there is a DataVersion type mismatch here, leave it up to the DataVersion impl to barf if necessary. - JBCACHE-962
else if (underlyingNode.getVersion().newerThan(workspaceNode.getVersion()))
{
// we have an out of date node here
throw new DataVersioningException("Version mismatch for node " + fqn + ": underlying node with version " + workspaceNode.getNode().getVersion() + " is newer than workspace node, with version " + workspaceNode.getVersion());
}
}
}
else
{
if (trace) log.trace("Node [" + workspaceNode.getFqn() + "] doesn't need validating as it isn't dirty");
}
}
log.debug("Successfully validated nodes");
return nextInterceptor(ctx);
}
@Override
protected Object handleCommitMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
GlobalTransaction gtx = getGlobalTransaction(ctx);
TransactionWorkspace workspace;
try
{
workspace = getTransactionWorkspace(gtx);
}
catch (CacheException e)
{
log.warn("we can't rollback", e);
return nextInterceptor(ctx);
}
if (log.isDebugEnabled()) log.debug("Commiting successfully validated changes for GlobalTransaction " + gtx);
Collection<WorkspaceNode> workspaceNodes = workspace.getNodes().values();
for (WorkspaceNode workspaceNode : workspaceNodes)
{
NodeSPI underlyingNode = workspaceNode.getNode();
// short circuit if this node is deleted?
if (workspaceNode.isDeleted())
{
if (trace) log.trace("Workspace node " + workspaceNode.getFqn() + " deleted; removing");
if (underlyingNode.getFqn().isRoot())
{
throw new CacheException("An illegal attempt to delete the root node!");
}
else
{
// mark it as invalid so any direct references are marked as such
underlyingNode.setValid(false, true);
// we need to update versions here, too
performVersionUpdate(underlyingNode, workspaceNode);
if (!useTombstones)
{
// don't retain the tombstone
NodeSPI parent = underlyingNode.getParent();
if (parent == null)
{
throw new CacheException("Underlying node " + underlyingNode + " has no parent");
}
parent.removeChildDirect(underlyingNode.getFqn().getLastElement());
}
}
}
else
{
boolean updateVersion = false;
if (workspaceNode.isChildrenModified() || workspaceNode.isResurrected()) // if it is newly created make sure we remove all underlying children that may exist, to solve a remove-and-create-in-tx condition
{
if (trace) log.trace("Updating children since node has modified children");
// merge children.
List<Set<Fqn>> deltas = workspaceNode.getMergedChildren();
if (trace) log.trace("Applying children deltas to parent node " + underlyingNode.getFqn());
if (workspaceNode.isResurrected())
{
// mark it as invalid so any direct references are marked as such
Map childNode = underlyingNode.getChildrenMapDirect();
if (childNode != null)
{
for (Object o : childNode.values())
{
NodeSPI cn = (NodeSPI) o;
cn.setValid(false, true);
if (!useTombstones) underlyingNode.removeChildDirect(cn.getFqn().getLastElement());
}
}
}
for (Fqn child : deltas.get(0))
{
NodeSPI childNode = workspace.getNode(child).getNode();
underlyingNode.addChildDirect(childNode);
childNode.setValid(true, false);
}
for (Fqn child : deltas.get(1))
{
// mark it as invalid so any direct references are marked as such
NodeSPI childNode = underlyingNode.getChildDirect(child.getLastElement());
if (childNode != null) childNode.setValid(false, true);
if (!useTombstones) underlyingNode.removeChildDirect(child.getLastElement());
}
updateVersion = underlyingNode.isLockForChildInsertRemove();
// do we need to notify listeners of a modification?? If all we've done is added children then don't
// notify.
}
if (workspaceNode.isModified())
{
if (trace) log.trace("Merging data since node is dirty");
Map mergedData = workspaceNode.getMergedData();
underlyingNode.clearDataDirect();
underlyingNode.putAllDirect(mergedData);
// mark node and any parents as valid- if available. Versioning parents are tough though - leave as old versions?
validateNodeAndParents(underlyingNode);
updateVersion = true;
}
if (updateVersion)
{
performVersionUpdate(underlyingNode, workspaceNode);
}
}
}
return nextInterceptor(ctx);
}
@Override
protected Object handleRollbackMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
TransactionWorkspace workspace;
workspace = getTransactionWorkspace(getGlobalTransaction(ctx));
workspace.clearNodes();
return nextInterceptor(ctx);
}
private void validateNodeAndParents(NodeSPI node)
{
node.setValid(true, false);
if (!node.getFqn().isRoot()) validateNodeAndParents(node.getParent());
}
private void performVersionUpdate(NodeSPI underlyingNode, WorkspaceNode workspaceNode)
{
if (workspaceNode.isVersioningImplicit())
{
if (trace) log.trace("Versioning is implicit; incrementing.");
underlyingNode.setVersion(((DefaultDataVersion) workspaceNode.getVersion()).increment());
}
else
{
if (trace) log.trace("Versioning is explicit; not attempting an increment.");
underlyingNode.setVersion(workspaceNode.getVersion());
}
if (trace)
log.trace("Setting version of node " + underlyingNode.getFqn() + " from " + workspaceNode.getVersion() + " to " + underlyingNode.getVersion());
}
}