Commit 2ba602c74d12796589f9b4eea71d1d365a7a84b1

Authored by Rui Fernandes
1 parent 6240e8e9a5
Exists in master

first version

... ... @@ -6,4 +6,6 @@ assembly
6 6 .project
7 7 .classpath
8 8 bin/**/*
9   -
  9 +bin
  10 +src/log4j.properties
  11 +alfresco.log
... ...
src/alfresco/extension/trashcan-cleaner-context.xml
... ... @@ -0,0 +1,24 @@
  1 +<?xml version='1.0' encoding='UTF-8'?>
  2 +<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
  3 +
  4 +<beans>
  5 + <bean name="trashcanCleaner" class="org.springframework.scheduling.quartz.JobDetailBean">
  6 + <property name="jobClass" value="org.alfresco.trashcan.TrashcanCleanerJob" />
  7 + <property name="jobDataAsMap">
  8 + <map>
  9 + <entry key="nodeRegistry" value-ref="nodeService" />
  10 + <entry key="transactionService" value-ref="transactionService" />
  11 + <entry key="authenticationComponent" value-ref="authenticationComponent" />
  12 + <entry key="jobLockService" value-ref="jobLockService" />
  13 + </map>
  14 + </property>
  15 + </bean>
  16 +
  17 + <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
  18 + <property name="jobDetail" ref="trashcanCleaner" />
  19 + <!-- Repeat hourly on the half hour -->
  20 + <property name="cronExpression">
  21 + <value>0 30 * * * ?</value>
  22 + </property>
  23 + </bean>
  24 +</beans>
0 25 \ No newline at end of file
... ...
src/org/alfresco/schedule/AbstractScheduledLockedJob.java
... ... @@ -0,0 +1,38 @@
  1 +package org.alfresco.schedule;
  2 +
  3 +import org.alfresco.repo.lock.JobLockService;
  4 +import org.quartz.JobExecutionContext;
  5 +import org.quartz.JobExecutionException;
  6 +import org.springframework.scheduling.quartz.QuartzJobBean;
  7 +
  8 +/**
  9 + *
  10 + * @author rjmfernandes@gmail.com
  11 + *
  12 + */
  13 +public abstract class AbstractScheduledLockedJob extends QuartzJobBean {
  14 +
  15 + private ScheduledJobLockExecuter locker;
  16 +
  17 + @Override
  18 + protected final void executeInternal(final JobExecutionContext jobContext)
  19 + throws JobExecutionException {
  20 + if (locker == null) {
  21 + JobLockService jobLockServiceBean = (JobLockService) jobContext
  22 + .getJobDetail().getJobDataMap().get("jobLockService");
  23 + if (jobLockServiceBean == null)
  24 + throw new JobExecutionException(
  25 + "Missing setting for bean jobLockService");
  26 + String name = (String) jobContext.getJobDetail().getJobDataMap()
  27 + .get("name");
  28 + String jobName = name == null ? this.getClass().getSimpleName()
  29 + : name;
  30 + locker = new ScheduledJobLockExecuter(jobLockServiceBean, jobName,
  31 + this);
  32 + }
  33 + locker.execute(jobContext);
  34 + }
  35 +
  36 + public abstract void executeJob(JobExecutionContext jobContext)
  37 + throws JobExecutionException;
  38 +}
... ...
src/org/alfresco/schedule/ScheduledJobLockExecuter.java
... ... @@ -0,0 +1,111 @@
  1 +package org.alfresco.schedule;
  2 +
  3 +import org.alfresco.repo.lock.JobLockService;
  4 +import org.alfresco.repo.lock.LockAcquisitionException;
  5 +import org.alfresco.service.namespace.NamespaceService;
  6 +import org.alfresco.service.namespace.QName;
  7 +import org.alfresco.util.Pair;
  8 +import org.alfresco.util.VmShutdownListener.VmShutdownException;
  9 +import org.apache.log4j.Logger;
  10 +import org.quartz.JobExecutionContext;
  11 +import org.quartz.JobExecutionException;
  12 +
  13 +/**
  14 + *
  15 + * @author rjmfernandes@gmail.com
  16 + *
  17 + */
  18 +
  19 +public class ScheduledJobLockExecuter {
  20 +
  21 + private static Logger logger = Logger
  22 + .getLogger(ScheduledJobLockExecuter.class.getName());
  23 +
  24 + private static final long LOCK_TTL = 30000L;
  25 + private static ThreadLocal<Pair<Long, String>> lockThreadLocal = new ThreadLocal<Pair<Long, String>>();
  26 + private JobLockService jobLockService;
  27 + private QName lockQName;
  28 + private AbstractScheduledLockedJob job;
  29 +
  30 + public ScheduledJobLockExecuter(JobLockService jobLockService, String name,
  31 + AbstractScheduledLockedJob job) {
  32 + this.jobLockService = jobLockService;
  33 + lockQName = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI,
  34 + name);
  35 + this.job = job;
  36 + }
  37 +
  38 + public void execute(JobExecutionContext jobContext)
  39 + throws JobExecutionException {
  40 + try {
  41 + if (logger.isDebugEnabled()) {
  42 + logger.debug(String.format(" Job %s started.",
  43 + lockQName.getLocalName()));
  44 + }
  45 + refreshLock();
  46 + job.executeJob(jobContext);
  47 + if (logger.isDebugEnabled()) {
  48 + logger.debug(String.format(" Job %s completed.",
  49 + lockQName.getLocalName()));
  50 + }
  51 + } catch (LockAcquisitionException e) {
  52 + // Job being done by another process
  53 + if (logger.isDebugEnabled()) {
  54 + logger.debug(String.format(" Job %s already underway.",
  55 + lockQName.getLocalName()));
  56 + }
  57 + } catch (VmShutdownException e) {
  58 + // Aborted
  59 + if (logger.isDebugEnabled()) {
  60 + logger.debug(String.format(" Job %s aborted.",
  61 + lockQName.getLocalName()));
  62 + }
  63 + } finally {
  64 + releaseLock();
  65 + }
  66 + }
  67 +
  68 + /**
  69 + * Lazily update the job lock
  70 + *
  71 + */
  72 + private void refreshLock() {
  73 + Pair<Long, String> lockPair = lockThreadLocal.get();
  74 + if (lockPair == null) {
  75 + String lockToken = jobLockService.getLock(lockQName, LOCK_TTL);
  76 + Long lastLock = new Long(System.currentTimeMillis());
  77 + // We have not locked before
  78 + lockPair = new Pair<Long, String>(lastLock, lockToken);
  79 + lockThreadLocal.set(lockPair);
  80 + } else {
  81 + long now = System.currentTimeMillis();
  82 + long lastLock = lockPair.getFirst().longValue();
  83 + String lockToken = lockPair.getSecond();
  84 + // Only refresh the lock if we are past a threshold
  85 + if (now - lastLock > (long) (LOCK_TTL / 2L)) {
  86 + jobLockService.refreshLock(lockToken, lockQName, LOCK_TTL);
  87 + lastLock = System.currentTimeMillis();
  88 + lockPair = new Pair<Long, String>(lastLock, lockToken);
  89 + }
  90 + }
  91 + }
  92 +
  93 + /**
  94 + * Release the lock after the job completes
  95 + */
  96 + private void releaseLock() {
  97 + Pair<Long, String> lockPair = lockThreadLocal.get();
  98 + if (lockPair != null) {
  99 + // We can't release without a token
  100 + try {
  101 + jobLockService.releaseLock(lockPair.getSecond(), lockQName);
  102 + } finally {
  103 + // Reset
  104 + lockThreadLocal.set(null);
  105 + }
  106 + }
  107 + // else: We can't release without a token
  108 + }
  109 +
  110 +}
  111 +
... ...
src/org/alfresco/trashcan/TrashcanCleaner.java
... ... @@ -0,0 +1,104 @@
  1 +package org.alfresco.trashcan;
  2 +
  3 +import java.util.ArrayList;
  4 +import java.util.Date;
  5 +import java.util.List;
  6 +
  7 +import org.alfresco.model.ContentModel;
  8 +import org.alfresco.service.cmr.repository.ChildAssociationRef;
  9 +import org.alfresco.service.cmr.repository.NodeRef;
  10 +import org.alfresco.service.cmr.repository.NodeService;
  11 +import org.alfresco.service.cmr.repository.StoreRef;
  12 +import org.apache.commons.logging.Log;
  13 +import org.apache.commons.logging.LogFactory;
  14 +
  15 +/**
  16 + *
  17 + * @author rjmfernandes@gmail.com
  18 + *
  19 + */
  20 +public class TrashcanCleaner {
  21 +
  22 + private static Log logger = LogFactory.getLog(TrashcanCleaner.class);
  23 +
  24 + private NodeService nodeService;
  25 + private String archiveStoreUrl = "archive://SpacesStore";
  26 + private int deleteBatchCount = 1000;
  27 + private int daysToKeep = -1;
  28 + private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24;
  29 +
  30 + public TrashcanCleaner(NodeService nodeService) {
  31 + this.nodeService = nodeService;
  32 + }
  33 +
  34 + public TrashcanCleaner(NodeService nodeService, int deleteBatchCount,
  35 + int daysToKeep) {
  36 + this(nodeService);
  37 + this.deleteBatchCount = deleteBatchCount;
  38 + this.daysToKeep = daysToKeep;
  39 + }
  40 +
  41 + public TrashcanCleaner(NodeService nodeService, String archiveStoreUrl,
  42 + int deleteBatchCount, int daysToKeep) {
  43 + this(nodeService, deleteBatchCount, daysToKeep);
  44 + this.archiveStoreUrl = archiveStoreUrl;
  45 + }
  46 +
  47 + public void clean() {
  48 + List<NodeRef> nodes = getBatchToDelete();
  49 +
  50 + if (logger.isDebugEnabled()) {
  51 + logger.debug(String.format("Number of nodes to delete: %s", nodes
  52 + .size()));
  53 + }
  54 +
  55 + deleteNodes(nodes);
  56 +
  57 + if (logger.isDebugEnabled()) {
  58 + logger.debug("Nodes deleted");
  59 + }
  60 + }
  61 +
  62 + private void deleteNodes(List<NodeRef> nodes) {
  63 + for (int i = nodes.size(); i > 0; i--) {
  64 + nodeService.deleteNode(nodes.get(i - 1));
  65 + }
  66 + }
  67 +
  68 + private List<NodeRef> getBatchToDelete() {
  69 + List<ChildAssociationRef> childAssocs = getTrashcanChildAssocs();
  70 + List<NodeRef> nodes = new ArrayList<NodeRef>(deleteBatchCount);
  71 + if (logger.isDebugEnabled()) {
  72 + logger.debug(String.format("Found %s nodes on trashcan",
  73 + childAssocs.size()));
  74 + }
  75 + return fillBatchToDelete(nodes, childAssocs);
  76 + }
  77 +
  78 + private List<NodeRef> fillBatchToDelete(List<NodeRef> batch,
  79 + List<ChildAssociationRef> trashChildAssocs) {
  80 + for (int j = trashChildAssocs.size(); j > 0
  81 + && batch.size() < deleteBatchCount; j--) {
  82 + ChildAssociationRef childAssoc = trashChildAssocs.get(j - 1);
  83 + NodeRef childRef = childAssoc.getChildRef();
  84 + if (olderThanDaysToKeep(childRef)) {
  85 + batch.add(childRef);
  86 + }
  87 + }
  88 + return batch;
  89 + }
  90 +
  91 + private List<ChildAssociationRef> getTrashcanChildAssocs() {
  92 + StoreRef archiveStore = new StoreRef(archiveStoreUrl);
  93 + NodeRef archiveRoot = nodeService.getRootNode(archiveStore);
  94 + return nodeService.getChildAssocs(archiveRoot);
  95 + }
  96 +
  97 + private boolean olderThanDaysToKeep(NodeRef node) {
  98 + Date archivedDate = (Date) nodeService.getProperty(node,
  99 + ContentModel.PROP_ARCHIVED_DATE);
  100 + return daysToKeep * DAYS_TO_MILLIS < System.currentTimeMillis()
  101 + - archivedDate.getTime();
  102 + }
  103 +
  104 +}
... ...
src/org/alfresco/trashcan/TrashcanCleanerJob.java
... ... @@ -0,0 +1,52 @@
  1 +package org.alfresco.trashcan;
  2 +
  3 +import org.alfresco.repo.security.authentication.AuthenticationComponent;
  4 +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
  5 +import org.alfresco.schedule.AbstractScheduledLockedJob;
  6 +import org.alfresco.service.cmr.repository.NodeService;
  7 +import org.alfresco.service.transaction.TransactionService;
  8 +import org.quartz.JobExecutionContext;
  9 +import org.quartz.JobExecutionException;
  10 +
  11 +/**
  12 + *
  13 + * @author rjmfernandes@gmail.com
  14 + *
  15 + */
  16 +public class TrashcanCleanerJob extends AbstractScheduledLockedJob {
  17 +
  18 + protected NodeService nodeService;
  19 + protected TransactionService transactionService;
  20 + protected AuthenticationComponent authenticationComponent;
  21 +
  22 + @Override
  23 + public void executeJob(JobExecutionContext jobContext)
  24 + throws JobExecutionException {
  25 + setServices(jobContext);
  26 + authenticationComponent.setSystemUserAsCurrentUser();
  27 + cleanInTransaction();
  28 + }
  29 +
  30 + private void cleanInTransaction() {
  31 + RetryingTransactionCallback<Object> txnWork = new RetryingTransactionCallback<Object>() {
  32 + public Object execute() throws Exception {
  33 + TrashcanCleaner cleaner = new TrashcanCleaner(nodeService);
  34 + cleaner.clean();
  35 + return null;
  36 + }
  37 + };
  38 + transactionService.getRetryingTransactionHelper().doInTransaction(
  39 + txnWork);
  40 + }
  41 +
  42 + private void setServices(JobExecutionContext jobContext) {
  43 + nodeService = (NodeService) jobContext.getJobDetail().getJobDataMap()
  44 + .get("nodeService");
  45 + transactionService = (TransactionService) jobContext.getJobDetail()
  46 + .getJobDataMap().get("transactionService");
  47 + authenticationComponent = (AuthenticationComponent) jobContext
  48 + .getJobDetail().getJobDataMap().get("authenticationComponent");
  49 +
  50 + }
  51 +
  52 +}
... ...
test/org/alfresco/trashcan/TrashcanCleanerTest.java
... ... @@ -0,0 +1,129 @@
  1 +package org.alfresco.trashcan;
  2 +
  3 +import java.io.Serializable;
  4 +import java.util.HashMap;
  5 +import java.util.List;
  6 +import java.util.Map;
  7 +
  8 +import javax.transaction.UserTransaction;
  9 +
  10 +import junit.framework.TestCase;
  11 +
  12 +import org.alfresco.model.ContentModel;
  13 +import org.alfresco.repo.model.Repository;
  14 +import org.alfresco.repo.security.authentication.AuthenticationComponent;
  15 +import org.alfresco.service.cmr.repository.ChildAssociationRef;
  16 +import org.alfresco.service.cmr.repository.NodeRef;
  17 +import org.alfresco.service.cmr.repository.NodeService;
  18 +import org.alfresco.service.cmr.repository.StoreRef;
  19 +import org.alfresco.service.namespace.NamespaceService;
  20 +import org.alfresco.service.namespace.QName;
  21 +import org.alfresco.service.transaction.TransactionService;
  22 +import org.alfresco.util.ApplicationContextHelper;
  23 +import org.apache.commons.logging.Log;
  24 +import org.apache.commons.logging.LogFactory;
  25 +import org.springframework.context.ApplicationContext;
  26 +
  27 +/**
  28 + *
  29 + * @author rjmfernandes@gmail.com
  30 + *
  31 + */
  32 +public class TrashcanCleanerTest extends TestCase {
  33 +
  34 + private static final int BATCH_SIZE = 1000;
  35 +
  36 + private static Log logger = LogFactory.getLog(TrashcanCleanerTest.class);
  37 +
  38 + private static ApplicationContext applicationContext = ApplicationContextHelper
  39 + .getApplicationContext();
  40 + protected NodeService nodeService;
  41 + protected TransactionService transactionService;
  42 + protected Repository repository;
  43 + protected AuthenticationComponent authenticationComponent;
  44 +
  45 + @Override
  46 + public void setUp() {
  47 + nodeService = (NodeService) applicationContext.getBean("nodeService");
  48 + authenticationComponent = (AuthenticationComponent) applicationContext
  49 + .getBean("authenticationComponent");
  50 + transactionService = (TransactionService) applicationContext
  51 + .getBean("transactionComponent");
  52 + repository = (Repository) applicationContext
  53 + .getBean("repositoryHelper");
  54 +
  55 + // Authenticate as the system user
  56 + authenticationComponent.setSystemUserAsCurrentUser();
  57 + }
  58 +
  59 + @Override
  60 + public void tearDown() {
  61 + authenticationComponent.clearCurrentSecurityContext();
  62 + }
  63 +
  64 + public void testCleanSimple() throws Throwable {
  65 + cleanBatchTest(1, 0);
  66 + }
  67 +
  68 + public void testCleanBatch() throws Throwable {
  69 + cleanBatchTest(BATCH_SIZE + 1, 1);
  70 + }
  71 +
  72 + private void cleanBatchTest(int nodesCreate, int nodesRemain)
  73 + throws Throwable {
  74 + UserTransaction userTransaction1 = transactionService
  75 + .getUserTransaction();
  76 + try {
  77 + userTransaction1.begin();
  78 + TrashcanCleaner cleaner = new TrashcanCleaner(nodeService,BATCH_SIZE,-1);
  79 + createAndDeleteNodes(nodesCreate);
  80 + long nodesToDelete = getNumberOfNodesInTrashcan();
  81 + logger.info(String.format("Existing nodes to delete: %s",
  82 + nodesToDelete));
  83 + cleaner.clean();
  84 + nodesToDelete = getNumberOfNodesInTrashcan();
  85 + logger.info(String.format("Existing nodes to delete after: %s",
  86 + nodesToDelete));
  87 + assertEquals(nodesToDelete, nodesRemain);
  88 + logger.info("Clean trashcan...");
  89 + cleaner.clean();
  90 + userTransaction1.commit();
  91 + } catch (Throwable e) {
  92 + try {
  93 + userTransaction1.rollback();
  94 + } catch (IllegalStateException ee) {
  95 + }
  96 + throw e;
  97 + }
  98 + }
  99 +
  100 + private void createAndDeleteNodes(int n) {
  101 + for (int i = n; i > 0; i--) {
  102 + createAndDeleteNode();
  103 + }
  104 +
  105 + }
  106 +
  107 + private void createAndDeleteNode() {
  108 + NodeRef companyHome = repository.getCompanyHome();
  109 + String name = "Sample (" + System.currentTimeMillis() + ")";
  110 + Map<QName, Serializable> contentProps = new HashMap<QName, Serializable>();
  111 + contentProps.put(ContentModel.PROP_NAME, name);
  112 + ChildAssociationRef association = nodeService.createNode(companyHome,
  113 + ContentModel.ASSOC_CONTAINS, QName.createQName(
  114 + NamespaceService.CONTENT_MODEL_PREFIX, name),
  115 + ContentModel.TYPE_CONTENT, contentProps);
  116 + nodeService.deleteNode(association.getChildRef());
  117 +
  118 + }
  119 +
  120 + private long getNumberOfNodesInTrashcan() {
  121 + StoreRef archiveStore = new StoreRef("archive://SpacesStore");
  122 + NodeRef archiveRoot = nodeService.getRootNode(archiveStore);
  123 + List<ChildAssociationRef> childAssocs = nodeService
  124 + .getChildAssocs(archiveRoot);
  125 + return childAssocs.size();
  126 +
  127 + }
  128 +
  129 +}
... ...