Fix for ACE-4420 SOLR 4 - sharded - rename and move operations will not update descendants and add incorrect nodes to the shard

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@114654 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Andrew Hind
2015-10-19 09:24:52 +00:00
parent 54257dfdb6
commit d79d0def14
6 changed files with 377 additions and 5 deletions

View File

@@ -373,6 +373,25 @@
</property>
</properties>
</aspect>
<!-- Aspect to record the last transaction that requires a cascade update to index children (like move, rename, link and unlink) -->
<aspect name="sys:cascadeUpdate">
<title>Cascade update</title>
<properties>
<property name="sys:cascadeCRC">
<title>Cascade CRC</title>
<type>d:long</type>
<mandatory>false</mandatory>
<multiple>false</multiple>
</property>
<property name="sys:cascadeTx">
<title>Cascade Tx</title>
<type>d:long</type>
<mandatory>false</mandatory>
<multiple>false</multiple>
</property>
</properties>
</aspect>
</aspects>
</model>

View File

@@ -305,4 +305,12 @@
<property name="jobLockService" ref="jobLockService"/>
</bean>
<!-- The cascade update aspect -->
<bean id="cascadeUpdateAspect" class="org.alfresco.repo.node.CascadeUpdateAspect" init-method="init">
<property name="nodeService" ref="nodeService"/>
<property name="policyComponent" ref="policyComponent"/>
<property name="dictionaryService" ref="dictionaryService" />
<property name="solrTrackingComponent" ref="solrTrackingComponent" />
</bean>
</beans>

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2005-2015 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco 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 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.repo.node;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
import org.alfresco.repo.policy.Behaviour;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.solr.SOLRTrackingComponent;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeRef.Status;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
/**
* @author Andy
*
*/
public class CascadeUpdateAspect implements OnCreateChildAssociationPolicy, OnDeleteChildAssociationPolicy, OnMoveNodePolicy
{
private PolicyComponent policyComponent;
private NodeService nodeService;
private SOLRTrackingComponent solrTrackingComponent;
private DictionaryService dictionaryService;
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent)
{
this.solrTrackingComponent = solrTrackingComponent;
}
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
/**
* Initialise method
*/
public void init()
{
// need to listen to:
// invokeOnCreateChildAssociation(newParentAssocRef, false);
// invokeOnDeleteChildAssociation(oldParentAssocRef);
// invokeOnMoveNode(oldParentAssocRef, newParentAssocRef);
// categories affect paths via membership (not paths beneath nodes that are categories)
// - only changing category structure requires a cascade not changing a node's on a categories
this.policyComponent.bindAssociationBehaviour(OnCreateChildAssociationPolicy.QNAME,
ContentModel.TYPE_BASE,
new JavaBehaviour(this, "onCreateChildAssociation", Behaviour.NotificationFrequency.EVERY_EVENT));
this.policyComponent.bindAssociationBehaviour(OnDeleteChildAssociationPolicy.QNAME,
ContentModel.TYPE_BASE,
new JavaBehaviour(this, "onDeleteChildAssociation", Behaviour.NotificationFrequency.EVERY_EVENT));
this.policyComponent.bindClassBehaviour(OnMoveNodePolicy.QNAME,
ContentModel.TYPE_BASE,
new JavaBehaviour(this, "onMoveNode", Behaviour.NotificationFrequency.EVERY_EVENT));
}
/* (non-Javadoc)
* @see org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy#onMoveNode(org.alfresco.service.cmr.repository.ChildAssociationRef, org.alfresco.service.cmr.repository.ChildAssociationRef)
*/
@Override
public void onMoveNode(ChildAssociationRef oldChildAssocRef, ChildAssociationRef newChildAssocRef)
{
markCascadeUpdate(oldChildAssocRef.getChildRef());
markCascadeUpdate(newChildAssocRef.getChildRef());
}
/* (non-Javadoc)
* @see org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy#onDeleteChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef)
*/
@Override
public void onDeleteChildAssociation(ChildAssociationRef childAssocRef)
{
markCascadeUpdate(childAssocRef.getChildRef());
}
/* (non-Javadoc)
* @see org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy#onCreateChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef, boolean)
*/
@Override
public void onCreateChildAssociation(ChildAssociationRef childAssocRef, boolean isNewNode)
{
if(!isNewNode)
{
markCascadeUpdate(childAssocRef.getChildRef());
}
}
private void markCascadeUpdate(NodeRef nodeRef)
{
Status status = nodeService.getNodeStatus(nodeRef);
nodeService.setProperty(status.getNodeRef(), ContentModel.PROP_CASCADE_CRC, solrTrackingComponent.getCRC(status.getDbId()));
nodeService.setProperty(status.getNodeRef(), ContentModel.PROP_CASCADE_TX, status.getDbTxnId());
}
// /* (non-Javadoc)
// * @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map)
// */
// @Override
// public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before, Map<QName, Serializable> after)
// {
// HashSet<QName> combinedPropertyNames = new HashSet<QName>(before.size() + 10);
// combinedPropertyNames.addAll(before.keySet());
// combinedPropertyNames.addAll(after.keySet());
// for(QName propertyQName : combinedPropertyNames)
// {
// PropertyDefinition propDef = dictionaryService.getProperty(propertyQName);
// if((propDef != null) && (propDef.getDataType().getName().equals(DataTypeDefinition.CATEGORY)))
// {
// Serializable beforeValue = before.get(propDef.getName());
// Serializable afterValue = after.get(propDef.getName());
// if(false == EqualsHelper.nullSafeEquals(beforeValue, afterValue))
// {
// markCascadeUpdate(nodeRef);
// }
// }
//
// }
//
// }
}

View File

@@ -181,4 +181,12 @@ public interface SOLRTrackingComponent
* This is an optional feature.
*/
public ShardRegistry getShardRegistry();
/**
* Compute the CRC for the parent associations to this node that can cause its PATH to change
* - primary & secondary associations and virtual associations from categories.
* @param nodeId
* @return
*/
public long getCRC(Long nodeId);
}

View File

@@ -650,6 +650,66 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent
return props;
}
public long getCRC(Long nodeId)
{
Status status = nodeDAO.getNodeIdStatus(nodeId);
Set<QName> aspects = getNodeAspects(nodeId);
Map<QName, Serializable> props = getProperties(nodeId);
//Category membership does not cascade to children - only the node needs reindexing, not its children
//This was producing cascade updates that were not required
////CategoryPaths categoryPaths = new CategoryPaths(new ArrayList<Pair<Path, QName>>(), new ArrayList<ChildAssociationRef>());
////categoryPaths = getCategoryPaths(status.getNodeRef(), aspects, props);
final List<ChildAssociationRef> parentAssocs = new ArrayList<ChildAssociationRef>(100);
nodeDAO.getParentAssocs(nodeId, null, null, null, new ChildAssocRefQueryCallback()
{
@Override
public boolean preLoadNodes()
{
return false;
}
@Override
public boolean orderResults()
{
return false;
}
@Override
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
{
parentAssocs.add(tenantService.getBaseName(childAssocPair.getSecond(), true));
return true;
}
@Override
public void done()
{
}
});
// for(ChildAssociationRef ref : categoryPaths.getCategoryParents())
// {
// parentAssocs.add(tenantService.getBaseName(ref, true));
// }
CRC32 crc = new CRC32();
for(ChildAssociationRef car : parentAssocs)
{
try
{
crc.update(car.toString().getBytes("UTF-8"));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException("UTF-8 encoding is not supported");
}
}
return crc.getValue();
}
/**
* {@inheritDoc}
*/

View File

@@ -980,11 +980,12 @@ public class NodeServiceTest
props.put(ContentModel.PROP_TITLE, "some title");
nodeService.addAspect(nodeRef, ContentModel.ASPECT_TITLED, props);
nodeService.setProperty(nodeRef, ContentModel.PROP_DESCRIPTION, "Some description");
nodeService.addChild(
Collections.singletonList(workspaceRootNodeRef),
nodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(TEST_PREFIX, "secondary"));
// Adding a child node now triggers behaviour to update a CRC property
// nodeService.addChild(
// Collections.singletonList(workspaceRootNodeRef),
// nodeRef,
// ContentModel.ASSOC_CHILDREN,
// QName.createQName(TEST_PREFIX, "secondary"));
return null;
}
};
@@ -1781,4 +1782,118 @@ public class NodeServiceTest
assertEquals(nodes[1], nodesParentFirst.get(0).sourceAssocs.get(0).getSecond().getSourceRef());
assertEquals(0, nodesParentFirst.get(1).sourceAssocs.size());
}
@Test public void testCascadeUpdate()
{
Map<QName, Serializable> props = new HashMap<QName, Serializable>();
NodeRef nodeRef1 = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
ContentModel.TYPE_CONTAINER).getChildRef();
assertFalse(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_CASCADE_UPDATE));
Map<QName, Serializable> aspectProps = new HashMap<QName, Serializable>();
ArrayList<NodeRef> cats = new ArrayList<NodeRef>();
cats.add(nodeRef1);
aspectProps.put(ContentModel.PROP_CATEGORIES, cats);
nodeService.addAspect(nodeRef1, ContentModel.ASPECT_GEN_CLASSIFIABLE, aspectProps);
assertTrue(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_GEN_CLASSIFIABLE));
assertFalse(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_CASCADE_UPDATE));
NodeRef nodeRef2 = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
ContentModel.TYPE_CONTAINER).getChildRef();
NodeRef nodeRef3 = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
ContentModel.TYPE_CONTAINER).getChildRef();
NodeRef nodeRef4 = nodeService.createNode(
nodeRef2,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
ContentModel.TYPE_CONTAINER).getChildRef();
assertFalse(nodeService.getAspects(nodeRef2).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef3).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef4).contains(ContentModel.ASPECT_CASCADE_UPDATE));
nodeService.moveNode(nodeRef4, nodeRef3, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
assertFalse(nodeService.getAspects(nodeRef2).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef3).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertTrue(nodeService.getAspects(nodeRef4).contains(ContentModel.ASPECT_CASCADE_UPDATE));
Status status = nodeService.getNodeStatus(nodeRef4);
Long lastCascadeTx = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_TX);
assertTrue(status.getDbTxnId().equals(lastCascadeTx));
assertTrue(nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC) != null);
Long crcIn3 = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC);
nodeService.moveNode(nodeRef4, nodeRef2, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
Long crcIn2 = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC);
assertFalse(crcIn2.equals(crcIn3));
NodeRef nodeRef5 = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "5"),
ContentModel.TYPE_CONTAINER).getChildRef();
NodeRef nodeRef6 = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "6"),
ContentModel.TYPE_CONTAINER).getChildRef();
NodeRef nodeRef7 = nodeService.createNode(
nodeRef5,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "7"),
ContentModel.TYPE_CONTAINER).getChildRef();
NodeRef nodeRef8 = nodeService.createNode(
nodeRef5,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "8"),
ContentModel.TYPE_CONTAINER).getChildRef();
assertFalse(nodeService.getAspects(nodeRef5).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef6).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef7).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef8).contains(ContentModel.ASPECT_CASCADE_UPDATE));
nodeService.addChild(nodeRef6, nodeRef7, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
assertFalse(nodeService.getAspects(nodeRef5).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef6).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertTrue(nodeService.getAspects(nodeRef7).contains(ContentModel.ASPECT_CASCADE_UPDATE));
assertFalse(nodeService.getAspects(nodeRef8).contains(ContentModel.ASPECT_CASCADE_UPDATE));
Long doubleLinkCRC = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
nodeService.removeChild(nodeRef6, nodeRef7);
Long singleLinkCRC = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
assertFalse(doubleLinkCRC.equals(singleLinkCRC));
nodeService.addChild(nodeRef6, nodeRef7, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
Long doubleLinkCRC2 = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
assertTrue(doubleLinkCRC.equals(doubleLinkCRC2));
nodeService.removeChild(nodeRef6, nodeRef7);
Long singleLinkCRC2 = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
assertTrue(singleLinkCRC2.equals(singleLinkCRC));
; }
}