diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java index 11ff333f77..dd5ac23c58 100644 --- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java @@ -3143,6 +3143,28 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO // Index assoc.setAssocIndex(-1); + Long assocId = newChildAssocImplInsert(assoc, assocTypeQName, childNodeName); + + // Persist it + assoc.setId(assocId); + + // Primary associations accompany new nodes, so we only have to bring the + // node into the current transaction for secondary associations + if (!isPrimary) + { + updateNode(childNodeId, null, null); + } + + // Done + if (isDebugEnabled) + { + logger.debug("Created child association: " + assoc); + } + return assoc; + } + + protected Long newChildAssocImplInsert(final ChildAssocEntity assoc, final QName assocTypeQName, final String childNodeName) + { RetryingCallback callback = new RetryingCallback() { public Long execute() throws Throwable @@ -3175,7 +3197,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO // We assume that this is from the child cm:name constraint violation throw new DuplicateChildNodeNameException( - parentNode.getNodeRef(), + assoc.getParentNode().getNodeRef(), assocTypeQName, childNodeName, e); @@ -3183,22 +3205,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO } }; Long assocId = childAssocRetryingHelper.doWithRetry(callback); - // Persist it - assoc.setId(assocId); - - // Primary associations accompany new nodes, so we only have to bring the - // node into the current transaction for secondary associations - if (!isPrimary) - { - updateNode(childNodeId, null, null); - } - - // Done - if (isDebugEnabled) - { - logger.debug("Created child association: " + assoc); - } - return assoc; + return assocId; } @Override diff --git a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java index 14ef4fb2b9..80ea14bd24 100644 --- a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java @@ -54,6 +54,7 @@ import org.alfresco.repo.domain.node.TransactionQueryEntity; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; @@ -63,6 +64,7 @@ import org.apache.ibatis.session.ResultContext; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.util.Assert; @@ -1755,6 +1757,12 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl */ public static class MySQLClusterNDB extends MySQL { + // eg. see + // ArchiveAndRestoreTest.*, + // LargeArchiveAndRestoreTest.testCreateAndRestore (also bumped MaxNoOfFiredTriggers from 4000 to 12000) + // DbNodeServiceImplTest.testNodeStatus + // MultilingualDocumentAspectTest.testDeleteNode + // ... @Override protected Long newNodeImplInsert(NodeEntity node) { @@ -1796,6 +1804,42 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl return id; } + + // eg. see DbNodeServiceImplTest.testDuplicateCatch + @Override + protected Long newChildAssocImplInsert(final ChildAssocEntity assoc, final QName assocTypeQName, final String childNodeName) + { + try + { + Long id = insertChildAssoc(assoc); + return id; + } + catch (Throwable e) + { + // DuplicateChildNodeNameException implements DoNotRetryException. + + // Allow real DB concurrency issues (e.g. DeadlockLoserDataAccessException) straight through for a retry + if (e instanceof ConcurrencyFailureException) + { + throw e; + } + + // There are some cases - FK violations, specifically - where we DO actually want to retry. + // Detecting this is done by looking for the related FK names, 'fk_alf_cass_*' in the error message + String lowerMsg = e.getMessage().toLowerCase(); + if (lowerMsg.contains("fk_alf_cass_")) + { + throw new ConcurrencyFailureException("FK violation updating primary parent association:" + assoc, e); + } + + // We assume that this is from the child cm:name constraint violation + throw new DuplicateChildNodeNameException( + assoc.getParentNode().getNodeRef(), + assocTypeQName, + childNodeName, + e); + } + } } /** diff --git a/source/test-java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/test-java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java index 171c51aa08..0595656a65 100644 --- a/source/test-java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java +++ b/source/test-java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2016 Alfresco Software Limited. * * This file is part of Alfresco * @@ -46,6 +46,7 @@ import org.alfresco.repo.dictionary.DictionaryBootstrap; import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.dictionary.M2Type; +import org.alfresco.repo.domain.hibernate.dialect.AlfrescoMySQLClusterNDBDialect; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl.InvalidTypeException; import org.alfresco.repo.node.integrity.IntegrityChecker; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -80,6 +81,7 @@ import org.alfresco.util.FileFilterMode; import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.GUID; import org.alfresco.util.Pair; +import org.hibernate.dialect.Dialect; import org.junit.experimental.categories.Category; import org.springframework.context.ApplicationContext; import org.springframework.extensions.surf.util.I18NUtil; @@ -1736,7 +1738,7 @@ public class FileFolderServiceImplTest extends TestCase checkFileList(pageRes, 6, 0, expectedNames); } - public void testMoveCopy3000Files() throws FileNotFoundException + public void testMoveCopyLotsOfFiles() throws FileNotFoundException { final String CONTAINING_FOLDER = "CONTAINING FOLDER " + GUID.generate(), FOLDER_1 = "FOLDER 1 " + GUID.generate(), @@ -1746,8 +1748,18 @@ public class FileFolderServiceImplTest extends TestCase folder1 = fileFolderService.create(containingFolder.getNodeRef(), FOLDER_1, ContentModel.TYPE_FOLDER), folder2 = fileFolderService.create(containingFolder.getNodeRef(), FOLDER_2, ContentModel.TYPE_FOLDER); - // create 3000 files within the folder - final int COUNT = 3000; + // create thousand(s) of files within the folder + int COUNT = 3000; + + Dialect dialect = (Dialect) ctx.getBean("dialect"); + if (dialect instanceof AlfrescoMySQLClusterNDBDialect) + { + // note: to increase the file count on NDB, may need to further bump-up NDB cluster config + // eg. DataMemory, IndexMemory, MaxNoOfConcurrentOperations, ... + // also consider splitting into separate txns (eg. after each bulk create, move, delete, copy, ...) + COUNT = 1000; + } + for (int index = 0; index < COUNT; index++) { fileFolderService.create(folder1.getNodeRef(), "Name " + index, ContentModel.TYPE_CONTENT); @@ -1778,11 +1790,13 @@ public class FileFolderServiceImplTest extends TestCase assertEquals(COUNT, fileFolderService.listFiles(folders.get(0).getNodeRef()).size()); fileFolderService.delete(folder1.getNodeRef()); + assertEquals(1, fileFolderService.list(containingFolder.getNodeRef()).size()); folder1 = folders.get(0); // copy back FileInfo newFolder = fileFolderService.copy(folder1.getNodeRef(), containingFolder.getNodeRef(), null); + assertEquals(2, fileFolderService.list(containingFolder.getNodeRef()).size()); assertEquals(COUNT, fileFolderService.list(newFolder.getNodeRef()).size()); }