/**
* This program 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Gabriel Roldan (OpenGeo) 2010
*
*/
package org.geowebcache.diskquota;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.diskquota.CacheCleaner.QuotaResolver;
import org.geowebcache.diskquota.storage.LayerQuota;
import org.geowebcache.diskquota.storage.Quota;
class CacheCleanerTask implements Runnable {
static final Log log = LogFactory.getLog(CacheCleanerTask.class);
/**
* Maintains a set of per layer enforcement tasks, so that no enforcement task is spawn for a
* layer when one is still running.
*/
private final Map<String, Future<?>> perLayerRunningCleanUps;
/**
* Caches the currently running {@link GlobalQuotaEnforcementTask} so that not two are launched
* at the same time
*/
private Future<?> globalCleanUpTask;
private ExecutorService cleanUpExecutorService;
private final DiskQuotaMonitor monitor;
/**
* @param executor
* ExecutorService used to launch quota enforcement tasks
*/
public CacheCleanerTask(final DiskQuotaMonitor monitor, final ExecutorService executor) {
this.monitor = monitor;
this.cleanUpExecutorService = executor;
this.perLayerRunningCleanUps = new HashMap<String, Future<?>>();
}
/**
* Runs the cache enforcement tasks asynchronously using the {@link ExecutorService} provided in
* the constructor.
* <p>
* Exceptions are catched and logged, not propagated, in order to allow this runnable to be used
* as a timer so even if one run fails the next runs are still called
* </p>
* <p>
* The process submits one cache cleanup execution task per layer that exceeds it's configured
* quota, and a single global cache enforcement task for the layers that have no explicitly
* configured quota limit.
* </p>
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
innerRun();
} catch (InterruptedException e) {
log.info("CacheCleanerTask called for shut down", e);
e.printStackTrace();
} catch (Exception e) {
log.error("Error running cache diskquota enforcement task", e);
}
}
private void innerRun() throws InterruptedException {
// first, save the config to account for changes in used quotas
final DiskQuotaConfig quotaConfig = monitor.getConfig();
if (!quotaConfig.isEnabled()) {
log.trace("DiskQuota disabled, ignoring run...");
return;
}
quotaConfig.setLastCleanUpTime(new Date());
final Set<String> allLayerNames = monitor.getLayerNames();
final Set<String> configuredLayerNames = quotaConfig.layerNames();
final Set<String> globallyManagedLayerNames = new HashSet<String>(allLayerNames);
globallyManagedLayerNames.removeAll(configuredLayerNames);
for (String layerName : configuredLayerNames) {
if (monitor.isCacheInfoBuilderRunning(layerName)) {
if (log.isInfoEnabled()) {
log.info("Cache information is still being gathered for layer '" + layerName
+ "'. Skipping quota enforcement task for this layer.");
}
continue;
}
Future<?> runningCleanup = perLayerRunningCleanUps.get(layerName);
if (runningCleanup != null && !runningCleanup.isDone()) {
if (log.isDebugEnabled()) {
log.debug("Cache clean up task still running for layer '" + layerName
+ "'. Ignoring it for this run.");
}
continue;
}
final LayerQuota definedQuotaForLayer = quotaConfig.layerQuota(layerName);
final ExpirationPolicy policy = definedQuotaForLayer.getExpirationPolicyName();
final Quota quota = definedQuotaForLayer.getQuota();
final Quota usedQuota = monitor.getUsedQuotaByLayerName(layerName);
Quota excedent = usedQuota.difference(quota);
if (excedent.getBytes().compareTo(BigInteger.ZERO) > 0) {
if (log.isInfoEnabled()) {
log.info("Layer '" + layerName + "' exceeds its quota of "
+ quota.toNiceString() + " by " + excedent.toNiceString()
+ ". Currently used: " + usedQuota.toNiceString()
+ ". Clean up task will be performed using expiration policy " + policy);
}
Set<String> layerNames = Collections.singleton(layerName);
QuotaResolver quotaResolver;
quotaResolver = monitor.newLayerQuotaResolver(layerName);
LayerQuotaEnforcementTask task;
task = new LayerQuotaEnforcementTask(layerNames, quotaResolver, monitor);
Future<Object> future = this.cleanUpExecutorService.submit(task);
perLayerRunningCleanUps.put(layerName, future);
}
}
if (globallyManagedLayerNames.size() > 0) {
ExpirationPolicy globalExpirationPolicy = quotaConfig.getGlobalExpirationPolicyName();
if (globalExpirationPolicy == null) {
return;
}
final Quota globalQuota = quotaConfig.getGlobalQuota();
if (globalQuota == null) {
log.info("There's not a global disk quota configured. The following layers "
+ "will not be checked for excess of disk usage: "
+ globallyManagedLayerNames);
return;
}
if (globalCleanUpTask != null && !globalCleanUpTask.isDone()) {
log.debug("Global cache quota enforcement task still running, avoiding issueing a new one...");
return;
}
Quota globalUsedQuota = monitor.getGloballyUsedQuota();
Quota excedent = globalUsedQuota.difference(globalQuota);
if (excedent.getBytes().compareTo(BigInteger.ZERO) > 0) {
log.debug("Submitting global cache quota enforcement task");
LayerQuotaEnforcementTask task;
QuotaResolver quotaResolver = monitor.newGlobalQuotaResolver();
task = new LayerQuotaEnforcementTask(globallyManagedLayerNames, quotaResolver,
monitor);
this.globalCleanUpTask = this.cleanUpExecutorService.submit(task);
} else {
if (log.isTraceEnabled()) {
log.trace("Won't launch global quota enforcement task, "
+ globalUsedQuota.toNiceString() + " used out of "
+ globalQuota.toNiceString() + " configured for the whole cache size.");
}
}
}
}
/**
*
* @author Gabriel Roldan
*/
private static class LayerQuotaEnforcementTask implements Callable<Object> {
private final Set<String> layerNames;
private final QuotaResolver quotaResolver;
private final DiskQuotaMonitor monitor;
public LayerQuotaEnforcementTask(final Set<String> layerNames,
final QuotaResolver quotaResolver, final DiskQuotaMonitor monitor) {
this.layerNames = layerNames;
this.quotaResolver = quotaResolver;
this.monitor = monitor;
}
/**
* @see java.util.concurrent.Callable#call()
*/
public Object call() throws Exception {
try {
monitor.expireByLayerNames(layerNames, quotaResolver);
} catch (InterruptedException e) {
log.info("Layer quota enforcement task terminated prematurely");
return null;
} catch (Exception e) {
log.warn("Exception expiring tiles for " + layerNames, e);
throw e;
}
return null;
}
}
}