blob: 353b6c6587d3a589f43595163cceaba5a130ba22 [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.servlets.post.impl.helper;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>ChunkCleanUpTask</code> implements a job run at regular intervals
* to find incomplete chunk uploads and remove them from the repository to
* prevent littering the repository with incomplete chunks.
* <p>
* This task is configured with OSGi configuration for the PID
* <code>org.apache.sling.servlets.post.impl.helper.ChunkCleanUpTask</code> with
* property <code>scheduler.expression</code> being the schedule to execute the
* task. The schedule is a cron job expression as described at <a
* href="http://www.docjar.com/docs/api/org/quartz/CronTrigger.html">Cron
* Trigger</a> with the default value configured to run twice a day at 0h41m31s
* and 12h4131s.
* <p>
* The property <code>chunk.cleanup.age</code> specifies chunk's age in minutes
* before it is considered for clean up.
* <p>
* Currently the cleanup tasks connects as the administrative user to the
* default workspace assuming users are stored in that workspace and the
* administrative user has full access.
*/
@Component(service = Runnable.class,
property = {
"service.description=Periodic Chunk Cleanup Job",
"service.vendor=The Apache Software Foundation"
})
@Designate(ocd = ChunkCleanUpTask.Config.class)
public class ChunkCleanUpTask implements Runnable {
@ObjectClassDefinition(name = "Apache Sling Post Chunk Upload : Cleanup Task",
description = "Task to regularly purge incomplete chunks from the repository")
public @interface Config {
@AttributeDefinition(name = "Schedule", description = "Cron expression scheudling this job. Default is hourly 17m23s after the hour. "
+ "See http://www.docjar.com/docs/api/org/quartz/CronTrigger.html for a description "
+ "of the format for this value.")
String scheduler_expression() default "31 41 0/12 * * ?";
@AttributeDefinition(name = "scheduler.concurrent",
description = "Allow Chunk Cleanup Task to run concurrently (default: false).")
boolean scheduler_concurrent() default false;
@AttributeDefinition(name = "Cleanup Age",
description = "The chunk's age in minutes before it is considered for clean up.")
int chunk_cleanup_age() default 360;
}
/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());
@Reference
private SlingRepository repository;
private SlingFileUploadHandler uploadhandler = new SlingFileUploadHandler();
/**
* Clean up age criterion in millisec.
*/
private long chunkCleanUpAge;
/**
* Executes the job. Is called for each triggered schedule point.
*/
@Override
public void run() {
log.debug("ChunkCleanUpTask: Starting cleanup");
cleanup();
}
/**
* This method deletes chunks which are {@link #isEligibleForCleanUp(Node)}
* for cleanup. It queries all
* {@link SlingPostConstants#NT_SLING_CHUNK_MIXIN} nodes and filter nodes
* which are {@link #isEligibleForCleanUp(Node)} for cleanup. It then
* deletes old chunks upload.
*/
private void cleanup() {
long start = System.currentTimeMillis();
int numCleaned = 0;
int numLive = 0;
Session admin = null;
try {
// assume chunks are stored in the default workspace
admin = repository.loginAdministrative(null);
QueryManager qm = admin.getWorkspace().getQueryManager();
QueryResult queryres = qm.createQuery(
"SELECT * FROM [sling:chunks] ", Query.JCR_SQL2).execute();
NodeIterator nodeItr = queryres.getNodes();
while (nodeItr.hasNext()) {
Node node = nodeItr.nextNode();
if (isEligibleForCleanUp(node)) {
numCleaned++;
uploadhandler.deleteChunks(node);
} else {
numLive++;
}
}
if (admin.hasPendingChanges()) {
try {
admin.refresh(true);
admin.save();
} catch (InvalidItemStateException iise) {
log.info("ChunkCleanUpTask: Concurrent modification to one or more of the chunk to be removed. Retrying later");
} catch (RepositoryException re) {
log.info("ChunkCleanUpTask: Failed persisting chunk removal. Retrying later");
}
}
} catch (Throwable t) {
log.error(
"ChunkCleanUpTask: General failure while trying to cleanup chunks",
t);
} finally {
if (admin != null) {
admin.logout();
}
}
long end = System.currentTimeMillis();
log.info(
"ChunkCleanUpTask finished: Removed {} chunk upload(s) in {}ms ({} chunk upload(s) still active)",
new Object[] { numCleaned, (end - start), numLive });
}
/**
* Check if {@link Node} is eligible of
* {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} cleanup. To be
* eligible the age of last
* {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} uploaded should be
* greater than @link {@link #chunkCleanUpAge}
*
* @param node {@link Node} containing
* {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE}
* {@link Node}s
* @return true if eligible else false.
* @throws RepositoryException
*/
private boolean isEligibleForCleanUp(Node node) throws RepositoryException {
Node lastChunkNode = uploadhandler.getLastChunk(node);
return lastChunkNode != null
&& (System.currentTimeMillis() - lastChunkNode.getProperty(
javax.jcr.Property.JCR_CREATED).getDate().getTimeInMillis()) > chunkCleanUpAge;
}
@Activate
protected void activate(final Config configuration) {
chunkCleanUpAge = configuration.chunk_cleanup_age();
log.info("scheduler config [{}], chunkGarbageTime [{}] ms",
configuration.scheduler_expression(),
chunkCleanUpAge);
}
}