blob: c729b7ad0da1075af41008e0e297db551937114d [file] [log] [blame]
/*
* 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@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() {
setupConfiguration();
setUpTask();
getCalendarInstance();
createResource(JobManagerConfiguration.DEFAULT_REPOSITORY_PATH);
}
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 current date
private Calendar getCalendarInstance() {
Calendar cal = Calendar.getInstance();
return getCalendarInstance(cal.get(Calendar.YEAR),cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR),cal.get(Calendar.MINUTE));
}
// new calendar based on provided data
private Calendar getCalendarInstance(int year, int month, int day, int hour, int minute) {
Calendar calendar = Calendar.getInstance();
calendar.set(year,month,day,hour,minute);
this.clock = new DynamicClock(Clock.fixed(calendar.toInstant(), ZoneId.systemDefault()), null);
task.setClock(clock);
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(2022,4,17,17,30); // 2022 May 17, 17:30
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));
deleteResource(job);
// additional 72 hours
for( int i = 0; i < 72; i++) {
simulate(60, Duration.ofMinutes(1));
}
assertEquals(4, countFolders(UNASSIGNED_JOBS_JCR_PATH)); // unassigned/test/2022/05
}
@Test
public void testUnassignedDec31() throws PersistenceException {
Calendar calendar = getCalendarInstance(2021,11,31,17,30); // 2021 Dec 31, 17:30
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));
deleteResource(job);
// 72 hours later => now it's 2022
for( int i = 0; i < 72; i++) {
simulate(60, Duration.ofMinutes(1));
}
assertEquals(2, countFolders(UNASSIGNED_JOBS_JCR_PATH)); // unassigned/test
}
}