You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2020/03/19 19:27:15 UTC

[sling-org-apache-sling-app-cms] 04/04: Fixes SLING-9225 - Adding repository cleanup jobs

This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-app-cms.git

commit a364a3ab6ac8c38052d02e6623126cba18d5cecb
Author: Dan Klco <dk...@apache.org>
AuthorDate: Thu Mar 19 15:24:41 2020 -0400

    Fixes SLING-9225 - Adding repository cleanup jobs
---
 .../repository/AbstractMaintenanceJob.java         | 124 +++++++++++++++++++
 .../repository/DataStoreCleanupConfig.java         |  31 +++++
 .../repository/DataStoreCleanupScheduler.java      |  78 ++++++++++++
 .../internal/repository/RevisionCleanupConfig.java |  31 +++++
 .../repository/RevisionCleanupScheduler.java       |  77 ++++++++++++
 .../main/resources/OSGI-INF/l10n/bundle.properties |  15 +++
 .../repository/AbstractMaintenanceJobTest.java     | 135 +++++++++++++++++++++
 .../internal/repository/CompositeDataMock.java     |  55 +++++++++
 .../repository/DataStoreCleanupSchedulerTest.java  |  73 +++++++++++
 .../repository/RevisionCleanupSchedulerTest.java   |  73 +++++++++++
 ...nal.repository.DataStoreCleanupScheduler.config |  19 +++
 ...rnal.repository.RevisionCleanupScheduler.config |  19 +++
 12 files changed, 730 insertions(+)

diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJob.java b/core/src/main/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJob.java
new file mode 100644
index 0000000..af0fef1
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJob.java
@@ -0,0 +1,124 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext.ResultBuilder;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for running the Jackrabbit OAK Segment Store cleanup on a schedule.
+ */
+public abstract class AbstractMaintenanceJob implements Runnable, JobExecutor {
+
+    private static final Logger log = LoggerFactory.getLogger(AbstractMaintenanceJob.class);
+
+    protected JobManager jobManager;
+
+    protected final JobExecutionResult createResult(JobExecutionContext context, Optional<CompositeData> data,
+            Integer startId) {
+        String message = data.map(d -> ((String) d.get("message"))).orElse(null);
+        StatusCode code = data.map(d -> ((Integer) d.get("code"))).map(c -> Arrays.stream(StatusCode.values())
+                .filter(sc -> sc.ordinal() == c).findFirst().orElse(StatusCode.NONE)).orElse(StatusCode.NONE);
+        log.trace("Loaded status code: {}", code);
+        Integer id = data.map(d -> ((Integer) d.get("id"))).orElse(null);
+        boolean success = false;
+        StringBuilder sb = new StringBuilder(getPrefix());
+        if (!data.isPresent() || code == null) {
+            log.trace("No result...");
+            sb.append("No result.");
+        } else if (startId != null && (id == null || id.intValue() != startId.intValue())) {
+            log.trace("ID does not match original ID, assuming successful...");
+            sb.append(StatusCode.SUCCEEDED.name);
+            success = true;
+        } else if (code == StatusCode.INITIATED || code == StatusCode.SUCCEEDED) {
+            log.trace("Successful result: {}...", code.name);
+            sb.append(code.name);
+            success = true;
+        } else if (code == StatusCode.UNAVAILABLE || code == StatusCode.NONE || code == StatusCode.FAILED) {
+            log.trace("Failed result: {}...", code.name);
+            sb.append(code.name);
+        } else {
+            return null;
+        }
+        if (message != null) {
+            sb.append(" ");
+            sb.append(message);
+        }
+        ResultBuilder rb = context.result().message(sb.toString());
+        return success ? rb.succeeded() : rb.failed();
+    }
+
+    public abstract String getJobTopic();
+
+    public abstract String getPrefix();
+
+    public abstract Optional<CompositeData> getStatus();
+
+    public JobExecutionResult process(Job job, JobExecutionContext context) {
+        log.info("Starting {}", getPrefix());
+        Optional<CompositeData> data = startMaintenance();
+        Integer id = data.map(d -> ((Integer) d.get("id"))).orElse(null);
+        JobExecutionResult result = null;
+        while (result == null) {
+            data = getStatus();
+            result = createResult(context, data, id);
+            if (result == null) {
+                if (context.isStopped()) {
+                    log.info(
+                            "Canceling {}. The task was either stopped by the user or the Maintenance Window reached its end",
+                            getPrefix());
+                    stopMaintenance();
+                    return context.result().message(String.format("%sStopped by user.", getPrefix())).failed();
+                }
+                try {
+                    Thread.sleep(1000L);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            } else {
+                log.debug("Retrieved result: {}", result);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void run() {
+        log.trace("Kicking off job: {}", getJobTopic());
+        jobManager.addJob(getJobTopic(), Collections.emptyMap());
+    }
+
+    public abstract void setJobManager(JobManager jobManager);
+
+    public abstract Optional<CompositeData> startMaintenance();
+
+    public abstract Optional<CompositeData> stopMaintenance();
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupConfig.java b/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupConfig.java
new file mode 100644
index 0000000..7ca6ca8
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupConfig.java
@@ -0,0 +1,31 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+
+/**
+ * Configuration for the DataStore Cleanup Service
+ */
+@ObjectClassDefinition(name = "%datastore.cleanup.name", description = "%datastore.cleanup.description", localization = "OSGI-INF/l10n/bundle")
+public @interface DataStoreCleanupConfig {
+
+    @AttributeDefinition(name = "%scheduler.expression.name", description = "%scheduler.expression.description")
+    String scheduler_expression();
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupScheduler.java b/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupScheduler.java
new file mode 100644
index 0000000..7211f02
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupScheduler.java
@@ -0,0 +1,78 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import java.util.Optional;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+
+/**
+ * Service for running the Jackrabbit OAK Segment Store cleanup on a schedule.
+ */
+@Component(service = { JobExecutor.class, Runnable.class }, property = { JobExecutor.PROPERTY_TOPICS
+        + "=org/apache/sling/cms/repository/DataStoreCleanup" }, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = DataStoreCleanupConfig.class)
+public class DataStoreCleanupScheduler extends AbstractMaintenanceJob {
+
+    private RepositoryManagementMBean repositoryManager;
+
+    @Override
+    public String getJobTopic() {
+        return "org/apache/sling/cms/repository/DataStoreCleanup";
+    }
+
+    @Override
+    public String getPrefix() {
+        return "DataStore Cleanup";
+    }
+
+    @Override
+    public Optional<CompositeData> getStatus() {
+        return Optional.ofNullable(repositoryManager.getDataStoreGCStatus());
+    }
+
+    @Reference
+    @Override
+    public void setJobManager(final JobManager jobManager) {
+        super.jobManager = jobManager;
+    }
+
+    @Reference
+    public void setRepositoryManager(final RepositoryManagementMBean repositoryManager) {
+        this.repositoryManager = repositoryManager;
+    }
+
+    @Override
+    public Optional<CompositeData> startMaintenance() {
+        return Optional.ofNullable(repositoryManager.startDataStoreGC(false));
+    }
+
+    @Override
+    public Optional<CompositeData> stopMaintenance() {
+        // Can't really stop this one
+        return Optional.ofNullable(repositoryManager.getDataStoreGCStatus());
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupConfig.java b/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupConfig.java
new file mode 100644
index 0000000..14c0c1f
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupConfig.java
@@ -0,0 +1,31 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+
+/**
+ * Configuration for the Reference Mapping Transformer
+ */
+@ObjectClassDefinition(name = "%revision.cleanup.name", description = "%revision.cleanup.description", localization = "OSGI-INF/l10n/bundle")
+public @interface RevisionCleanupConfig {
+
+    @AttributeDefinition(name = "%scheduler.expression.name", description = "%scheduler.expression.description")
+    String scheduler_expression();
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupScheduler.java b/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupScheduler.java
new file mode 100644
index 0000000..3d5afcd
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupScheduler.java
@@ -0,0 +1,77 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import java.util.Optional;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+
+/**
+ * Service for running the Jackrabbit OAK Segment Store cleanup on a schedule.
+ */
+@Component(service = { JobExecutor.class, Runnable.class }, property = { JobExecutor.PROPERTY_TOPICS
+        + "=org/apache/sling/cms/repository/RevisionCleanup" }, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = RevisionCleanupConfig.class)
+public class RevisionCleanupScheduler extends AbstractMaintenanceJob {
+
+    private RepositoryManagementMBean repositoryManager;
+
+    @Override
+    public String getJobTopic() {
+        return "org/apache/sling/cms/repository/RevisionCleanup";
+    }
+
+    @Override
+    public String getPrefix() {
+        return "Revision Cleanup";
+    }
+
+    @Override
+    public Optional<CompositeData> getStatus() {
+        return Optional.ofNullable(repositoryManager.getRevisionGCStatus());
+    }
+
+    @Reference
+    @Override
+    public void setJobManager(final JobManager jobManager) {
+        super.jobManager = jobManager;
+    }
+
+    @Reference
+    public void setRepositoryManager(final RepositoryManagementMBean repositoryManager) {
+        this.repositoryManager = repositoryManager;
+    }
+
+    @Override
+    public Optional<CompositeData> startMaintenance() {
+        return Optional.ofNullable(repositoryManager.startRevisionGC());
+    }
+
+    @Override
+    public Optional<CompositeData> stopMaintenance() {
+        return Optional.ofNullable(repositoryManager.cancelRevisionGC());
+    }
+
+}
diff --git a/core/src/main/resources/OSGI-INF/l10n/bundle.properties b/core/src/main/resources/OSGI-INF/l10n/bundle.properties
index 454ee05..282c357 100644
--- a/core/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/core/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -22,6 +22,21 @@
 # descriptions as used in the metatype.xml descriptor generated by the
 # the Sling SCR plugin
 
+## Global Entries
+scheduler.expression.name=Quartz Scheduler Expression
+scheduler.expression.description=A quartz expression for configuring when \
+a scheduler should be triggered
+
+# DataStore Cleanup Entries
+datastore.cleanup.name=Apache Sling CMS DataStore Cleanup
+datastore.cleanup.description=Initiate a Data Store garbage collection operation \
+the Jackrabbit OAK repository
+
+# Revision Cleanup Entries
+revision.cleanup.name=Apache Sling CMS Revision Cleanup
+revision.cleanup.description=Initiate a revision garbage collection operation \
+the Jackrabbit OAK repository
+
 ## CMS Security Filter Entries
 cms.security.filter.name=Apache Sling CMS Security Filter
 cms.security.filter.description=Checks to ensure that the user is logged in \
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJobTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJobTest.java
new file mode 100644
index 0000000..9b0521a
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/repository/AbstractMaintenanceJobTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import static org.junit.Assert.assertNull;
+
+import java.util.Optional;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext.ResultBuilder;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class AbstractMaintenanceJobTest {
+
+    private AbstractMaintenanceJob amj;
+    private JobExecutionContext context;
+    private ResultBuilder resultBuilder;
+
+    @Before
+    public void init() {
+        amj = new AbstractMaintenanceJob() {
+            @Override
+            public String getJobTopic() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public String getPrefix() {
+                return "Test Job";
+            }
+
+            @Override
+            public Optional<CompositeData> getStatus() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void setJobManager(JobManager jobManager) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Optional<CompositeData> startMaintenance() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Optional<CompositeData> stopMaintenance() {
+                throw new UnsupportedOperationException();
+            }
+        };
+
+        context = Mockito.mock(JobExecutionContext.class);
+        resultBuilder = Mockito.mock(ResultBuilder.class);
+        Mockito.when(resultBuilder.message(Mockito.anyString())).thenReturn(resultBuilder);
+        Mockito.when(context.result()).thenReturn(resultBuilder);
+    }
+
+    @Test
+    public void testRunningResult() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", StatusCode.RUNNING.ordinal())
+                .put("message", "Hello World").put("id", id).build();
+        JobExecutionResult result = amj.createResult(context, Optional.ofNullable(data), id);
+        assertNull(result);
+    }
+
+    @Test
+    public void testNewIdResult() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", StatusCode.RUNNING.ordinal())
+                .put("message", "Hello World").put("id", 2).build();
+        amj.createResult(context, Optional.ofNullable(data), id);
+        Mockito.verify(resultBuilder).succeeded();
+    }
+
+    @Test
+    public void testFailedResult() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", StatusCode.FAILED.ordinal())
+                .put("message", "Hello World").put("id", id).build();
+        amj.createResult(context, Optional.ofNullable(data), id);
+        Mockito.verify(resultBuilder).failed();
+
+    }
+
+    @Test
+    public void testNoneResult() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", StatusCode.NONE.ordinal())
+                .put("message", "Hello World").put("id", id).build();
+        amj.createResult(context, Optional.ofNullable(data), id);
+        Mockito.verify(resultBuilder).failed();
+    }
+
+    @Test
+    public void testInvalidCode() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", 2345677).put("message", "Hello World").put("id", id)
+                .build();
+        amj.createResult(context, Optional.ofNullable(data), id);
+        Mockito.verify(resultBuilder).failed();
+    }
+
+    @Test
+    public void testSucceededResult() {
+        int id = 1;
+        CompositeData data = CompositeDataMock.init().put("code", StatusCode.SUCCEEDED.ordinal())
+                .put("message", "Hello World").put("id", id).build();
+        amj.createResult(context, Optional.ofNullable(data), id);
+        Mockito.verify(resultBuilder).succeeded();
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/repository/CompositeDataMock.java b/core/src/test/java/org/apache/sling/cms/core/internal/repository/CompositeDataMock.java
new file mode 100644
index 0000000..28f628d
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/repository/CompositeDataMock.java
@@ -0,0 +1,55 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.management.openmbean.CompositeData;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * A mock for creating a fluent API version of a CompositeData Object.
+ */
+public class CompositeDataMock {
+
+    private Map<String, Object> data = new HashMap<>();
+
+    public static CompositeDataMock init() {
+        return new CompositeDataMock();
+    }
+
+    public CompositeDataMock put(String key, Object value) {
+        this.data.put(key, value);
+        return this;
+    }
+
+    public CompositeData build() {
+        CompositeData dc = Mockito.mock(CompositeData.class);
+        Mockito.when(dc.get(Mockito.anyString())).thenAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                return data.get(invocation.getArguments()[0]);
+            }
+        });
+        return dc;
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupSchedulerTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupSchedulerTest.java
new file mode 100644
index 0000000..d3918a6
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/repository/DataStoreCleanupSchedulerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext.ResultBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class DataStoreCleanupSchedulerTest {
+
+    private JobExecutionContext context;
+    private ResultBuilder resultBuilder;
+
+    @Before
+    public void init() {
+        context = Mockito.mock(JobExecutionContext.class);
+        resultBuilder = Mockito.mock(ResultBuilder.class);
+        Mockito.when(resultBuilder.message(Mockito.anyString())).thenReturn(resultBuilder);
+        Mockito.when(resultBuilder.succeeded()).thenReturn(Mockito.mock(JobExecutionResult.class));
+        Mockito.when(resultBuilder.failed()).thenReturn(Mockito.mock(JobExecutionResult.class));
+        Mockito.when(context.result()).thenReturn(resultBuilder);
+    }
+
+    @Test
+    public void testRunnable() {
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler();
+        final JobManager jobManager = Mockito.mock(JobManager.class);
+        dscs.setJobManager(jobManager);
+
+        Mockito.when(jobManager.addJob(Mockito.eq(dscs.getJobTopic()), Mockito.anyMap())).then((answer) -> {
+            dscs.process(Mockito.mock(Job.class), context);
+            return null;
+        });
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData startingCd = CompositeDataMock.init().put("id", id).build();
+        Mockito.when(repositoryManager.startDataStoreGC(false)).thenReturn(startingCd);
+        CompositeData doneCd = CompositeDataMock.init().put("id", id).put("code", StatusCode.SUCCEEDED.ordinal())
+                .build();
+        Mockito.when(repositoryManager.getDataStoreGCStatus()).thenReturn(doneCd);
+        dscs.setRepositoryManager(repositoryManager);
+
+        dscs.run();
+
+        Mockito.verify(repositoryManager).startDataStoreGC(false);
+        Mockito.verify(resultBuilder).succeeded();
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupSchedulerTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupSchedulerTest.java
new file mode 100644
index 0000000..ba8f255
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/repository/RevisionCleanupSchedulerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.cms.core.internal.repository;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext.ResultBuilder;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class RevisionCleanupSchedulerTest {
+
+    private JobExecutionContext context;
+    private ResultBuilder resultBuilder;
+
+    @Before
+    public void init() {
+        context = Mockito.mock(JobExecutionContext.class);
+        resultBuilder = Mockito.mock(ResultBuilder.class);
+        Mockito.when(resultBuilder.message(Mockito.anyString())).thenReturn(resultBuilder);
+        Mockito.when(resultBuilder.succeeded()).thenReturn(Mockito.mock(JobExecutionResult.class));
+        Mockito.when(resultBuilder.failed()).thenReturn(Mockito.mock(JobExecutionResult.class));
+        Mockito.when(context.result()).thenReturn(resultBuilder);
+    }
+
+    @Test
+    public void testRunnable() {
+        final RevisionCleanupScheduler dscs = new RevisionCleanupScheduler();
+        final JobManager jobManager = Mockito.mock(JobManager.class);
+        dscs.setJobManager(jobManager);
+
+        Mockito.when(jobManager.addJob(Mockito.eq(dscs.getJobTopic()), Mockito.anyMap())).then((answer) -> {
+            dscs.process(Mockito.mock(Job.class), context);
+            return null;
+        });
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData startingCd = CompositeDataMock.init().put("id", id).build();
+        Mockito.when(repositoryManager.startDataStoreGC(false)).thenReturn(startingCd);
+        CompositeData doneCd = CompositeDataMock.init().put("id", id).put("code", StatusCode.SUCCEEDED.ordinal())
+                .build();
+        Mockito.when(repositoryManager.getRevisionGCStatus()).thenReturn(doneCd);
+        dscs.setRepositoryManager(repositoryManager);
+
+        dscs.run();
+
+        Mockito.verify(repositoryManager).startRevisionGC();
+        Mockito.verify(resultBuilder).succeeded();
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.DataStoreCleanupScheduler.config b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.DataStoreCleanupScheduler.config
new file mode 100644
index 0000000..7e8e14c
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.DataStoreCleanupScheduler.config
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+scheduler.expression="0\ 31\ 1\ ?\ *\ SUN\ *"
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.RevisionCleanupScheduler.config b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.RevisionCleanupScheduler.config
new file mode 100644
index 0000000..e0676b7
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.repository.RevisionCleanupScheduler.config
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+scheduler.expression="0\ 31\ 0\ ?\ *\ SUN\ *"
\ No newline at end of file