Commit 2ba602c74d12796589f9b4eea71d1d365a7a84b1
1 parent
6240e8e9a5
Exists in
master
first version
Showing
7 changed files
with
461 additions
and
1 deletions
Show diff stats
.gitignore
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 | +} | ... | ... |