Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.content.caching.cleanup; import java.io.File; import java.util.Date; import org.alfresco.repo.content.caching.CacheFileProps; import org.alfresco.repo.content.caching.ContentCacheImpl; import org.alfresco.repo.content.caching.FileHandler; import org.alfresco.repo.content.caching.quota.UsageTracker; import org.alfresco.util.Deleter; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Required; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; /** * Cleans up redundant cache files from the cached content file store. Once references to cache files are * no longer in the in-memory cache, the binary content files can be removed. * * @author Matt Ward */ public class CachedContentCleaner extends Thread implements FileHandler, ApplicationEventPublisherAware { private static final Log log = LogFactory.getLog(CachedContentCleaner.class); private ContentCacheImpl cache; // impl specific functionality required private long minFileAgeMillis = 0; private Integer maxDeleteWatchCount = 1; private boolean running; private UsageTracker usageTracker; private long newDiskUsage; private long numFilesSeen; private long numFilesDeleted; private long sizeFilesDeleted; private long numFilesMarked; private Date timeStarted; private Date timeFinished; private ApplicationEventPublisher eventPublisher; private long targetReductionBytes; private String reasonMessage; public CachedContentCleaner() { setName(getClass().getSimpleName()); setDaemon(true); } /** * This method MUST be called after the cleaner has been fully constructed * to notify interested parties that the cleaner exists and to start the actual cleaner thread. */ public void init() { eventPublisher.publishEvent(new CachedContentCleanerCreatedEvent(this)); start(); } @Override public void run() { while (true) { doClean(); } } public synchronized void execute() { execute("none specified"); } public synchronized void executeAggressive(String reason, long targetReductionBytes) { this.targetReductionBytes = targetReductionBytes; execute(reason); } private void doClean() { synchronized (this) { try { wait(); } catch (InterruptedException error) { // Nothing to do. } } running = true; if (log.isInfoEnabled()) { log.info("Starting cleaner, reason: " + reasonMessage); } resetStats(); timeStarted = new Date(); cache.processFiles(this); timeFinished = new Date(); if (usageTracker != null) { usageTracker.setCurrentUsageBytes(newDiskUsage); } if (log.isInfoEnabled()) { log.info("Finished, duration: " + getDurationSeconds() + "s, seen: " + numFilesSeen + ", marked: " + numFilesMarked + ", deleted: " + numFilesDeleted + " (" + String.format("%.2f", getSizeFilesDeletedMB()) + "MB, " + sizeFilesDeleted + " bytes)" + ", target: " + targetReductionBytes + " bytes"); } this.targetReductionBytes = 0; running = false; synchronized (this) { notifyAll(); } } public synchronized void execute(String reasonMessage) { this.reasonMessage = reasonMessage; notifyAll(); } private void resetStats() { newDiskUsage = 0; numFilesSeen = 0; numFilesDeleted = 0; sizeFilesDeleted = 0; numFilesMarked = 0; } @Override public void handle(File cachedContentFile) { if (log.isDebugEnabled()) { log.debug("handle file: " + cachedContentFile + " (target reduction: " + targetReductionBytes + " bytes)"); } numFilesSeen++; CacheFileProps props = null; boolean deleted = false; if (targetReductionBytes > 0 && sizeFilesDeleted < targetReductionBytes) { if (log.isDebugEnabled()) { log.debug("Target reduction " + targetReductionBytes + " bytes not yet reached. Deleted so far: " + sizeFilesDeleted); } // Aggressive clean mode, delete file straight away. deleted = deleteFilesNow(cachedContentFile); } else { if (oldEnoughForCleanup(cachedContentFile)) { if (log.isDebugEnabled()) { log.debug("File is older than " + minFileAgeMillis + "ms - considering for cleanup: " + cachedContentFile); } props = new CacheFileProps(cachedContentFile); String url = cache.getContentUrl(cachedContentFile); if (url == null) { // Not in the cache, check the properties file props.load(); url = props.getContentUrl(); } if (url == null || !cache.contains(url)) { // If the url is null, it might still be in the cache, but we were unable to determine it // from the reverse lookup or the properties file. Delete the file as it is most likely orphaned. // If for some reason it is still in the cache, cache.getReader(url) must re-cache it. deleted = markOrDelete(cachedContentFile, props); } } else { if (log.isDebugEnabled()) { log.debug("File too young for cleanup - ignoring " + cachedContentFile); } } } if (!deleted) { if (props == null) { props = new CacheFileProps(cachedContentFile); } long size = cachedContentFile.length() + props.fileSize(); newDiskUsage += size; } } /** * Is the file old enough to be considered for cleanup/deletion? The file must be older than minFileAgeMillis * to be considered for deletion - the state of the cache and the file's associated properties file will not * be examined unless the file is old enough. * * @return true if the file is older than minFileAgeMillis, false otherwise. */ private boolean oldEnoughForCleanup(File file) { if (minFileAgeMillis == 0) { return true; } else { long now = System.currentTimeMillis(); return (file.lastModified() < (now - minFileAgeMillis)); } } /** * Marks a file for deletion by a future run of the CachedContentCleaner. Each time a file is observed * by the cleaner as being ready for deletion, the deleteWatchCount is incremented until it reaches * maxDeleteWatchCount - in which case the next run of cleaner will really delete it. * <p> * For maxDeleteWatchCount of 1 for example, the first cleaner run will mark the file for deletion and the second * run will really delete it. * <p> * This offers a degree of protection over the fairly unlikely event that a reader will be obtained for a file that * is in the cache but gets removed from the cache and is then deleted by the cleaner before * the reader was consumed. A maxDeleteWatchCount of 1 should normally be fine (recommended), whilst * 0 would result in immediate deletion the first time the cleaner sees it as a candidate * for deletion (not recommended). * * @param file File * @param props CacheFileProps * @return true if the content file was deleted, false otherwise. */ private boolean markOrDelete(File file, CacheFileProps props) { Integer deleteWatchCount = props.getDeleteWatchCount(); // Just in case the value has been corrupted somehow. if (deleteWatchCount < 0) deleteWatchCount = 0; boolean deleted = false; if (deleteWatchCount < maxDeleteWatchCount) { deleteWatchCount++; if (log.isDebugEnabled()) { log.debug("Marking file for deletion, deleteWatchCount=" + deleteWatchCount + ", file: " + file); } props.setDeleteWatchCount(deleteWatchCount); props.store(); numFilesMarked++; } else { if (log.isDebugEnabled()) { log.debug("Deleting cache file " + file); } deleted = deleteFilesNow(file); } return deleted; } /** * Deletes both the cached content file and its peer properties file that contains the * original content URL and deletion marker information. * * @param cacheFile Location of cached content file. * @return true if the content file was deleted, false otherwise. */ private boolean deleteFilesNow(File cacheFile) { CacheFileProps props = new CacheFileProps(cacheFile); props.delete(); long fileSize = cacheFile.length(); boolean deleted = cacheFile.delete(); if (deleted) { if (log.isTraceEnabled()) { log.trace("Deleted cache file: " + cacheFile); } numFilesDeleted++; sizeFilesDeleted += fileSize; Deleter.deleteEmptyParents(cacheFile, cache.getCacheRoot()); } else { if (log.isWarnEnabled()) { log.warn("Failed to delete cache file: " + cacheFile); } } return deleted; } @Required public void setCache(ContentCacheImpl cache) { this.cache = cache; } /** * Sets the minimum age of a cache file before it will be considered for deletion. * @see #oldEnoughForCleanup(File) * @param minFileAgeMillis long */ public void setMinFileAgeMillis(long minFileAgeMillis) { this.minFileAgeMillis = minFileAgeMillis; } /** * Sets the maxDeleteWatchCount value. * * @see #markOrDelete(File, CacheFileProps) * @param maxDeleteWatchCount Integer */ public void setMaxDeleteWatchCount(Integer maxDeleteWatchCount) { if (maxDeleteWatchCount < 0) { throw new IllegalArgumentException( "maxDeleteWatchCount cannot be negative [value=" + maxDeleteWatchCount + "]"); } this.maxDeleteWatchCount = maxDeleteWatchCount; } /** * @param usageTracker the usageTracker to set */ public void setUsageTracker(UsageTracker usageTracker) { this.usageTracker = usageTracker; } public boolean isRunning() { return running; } public long getNumFilesSeen() { return this.numFilesSeen; } public long getNumFilesDeleted() { return this.numFilesDeleted; } public long getSizeFilesDeleted() { return this.sizeFilesDeleted; } public double getSizeFilesDeletedMB() { return (double) getSizeFilesDeleted() / FileUtils.ONE_MB; } public long getNumFilesMarked() { return numFilesMarked; } public Date getTimeStarted() { return this.timeStarted; } public Date getTimeFinished() { return this.timeFinished; } public long getDurationSeconds() { return getDurationMillis() / 1000; } public long getDurationMillis() { return timeFinished.getTime() - timeStarted.getTime(); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } /** * Returns the cacheRoot that this cleaner is responsible for. * @return File */ public File getCacheRoot() { return cache.getCacheRoot(); } }