From 347b91ac1f07f8dbb3c9646f2fe8532d613aad5d Mon Sep 17 00:00:00 2001 From: Will Abson Date: Wed, 25 Jun 2014 16:28:14 +0000 Subject: [PATCH] Merged HEAD-BUG-FIX (5.0/Cloud) to HEAD (4.3/Cloud) 74021: Merged DEV 5.0 to HEAD-BUG-FIX (5.0) r73999: ACE-1979: BM-0015: S3 AWS error messages generated during load tests - Emptying tag scope cache has been changed to setting content property for the cache to null to avoid zero-size writes. New test has been added for 'UpdateTagScopesActionExecuter' which covers use-case described in point 4. The fix has been tested against 'TaggingServiceImplTest' git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@74867 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../UpdateTagScopesActionExecuter.java | 58 +-- .../org/alfresco/Repository01TestSuite.java | 1 + .../UpdateTagScopesActionExecuterTest.java | 333 ++++++++++++++++++ 3 files changed, 370 insertions(+), 22 deletions(-) create mode 100644 source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java diff --git a/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java b/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java index 6a3e816472..206f6db6cf 100644 --- a/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java +++ b/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2014 Alfresco Software Limited. * * This file is part of Alfresco * @@ -439,30 +439,44 @@ public class UpdateTagScopesActionExecuter extends ActionExecuterAbstractBase } } } - - // Order the list - Collections.sort(tags); - - // Write new content back to tag scope - String tagContent = TaggingServiceImpl.tagDetailsToString(tags); - ContentWriter contentWriter = contentService.getWriter(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, true); - contentWriter.setEncoding("UTF-8"); - contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - contentWriter.putContent(tagContent); - + + // ACE-1979: emptying tag scope cache by setting content property for the cache to null to avoid zero-size writes. Orphaned content will be deleted with content store + // cleaner job + if (tags.isEmpty()) + { + nodeService.setProperty(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, null); + + if (logger.isDebugEnabled()) + { + logger.debug("Updated tag scope: '" + tagScopeNode + "'. No tags were found. Emptying tags cache by setting content property to null..."); + } + } + else + { + // Order the list + Collections.sort(tags); + + // Write new content back to tag scope + String tagContent = TaggingServiceImpl.tagDetailsToString(tags); + ContentWriter contentWriter = contentService.getWriter(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, true); + contentWriter.setEncoding("UTF-8"); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.putContent(tagContent); + + // Log this if required + if(logger.isDebugEnabled()) + { + logger.debug( + "Updated tag scope " + tagScopeNode + " with " + updates + ", " + + "new contents are { " + tagContent.replace("\n", " : ") + " } " + + "from old contents of " + previousTagState + ); + } + } + // We're done making our changes // Allow behaviours to fire again if they want to behaviourFilter.enableBehaviour(); - - // Log this if required - if(logger.isDebugEnabled()) - { - logger.debug( - "Updated tag scope " + tagScopeNode + " with " + updates + ", " + - "new contents are { " + tagContent.replace("\n", " : ") + " } " + - "from old contents of " + previousTagState - ); - } } } diff --git a/source/test-java/org/alfresco/Repository01TestSuite.java b/source/test-java/org/alfresco/Repository01TestSuite.java index 3b469ef3b4..9fed80ebb7 100644 --- a/source/test-java/org/alfresco/Repository01TestSuite.java +++ b/source/test-java/org/alfresco/Repository01TestSuite.java @@ -354,6 +354,7 @@ public class Repository01TestSuite extends TestSuite static void tests53(TestSuite suite) { suite.addTestSuite(org.alfresco.repo.tagging.TaggingServiceImplTest.class); + suite.addTestSuite(org.alfresco.repo.tagging.UpdateTagScopesActionExecuterTest.class); } static void tests55(TestSuite suite) diff --git a/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java b/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java new file mode 100644 index 0000000000..7a7e795669 --- /dev/null +++ b/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2005-2014 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 . + */ +package org.alfresco.repo.tagging; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.nodelocator.CompanyHomeNodeLocator; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.tagging.TaggingService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.ApplicationContext; + +/** + * Test for {@link UpdateTagScopesActionExecuter} + * + * @author Dmitry Velichkevich + */ +public class UpdateTagScopesActionExecuterTest extends TestCase +{ + private static final int TAGSCOPE_LAYERS = 3; + + private static final int TEST_TAGS_AMOUNT = 3; + + private static final int TEST_DOCUMENTS_AMOUNT = 3; + + + private static final String ACTION_TRACKING_SERVICE_BEAN_NAME = "actionTrackingService"; + + private static final String UPDATE_TAGSCOPE_ACTION_EXECUTER_BEAN_NAME = "update-tagscope"; + + private static final String TEST_TAG_NAME_PATTERN = "testTag%d-%d-%d"; + + private static final String TEST_FOLDER_NAME_PATTERN = "TestFolder-%d"; + + private static final String TEST_DOCUMENT_NAME_PATTERN = "InFolder-%d-TestDocument-%d.txt"; + + + private ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + + private ActionService actionService; + + private TaggingService taggingService; + + private FileFolderService fileFolderService; + + private TransactionService transactionService; + + private UpdateTagScopesActionExecuter actionExecuter; + + private ActionTrackingService actionTrackingService; + + private UserTransaction transaction; + + private List expectedTagScopes; + + private List testTags; + + @Before + @Override + public void setUp() throws Exception + { + final ServiceRegistry registry = (ServiceRegistry) applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY); + + nodeService = registry.getNodeService(); + actionService = registry.getActionService(); + actionExecuter = (UpdateTagScopesActionExecuter) applicationContext.getBean(UPDATE_TAGSCOPE_ACTION_EXECUTER_BEAN_NAME); + taggingService = registry.getTaggingService(); + fileFolderService = registry.getFileFolderService(); + transactionService = registry.getTransactionService(); + actionTrackingService = (ActionTrackingService) applicationContext.getBean(ACTION_TRACKING_SERVICE_BEAN_NAME); + + AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); + + expectedTagScopes = new LinkedList(); + testTags = new LinkedList(); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + createTestContent(registry, expectedTagScopes); + return null; + } + }, false, true); + + waitForTagScopeUpdate(); + + transaction = transactionService.getUserTransaction(); + transaction.begin(); + } + + /** + * Creates simple hierarchy with documents tagged on the first layer only + * + * @param registry - {@link ServiceRegistry} instance + * @param createdTagScopes - {@link List}<{@link NodeRef}> instance which contains all tag scope folders + */ + private void createTestContent(ServiceRegistry registry, List createdTagScopes) + { + NodeRef rootNode = registry.getNodeLocatorService().getNode(CompanyHomeNodeLocator.NAME, null, null); + + NodeRef currentParent = rootNode; + for (int i = 0; i < TAGSCOPE_LAYERS; i++) + { + FileInfo newFolder = fileFolderService.create(currentParent, String.format(TEST_FOLDER_NAME_PATTERN, i), ContentModel.TYPE_FOLDER); + currentParent = newFolder.getNodeRef(); + + if (null != createdTagScopes) + { + createdTagScopes.add(currentParent); + } + + nodeService.addAspect(currentParent, ContentModel.ASPECT_TAGSCOPE, null); + + for (int j = 0; j < TEST_DOCUMENTS_AMOUNT; j++) + { + FileInfo newDocument = fileFolderService.create(currentParent, String.format(TEST_DOCUMENT_NAME_PATTERN, i, j), ContentModel.TYPE_CONTENT); + nodeService.addAspect(newDocument.getNodeRef(), ContentModel.ASPECT_TAGGABLE, null); + + if (0 == i) + { + for (int k = 0; k < TEST_TAGS_AMOUNT; k++) + { + String tagName = String.format(TEST_TAG_NAME_PATTERN, k, j, i); + testTags.add(tagName); + taggingService.addTag(newDocument.getNodeRef(), tagName); + } + } + } + } + } + + private void waitForTagScopeUpdate() throws Exception + { + List executingActions = null; + + do + { + synchronized (this) + { + wait(1000); + } + + executingActions = actionTrackingService.getExecutingActions(UpdateTagScopesActionExecuter.NAME); + } while (!executingActions.isEmpty()); + } + + @After + @Override + public void tearDown() throws Exception + { + final NodeRef rootTestFolder = expectedTagScopes.iterator().next(); + + for (String tagName : testTags) + { + taggingService.deleteTag(rootTestFolder.getStoreRef(), tagName); + } + + testTags.clear(); + testTags = null; + + if (Status.STATUS_ROLLEDBACK != transaction.getStatus()) + { + transaction.rollback(); + } + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + nodeService.deleteNode(rootTestFolder); + return null; + } + }, false, true); + + AuthenticationUtil.clearCurrentSecurityContext(); + + expectedTagScopes.clear(); + expectedTagScopes = null; + } + + /** + * Tests that tag scopes are properly updated. Cache on the first layer MUST NOT be empty. All other tag scopes MUST BE null + * + * @throws Exception + */ + @Test + public void testSimpleTagScopesUpsdate() throws Exception + { + Action tagScopeUpdateAction = actionService.createAction(UpdateTagScopesActionExecuter.NAME); + tagScopeUpdateAction.setParameterValue(UpdateTagScopesActionExecuter.PARAM_TAG_SCOPES, (Serializable) expectedTagScopes); + actionExecuter.execute(tagScopeUpdateAction, null); + + Iterator iterator = expectedTagScopes.iterator(); + assertTrue(iterator.hasNext()); + + NodeRef taggedTagScope = iterator.next(); + assertNotNull(taggedTagScope); + + ContentData contentData = getTagScopeCacheContentDataProperty(taggedTagScope); + assertNotNull(contentData); + assertTrue(contentData.getSize() > 0L); + + assertTrue(iterator.hasNext()); + + for (NodeRef tagScopeFolder = iterator.next(); iterator.hasNext(); tagScopeFolder = iterator.next()) + { + assertNotNull(tagScopeFolder); + contentData = getTagScopeCacheContentDataProperty(tagScopeFolder); + assertNull(contentData); + } + } + + /** + * ACE-1979: tag scope cache must be emptied when tag scope doesn't contain tags anymore. The fix nullifies + * content data property for the tag scope cache. This approach allows avoiding immediate update in content store and postponing it untill content store cleaner job is executed + * + * @throws Exception + */ + @Test + public void testTagScopesUpdateWhenTagsAreRemoved() throws Exception + { + Action tagScopeUpdateAction = actionService.createAction(UpdateTagScopesActionExecuter.NAME); + tagScopeUpdateAction.setParameterValue(UpdateTagScopesActionExecuter.PARAM_TAG_SCOPES, (Serializable) expectedTagScopes); + actionExecuter.execute(tagScopeUpdateAction, null); + + waitForTagScopeUpdate(); + + final NodeRef taggedTagScope = expectedTagScopes.iterator().next(); + assertNotNull(taggedTagScope); + + ContentData contentData = getTagScopeCacheContentDataProperty(taggedTagScope); + assertNotNull(contentData); + actionTrackingService.getExecutingActions(UpdateTagScopesActionExecuter.NAME); + assertTrue(contentData.getSize() > 0L); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + for (ChildAssociationRef child : nodeService.getChildAssocs(taggedTagScope, Collections.singleton(ContentModel.TYPE_CONTENT))) + { + taggingService.removeTags(child.getChildRef(), testTags); + } + + return null; + } + }, false, true); + + waitForTagScopeUpdate(); + + actionExecuter.execute(tagScopeUpdateAction, null); + + for (NodeRef tagScopeFolder : expectedTagScopes) + { + assertNotNull(tagScopeFolder); + contentData = getTagScopeCacheContentDataProperty(tagScopeFolder); + assertNull(contentData); + } + } + + /** + * @param nodeRef - {@link NodeRef} instance which represents tag scope folder + * @return {@link ContentModel#PROP_TAGSCOPE_CACHE} {@link ContentData} property instance for the given nodeRef or + */ + private ContentData getTagScopeCacheContentDataProperty(final NodeRef nodeRef) + { + ContentData result = null; + + Serializable contentProperty = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Serializable execute() throws Throwable + { + return nodeService.getProperty(nodeRef, ContentModel.PROP_TAGSCOPE_CACHE); + } + }, false, true); + + if (contentProperty instanceof ContentData) + { + result = (ContentData) contentProperty; + } + + return result; + } +}