| /* |
| * 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 java.util.Calendar; |
| import java.util.Iterator; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| 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.api.resource.ResourceResolverFactory; |
| 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 ResourceResolverFactory rrFactory; |
| |
| 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(Resource)} |
| * for cleanup. It queries all |
| * {@link SlingPostConstants#NT_SLING_CHUNK_MIXIN} nodes and filter nodes |
| * which are {@link #isEligibleForCleanUp(Resource)} for cleanup. It then |
| * deletes old chunks upload. |
| */ |
| private void cleanup() { |
| |
| long start = System.currentTimeMillis(); |
| |
| int numCleaned = 0; |
| int numLive = 0; |
| |
| ResourceResolver admin = null; |
| try { |
| admin = rrFactory.getAdministrativeResourceResolver(null); |
| |
| final Iterator<Resource> rsrcIter = admin.findResources( |
| "SELECT * FROM [sling:chunks] ", "sql"); |
| while (rsrcIter.hasNext()) { |
| final Resource rsrc = rsrcIter.next(); |
| if (isEligibleForCleanUp(rsrc)) { |
| numCleaned++; |
| uploadhandler.deleteChunks(rsrc); |
| } else { |
| numLive++; |
| } |
| } |
| if (admin.hasChanges()) { |
| try { |
| admin.refresh(); |
| admin.commit(); |
| } catch (PersistenceException 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.close(); |
| } |
| } |
| 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 Resource} 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 rsrc {@link Resource} containing |
| * {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} |
| * {@link Resource}s |
| * @return true if eligible else false. |
| */ |
| private boolean isEligibleForCleanUp(Resource rsrc) { |
| boolean result = false; |
| final Resource lastChunkNode = uploadhandler.getLastChunk(rsrc); |
| if ( lastChunkNode != null ) { |
| final Calendar created = lastChunkNode.getValueMap().get(JcrConstants.JCR_CREATED, Calendar.class); |
| if ( created != null && System.currentTimeMillis() - created.getTimeInMillis() > chunkCleanUpAge ) { |
| result = true; |
| } |
| } |
| return result; |
| } |
| |
| @Activate |
| protected void activate(final Config configuration) { |
| chunkCleanUpAge = configuration.chunk_cleanup_age(); |
| log.info("scheduler config [{}], chunkGarbageTime [{}] ms", |
| configuration.scheduler_expression(), |
| chunkCleanUpAge); |
| |
| } |
| } |