Merge pull request #18 from stefan-egli/SLING-9905
SLING-9905 : CleanupTest added to the project
diff --git a/pom.xml b/pom.xml
index 8a0e96d..9a5d42f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -327,6 +327,18 @@
<version>3.1.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.discovery.commons</artifactId>
+ <version>1.0.24</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.discovery.base</artifactId>
+ <version>2.0.10</version>
+ <scope>test</scope>
+ </dependency>
<!-- JUnit -->
<dependency>
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java
index 05bc079..66dc825 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java
@@ -33,9 +33,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
+import java.util.Date;
import java.util.Iterator;
import java.util.List;
@@ -67,6 +69,9 @@
/** We count the scheduler runs. */
private volatile long schedulerRuns;
+ /** specific clock instance that tests can the use to fiddle around with */
+ private Clock clock = Clock.systemDefaultZone();
+
/**
* Constructor
*/
@@ -75,6 +80,22 @@
this.jobScheduler = jobScheduler;
}
+ /** test hook to overwrite the default clock and fiddle with it */
+ void setClock(Clock clock) {
+ this.clock = clock;
+ }
+
+ private final Calendar getCalendarInstance() {
+ Calendar calendar = Calendar.getInstance();
+ // explicitly set the time based on the clock to allow test fiddlings
+ calendar.setTimeInMillis(clock.millis());
+ return calendar;
+ }
+
+ private final long currentTimeMillis() {
+ return clock.millis();
+ }
+
/**
* One maintenance run
*/
@@ -115,7 +136,7 @@
if (this.configuration.getHistoryCleanUpRemovedJobs() > 0 &&
schedulerRuns % 60 == 1) {
- Calendar removeDate = Calendar.getInstance();
+ Calendar removeDate = getCalendarInstance();
removeDate.add(Calendar.MINUTE, - this.configuration.getHistoryCleanUpRemovedJobs());
this.historyCleanUpRemovedJobs(removeDate);
}
@@ -187,7 +208,7 @@
this.logger.debug("Cleaning up job resource tree: looking for empty folders");
final ResourceResolver resolver = this.configuration.createResourceResolver();
try {
- final Calendar cleanUpDate = Calendar.getInstance();
+ final Calendar cleanUpDate = getCalendarInstance();
// go back five minutes
cleanUpDate.add(Calendar.MINUTE, -5);
@@ -255,7 +276,7 @@
final Resource baseResource = resolver.getResource(basePath);
// sanity check - should never be null
if ( baseResource != null ) {
- final Calendar now = Calendar.getInstance();
+ final Calendar now = getCalendarInstance();
final int removeYear = now.get(Calendar.YEAR);
final int removeMonth = now.get(Calendar.MONTH) + 1;
final int removeDay = now.get(Calendar.DAY_OF_MONTH);
@@ -385,7 +406,8 @@
if ( !hasJobs(caps, r) ) {
// check for timestamp
final long timestamp = r.getValueMap().get(PROPERTY_LAST_CHECKED, -1L);
- if ( timestamp > 0 && (timestamp + KEEP_DURATION <= System.currentTimeMillis()) ) {
+ final long now = currentTimeMillis();
+ if ( timestamp > 0 && (timestamp + KEEP_DURATION <= now) ) {
toDelete.add(r);
if ( toDelete.size() == MAX_REMOVE_ID_FOLDERS ) {
break;
@@ -393,7 +415,7 @@
} else if ( timestamp == -1 ) {
final ModifiableValueMap mvm = r.adaptTo(ModifiableValueMap.class);
if ( mvm != null ) {
- mvm.put(PROPERTY_LAST_CHECKED, System.currentTimeMillis());
+ mvm.put(PROPERTY_LAST_CHECKED, now);
resolver.commit();
}
}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTest.java b/src/test/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTest.java
new file mode 100644
index 0000000..4be8487
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTest.java
@@ -0,0 +1,419 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import static org.junit.Assert.assertEquals;
+
+import java.text.SimpleDateFormat;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.UUID;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.DefaultTopologyView;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CleanUpTest {
+
+ static class DynamicClock extends Clock {
+
+ private final Clock baseClock;
+ private Duration offset;
+
+ DynamicClock(Clock baseClock, Duration offsetOrNull) {
+ this.baseClock = baseClock;
+ setOffset(offsetOrNull);
+ }
+
+ public void setOffset(Duration offsetOrNull) {
+ this.offset = offsetOrNull;
+ }
+
+ public void incrementOffset(Duration offsetOrNull) {
+ if (offsetOrNull == null) {
+ // then why call this?
+ return;
+ }
+ if (this.offset == null) {
+ this.offset = offsetOrNull;
+ } else {
+ final long newOffsetMillis = Math.addExact(offsetOrNull.toMillis(), offset.toMillis());
+ this.offset = Duration.ofMillis(newOffsetMillis);
+ }
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return baseClock.getZone();
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return new DynamicClock(baseClock.withZone(zone), offset);
+ }
+
+ @Override
+ public Instant instant() {
+ if (offset == null) {
+ return baseClock.instant();
+ }
+ return baseClock.instant().plus(offset);
+ }
+
+ @Override
+ public long millis() {
+ if (offset == null) {
+ return baseClock.millis();
+ }
+ return Math.addExact(baseClock.millis(), offset.toMillis());
+ }
+ }
+
+ private static final String JCR_PATH = JobManagerConfiguration.DEFAULT_REPOSITORY_PATH + "/cancelled";
+ private static final String ASSIGNED_JOBS_JCR_PATH = JobManagerConfiguration.DEFAULT_REPOSITORY_PATH + "/assigned";
+ private static final String UNASSIGNED_JOBS_JCR_PATH = JobManagerConfiguration.DEFAULT_REPOSITORY_PATH + "/unassigned";
+ private static final String JCR_TOPIC = "test";
+ private static final String JCR_JOB_NAME = "test-job";
+ private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy/MM/dd/HH/mm");
+// private static final int MAX_AGE_IN_DAYS = 60;
+
+ @Rule
+ public final SlingContext ctx = new SlingContext();
+
+ @Mock
+ private JobManagerConfiguration configuration;
+ @Mock
+ private JobSchedulerImpl jobScheduler;
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.LENIENT);
+
+ private final String localSlingId = "myLocalSlingId";
+ private CleanUpTask task;
+ private TopologyCapabilities capabilities;
+ private TopologyView view;
+ private DynamicClock clock;
+
+ @Before
+ public void setUp() {
+ // let's use a fixed calendar which we can fast-forward as needed
+ Calendar realCalendar = Calendar.getInstance();
+ clock = new DynamicClock(Clock.fixed(realCalendar.toInstant(), ZoneId.systemDefault()), null);
+
+ setupConfiguration();
+ setUpTask();
+ createResource(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH);
+
+ task.setClock(clock);
+ }
+ public static DefaultInstanceDescription createInstanceDescription(
+ String instanceId, boolean isLocal, ClusterView clusterView) {
+ if (!(clusterView instanceof DefaultClusterView)) {
+ throw new IllegalArgumentException(
+ "Must pass a clusterView of type "
+ + DefaultClusterView.class);
+ }
+ DefaultInstanceDescription i = new DefaultInstanceDescription(
+ (DefaultClusterView) clusterView, false, isLocal, instanceId, new HashMap<String, String>());
+ return i;
+ }
+
+ public static DefaultTopologyView createTopologyView(String clusterViewId,
+ String slingId) {
+ DefaultTopologyView t = new DefaultTopologyView();
+ DefaultClusterView c = new DefaultClusterView(clusterViewId);
+ DefaultInstanceDescription i = new DefaultInstanceDescription(
+ c, true, true, slingId, new HashMap<String, String>());
+ Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+ instances.add(i);
+ t.addInstances(instances);
+ return t;
+ }
+
+ private TopologyView createView(boolean current) {
+ return createTopologyView(UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ }
+
+ private void setupConfiguration() {
+ Mockito.when(configuration.getLocalJobsPath()).thenReturn(ASSIGNED_JOBS_JCR_PATH + "/" + localSlingId);
+ Mockito.when(configuration.getUnassignedJobsPath()).thenReturn(UNASSIGNED_JOBS_JCR_PATH);
+ Mockito.when(configuration.getStoredCancelledJobsPath()).thenReturn(JCR_PATH);
+ Mockito.when(configuration.createResourceResolver()).thenReturn(ctx.resourceResolver());
+ Mockito.when(configuration.getHistoryCleanUpRemovedJobs()).thenReturn(1);
+
+ view = createView(true);
+ Mockito.when(configuration.getAssginedJobsPath()).thenReturn(ASSIGNED_JOBS_JCR_PATH);
+
+ capabilities = new TopologyCapabilities(view, configuration);
+ Mockito.when(configuration.getTopologyCapabilities()).thenReturn(capabilities);
+ Mockito.when(capabilities.isActive()).thenReturn(true);
+ }
+
+ private void setUpTask() {
+ task = new CleanUpTask(configuration, jobScheduler);
+ }
+
+// private Resource createJobResourceForDate(Calendar cal, String status) {
+// String path = JCR_PATH + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime()) + '/' + JCR_JOB_NAME;
+// return ctx.create().resource(path, JobImpl.PROPERTY_FINISHED_STATE, status);
+// }
+
+ private Resource createJobResourceForDate(String rootPath, Calendar cal) {
+ String path = rootPath + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime()) + '/' + JCR_JOB_NAME;
+ return ctx.create().resource(path, "sling:resourceType", ResourceHelper.RESOURCE_TYPE_JOB);
+ }
+
+// private void deleteJobResourceForDate(String rootPath, Calendar cal) throws PersistenceException {
+// String path = rootPath + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime()) + '/' + JCR_JOB_NAME;
+// ResourceResolver resolver = ctx.resourceResolver();
+// Resource r = resolver.getResource(path);
+// resolver.delete(r);
+// resolver.commit();
+// }
+
+ private void deleteResource(Resource r) throws PersistenceException {
+ ResourceResolver resolver = ctx.resourceResolver();
+ resolver.delete(r);
+ resolver.commit();
+ }
+
+ private Resource createEmptyJobResourceForDate(String rootPath, Calendar cal) {
+ String path = rootPath + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime());
+ return ctx.create().resource(path);
+ }
+
+ private Resource createResource(String rootPath) {
+ return ctx.create().resource(rootPath);
+ }
+
+ // new calendar based on the (test) clock
+ private Calendar getCalendarInstance() {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(clock.millis());
+ return calendar;
+ }
+
+ private void simulate(int num, Duration duration) {
+ for(int i=0; i<num; i++) {
+ clock.incrementOffset(duration);
+ task.run();
+ }
+ }
+ private int countFolders(String path) {
+ final ResourceResolver resolver = this.configuration.createResourceResolver();
+ if ( resolver == null ) {
+ return -1;
+ }
+ try {
+ final Resource baseResource = resolver.getResource(path);
+ return countChildren(baseResource);
+ } finally {
+ resolver.close();
+ }
+ }
+ private int countChildren(Resource parent) {
+ if (parent == null) {
+ return 0;
+ }
+ int count = 0;
+ final Iterator<Resource> it = parent.listChildren();
+ if (it != null) {
+ while( it.hasNext() ) {
+ final Resource child = it.next();
+ final int childCount = countChildren(child);
+ count += childCount;
+ }
+ }
+ return 1 + count;
+ }
+
+ @Test
+ public void testEmpty() {
+ assertEquals(1, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ createResource(ASSIGNED_JOBS_JCR_PATH);
+ createResource(UNASSIGNED_JOBS_JCR_PATH);
+ assertEquals(3, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ // 2 days worth of cleanup
+ for( int i = 0; i < 48; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(3, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ }
+
+ @Test
+ public void testAssignedEmpty() {
+ assertEquals(1, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ createResource(ASSIGNED_JOBS_JCR_PATH);
+ assertEquals(2, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ for( int i = 0; i < 100; i++) {
+ createResource(ASSIGNED_JOBS_JCR_PATH + "/" + UUID.randomUUID().toString());
+ }
+ assertEquals(102, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ // 2 days worth of cleanup
+ for( int i = 0; i < 480; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(2, countFolders(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH));
+ }
+
+ @Test
+ public void testAssignedSimple_nonLocal() throws PersistenceException {
+ final String mySlingId = UUID.randomUUID().toString();
+ Calendar calendar = getCalendarInstance();
+ Resource job = createJobResourceForDate(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId, calendar);
+ calendar.add(Calendar.DAY_OF_YEAR,-1);
+ createEmptyJobResourceForDate(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId, calendar);
+ assertEquals(11, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+
+ // full clean up is done every hour
+ // so let's simulate an hour
+ simulate(60, Duration.ofMinutes(1));
+
+ // after the first run it will just have marked the folder for deletion, but not deleted, so counter is still the same
+ assertEquals(11, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+
+ // simulate 72 hours
+ for( int i = 0; i < 72; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ // nothing should be deleted
+ assertEquals(11, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+
+ deleteResource(job);
+ for( int i = 0; i < 72; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(0, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+ }
+
+ @Test
+ public void testAssignedSimple_local() throws PersistenceException {
+ final String mySlingId = localSlingId;
+ Calendar calendar = getCalendarInstance();
+ Resource jobResource = createJobResourceForDate(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId, calendar);
+ calendar.add(Calendar.DAY_OF_YEAR,-1);
+ createEmptyJobResourceForDate(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId, calendar);
+ assertEquals(11, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+
+ // full clean up is done every hour
+ // so let's simulate an hour
+ simulate(60, Duration.ofMinutes(1));
+
+ // after the first run it will just have marked the folder for deletion, but not deleted, so counter is still the same
+ assertEquals(8, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+
+ // simulate 24 hours - still nothing
+ for( int i = 0; i < 24; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ assertEquals("i = " + i, 8, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+ }
+
+ // lets delete the job
+ deleteResource(jobResource);
+
+ for( int i = 0; i < 25; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(0, countFolders(ASSIGNED_JOBS_JCR_PATH + "/" + mySlingId));
+ }
+
+ @Test
+ public void testUnassigned_nothingToDelete() {
+ Calendar calendar = getCalendarInstance();
+ createJobResourceForDate(UNASSIGNED_JOBS_JCR_PATH, calendar);
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ // full clean up is done every hour
+ simulate(60, Duration.ofMinutes(1));
+
+ // fullEmptyFolderCleanup deletes only if 2 hour values older - so nothing changed yet
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ simulate(60, Duration.ofMinutes(1));
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ // simulate 23 hours
+ for( int i = 0; i < 23; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ assertEquals("i = " + i, 8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+ }
+
+ // now on the next run it should finally do its job
+ simulate(60, Duration.ofMinutes(1));
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+ }
+
+ @Test
+ public void testUnassignedSimple() throws PersistenceException {
+ Calendar calendar = getCalendarInstance();
+ Resource job = createJobResourceForDate(UNASSIGNED_JOBS_JCR_PATH, calendar);
+ calendar.add(Calendar.DAY_OF_YEAR,-1);
+ createEmptyJobResourceForDate(UNASSIGNED_JOBS_JCR_PATH, calendar);
+ assertEquals(11, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ // full clean up is done every hour
+ // so let's simulate an hour
+ simulate(60, Duration.ofMinutes(1));
+
+ // after the first run it will just have marked the folder for deletion, but not deleted, so counter is still the same
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ // simulate 72 hours
+ for( int i = 0; i < 72; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(8, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+
+ deleteResource(job);
+
+ for( int i = 0; i < 72; i++) {
+ simulate(60, Duration.ofMinutes(1));
+ }
+ assertEquals(4, countFolders(UNASSIGNED_JOBS_JCR_PATH));
+ }
+}