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.quota; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.concurrent.atomic.AtomicLong; import org.alfresco.repo.content.caching.ContentCacheImpl; import org.alfresco.repo.content.caching.cleanup.CachedContentCleaner; import org.alfresco.repo.content.filestore.FileContentReader; import org.alfresco.repo.content.filestore.FileContentWriter; import org.alfresco.util.PropertyCheck; 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; /** * Quota manager for the CachingContentStore that has the following characteristics: * <p> * When a cache file has been written that results in cleanThresholdPct (default 80%) of maxUsageBytes * being exceeded then the cached content cleaner is invoked (if not already running) in a new thread. * <p> * When the CachingContentStore is about to write a cache file but the disk usage is in excess of panicThresholdPct * (default 90%) then the cache file is not written and the cleaner is started (if not already running) in a new thread. * <p> * This quota manager works in conjunction with the cleaner to update disk usage levels in memory. When the quota * manager shuts down the current disk usage is saved to disk in {ContentCacheImpl.cacheRoot}/cache-usage.ser * <p> * Upon startup, if the cache-usage.ser file exists then the current usage is seeded with that value and the cleaner * is invoked in a new thread so that the value can be updated more accurately (perhaps some files were deleted * manually after shutdown for example). * * @author Matt Ward */ public class StandardQuotaStrategy implements QuotaManagerStrategy, UsageTracker { private static final String CACHE_USAGE_FILENAME = "cache-usage.txt"; private final static Log log = LogFactory.getLog(StandardQuotaStrategy.class); private static final long DEFAULT_DISK_USAGE_ESTIMATE = 0L; private int panicThresholdPct = 90; private int cleanThresholdPct = 80; private int targetUsagePct = 70; private long maxUsageBytes = 0; /* Threshold in seconds indicating a minimal gap between normal cleanup starts */ private long normalCleanThresholdSec = 0; private AtomicLong currentUsageBytes = new AtomicLong(0); private AtomicLong lastCleanupStart = new AtomicLong(0); private CachedContentCleaner cleaner; private ContentCacheImpl cache; // impl specific functionality required private int maxFileSizeMB = 0; /** * Lifecycle method. Should be called immediately after constructing objects of this type (e.g. by the * Spring framework's application context). */ public void init() { if (log.isDebugEnabled()) { log.debug("Starting quota strategy."); } PropertyCheck.mandatory(this, "cleaner", cleaner); PropertyCheck.mandatory(this, "cache", cache); if (maxUsageBytes < (10 * FileUtils.ONE_MB)) { if (log.isWarnEnabled()) { log.warn("Low maxUsageBytes of " + maxUsageBytes + "bytes - did you mean to specify in MB?"); } } loadDiskUsage(); // Set the time to start the normal clean lastCleanupStart.set(System.currentTimeMillis() - normalCleanThresholdSec); // Run the cleaner thread so that it can update the disk usage more accurately. signalCleanerStart("quota (init)"); } /** * Lifecycle method. Should be called when finished using an object of this type and before the application * container is shutdown (e.g. using a Spring framework destroy method). */ public void shutdown() { if (log.isDebugEnabled()) { log.debug("Shutting down quota strategy."); } saveDiskUsage(); } private void loadDiskUsage() { File usageFile = new File(cache.getCacheRoot(), CACHE_USAGE_FILENAME); if (!usageFile.exists()) { setCurrentUsageBytes(DEFAULT_DISK_USAGE_ESTIMATE); if (log.isInfoEnabled()) { log.info("No previous usage file found (" + usageFile + ") so assuming: " + getCurrentUsageBytes() + " bytes."); } } else { FileContentReader reader = new FileContentReader(usageFile); String usageStr = reader.getContentString(); long usage = Long.parseLong(usageStr); currentUsageBytes.set(usage); if (log.isInfoEnabled()) { log.info("Using last known disk usage estimate: " + getCurrentUsageBytes()); } } } private void saveDiskUsage() { File usageFile = new File(cache.getCacheRoot(), CACHE_USAGE_FILENAME); FileContentWriter writer = new FileContentWriter(usageFile); writer.putContent(currentUsageBytes.toString()); } @Override public boolean beforeWritingCacheFile(long contentSizeBytes) { long maxFileSizeBytes = getMaxFileSizeBytes(); if (maxFileSizeBytes > 0 && contentSizeBytes > maxFileSizeBytes) { if (log.isDebugEnabled()) { log.debug("File too large (" + contentSizeBytes + " bytes, max allowed is " + getMaxFileSizeBytes() + ") - vetoing disk write."); } return false; } else if (usageWillReach(panicThresholdPct, contentSizeBytes)) { if (log.isDebugEnabled()) { log.debug("Panic threshold reached (" + panicThresholdPct + "%) - vetoing disk write and starting cached content cleaner."); } signalCleanerStart("quota (panic threshold)"); return false; } return true; } @Override public boolean afterWritingCacheFile(long contentSizeBytes) { boolean keepNewFile = true; long maxFileSizeBytes = getMaxFileSizeBytes(); if (maxFileSizeBytes > 0 && contentSizeBytes > maxFileSizeBytes) { keepNewFile = false; } else { // The file has just been written so update the usage stats. addUsageBytes(contentSizeBytes); } if (getCurrentUsageBytes() >= maxUsageBytes) { // Reached quota limit - time to aggressively recover some space to make sure that // new requests to cache a file are likely to be honoured. if (log.isDebugEnabled()) { log.debug("Usage has reached or exceeded quota limit, limit: " + maxUsageBytes + " bytes, current usage: " + getCurrentUsageBytes() + " bytes."); } signalAggressiveCleanerStart("quota (limit reached)"); } else if (usageHasReached(cleanThresholdPct)) { // If usage has reached the clean threshold, start the cleaner if (log.isDebugEnabled()) { log.debug("Usage has reached " + cleanThresholdPct + "% - starting cached content cleaner."); } signalCleanerStart("quota (clean threshold)"); } return keepNewFile; } /** * Run the cleaner in a new thread. */ private void signalCleanerStart(final String reason, final boolean aggressive) { if (aggressive) { long targetReductionBytes = (long) (((double) targetUsagePct / 100) * maxUsageBytes); cleaner.executeAggressive(reason, targetReductionBytes); } else { long timePassedFromLastClean = System.currentTimeMillis() - lastCleanupStart.get(); if (timePassedFromLastClean < normalCleanThresholdSec * 1000) { if (log.isDebugEnabled()) { log.debug("Skipping a normal clean as it is too soon. The last cleanup was run " + timePassedFromLastClean / 1000f + " seconds ago."); } } else { lastCleanupStart.set(System.currentTimeMillis()); cleaner.execute(reason); } } } /** * Run a non-aggressive clean up job in a new thread. * * @param reason String */ private void signalCleanerStart(final String reason) { signalCleanerStart(reason, false); } /** * Run an aggressive clean up job in a new thread. * * @param reason String */ private void signalAggressiveCleanerStart(final String reason) { signalCleanerStart(reason, true); } /** * Will an increase in disk usage of <code>contentSize</code> bytes result in the specified * <code>threshold</code> (percentage of maximum allowed usage) being reached or exceeded? * * @param threshold int * @param contentSize long * @return true if additional content will reach <code>threshold</code>. */ private boolean usageWillReach(int threshold, long contentSize) { long potentialUsage = getCurrentUsageBytes() + contentSize; double pctOfMaxAllowed = ((double) potentialUsage / maxUsageBytes) * 100; return pctOfMaxAllowed >= threshold; } private boolean usageHasReached(int threshold) { return usageWillReach(threshold, 0); } public void setMaxUsageMB(long maxUsageMB) { setMaxUsageBytes(maxUsageMB * FileUtils.ONE_MB); } public void setMaxUsageBytes(long maxUsageBytes) { this.maxUsageBytes = maxUsageBytes; } public void setPanicThresholdPct(int panicThresholdPct) { this.panicThresholdPct = panicThresholdPct; } public void setCleanThresholdPct(int cleanThresholdPct) { this.cleanThresholdPct = cleanThresholdPct; } public void setTargetUsagePct(int targetUsagePct) { this.targetUsagePct = targetUsagePct; } public void setNormalCleanThresholdSec(long normalCleanThresholdSec) { this.normalCleanThresholdSec = normalCleanThresholdSec; } @Required public void setCache(ContentCacheImpl cache) { this.cache = cache; } @Required public void setCleaner(CachedContentCleaner cleaner) { this.cleaner = cleaner; } @Override public long getCurrentUsageBytes() { return currentUsageBytes.get(); } public double getCurrentUsageMB() { return (double) getCurrentUsageBytes() / FileUtils.ONE_MB; } public long getMaxUsageBytes() { return maxUsageBytes; } public long getMaxUsageMB() { return maxUsageBytes / FileUtils.ONE_MB; } public int getMaxFileSizeMB() { return this.maxFileSizeMB; } protected long getMaxFileSizeBytes() { return maxFileSizeMB * FileUtils.ONE_MB; } public void setMaxFileSizeMB(int maxFileSizeMB) { this.maxFileSizeMB = maxFileSizeMB; } @Override public long addUsageBytes(long sizeDelta) { long newUsage = currentUsageBytes.addAndGet(sizeDelta); if (log.isDebugEnabled()) { log.debug(String.format("Disk usage changed by %d to %d bytes", sizeDelta, newUsage)); } return newUsage; } @Override public void setCurrentUsageBytes(long newDiskUsage) { if (log.isInfoEnabled()) { log.info(String.format("Setting disk usage to %d bytes", newDiskUsage)); } currentUsageBytes.set(newDiskUsage); } }