package org.geowebcache.diskquota;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.diskquota.storage.PageStatsPayload;
import org.geowebcache.diskquota.storage.TilePage;
import org.geowebcache.diskquota.storage.TilePageCalculator;
import org.geowebcache.diskquota.storage.TileSet;
import org.springframework.util.Assert;
/**
*
* @author groldan
*
*/
public class QueuedUsageStatsConsumer implements Callable<Long>, Serializable {
private static final Log log = LogFactory.getLog(QueuedUsageStatsConsumer.class);
private static final long serialVersionUID = -625181087112272266L;
/**
* Default number of milliseconds before cached/aggregated quota update is saved to the store
*/
private static final long DEFAULT_SYNC_TIMEOUT = 100;
/**
* Default number of per TileSet aggregated quota updates before ensuring they're synchronized
* back to the store, regardless of whether the timeout expired for the TileSet
*/
private static final int MAX_AGGREGATES_BEFORE_COMMIT = 3000;
private final QuotaStore quotaStore;
private final BlockingQueue<UsageStats> usageStatsQueue;
private final TilePageCalculator tilePageCalculator;
private final TimedUsageUpdate aggregatedPendingUpdates;
/**
*
* @author groldan
*
*/
private static class TimedUsageUpdate {
/**
* Tracks aggregated usage stats per {@link TilePage#getId() pageId} until committed
*/
private final Map<String, PageStatsPayload> pages;
/**
* tracks the last time the aggregated updates for a given tile set were committed
*/
private long lastCommitTime;
/**
* tracks how many requests for the same tile page this aggregated stats is made of
*/
private int numAggregations;
public TimedUsageUpdate() {
this.pages = new HashMap<String, PageStatsPayload>();
this.lastCommitTime = System.currentTimeMillis();
numAggregations = 0;
}
}
/**
*
* @param quotaStore
* @param queue
*/
public QueuedUsageStatsConsumer(final QuotaStore quotaStore,
final BlockingQueue<UsageStats> queue, final TilePageCalculator tilePageCalculator) {
Assert.notNull(quotaStore, "quotaStore can't be null");
Assert.notNull(queue, "queue can't be null");
Assert.notNull(tilePageCalculator, "tilePageCalculator can't be null");
this.quotaStore = quotaStore;
this.usageStatsQueue = queue;
this.tilePageCalculator = tilePageCalculator;
aggregatedPendingUpdates = new TimedUsageUpdate();
}
/**
* @see java.util.concurrent.Callable#call()
*/
public Long call() {
while (true) {
if (Thread.interrupted()) {
log.debug("Job " + getClass().getSimpleName()
+ " finished due to interrupted thread.");
break;
}
if(terminate) {
log.debug("Exiting on explicit termination request: " + getClass().getSimpleName());
break;
}
try {
/*
* do not wait for more than 5 seconds for data to become available on the queue
*/
UsageStats requestedTile;
requestedTile = usageStatsQueue.poll(DEFAULT_SYNC_TIMEOUT, TimeUnit.MILLISECONDS);
if (requestedTile == null) {
/*
* poll timed out, nothing new, check there are no pending aggregated updates
* for too long if we're idle
*/
if (aggregatedPendingUpdates.pages.size() > 0) {
commit();
}
} else {
/*
* perform an aggregated update in case we're really busy
*/
performAggregatedUpdate(requestedTile);
}
} catch (InterruptedException e) {
log.info("Shutting down quota update background task due to interrupted exception");
break;
// it doesn't matter
} catch (RuntimeException e) {
// we're running as a single task on a single thread... we need to be really sure if
// we should terminate... think how to handle recovery if at all
e.printStackTrace();
// throw e;
}
}
return null;
}
private final int[] pageIndexTarget = new int[3];
private final StringBuilder pageIdTarget = new StringBuilder(128);
private boolean terminate = false;
/**
*
* @param requestedTile
* represents a single tile that was requested and for which its tile page needs to
* be looked up and updated
* @throws InterruptedException
*/
private void performAggregatedUpdate(final UsageStats requestedTile)
throws InterruptedException {
final TileSet tileSet = requestedTile.getTileSet();
final String tileSetId = tileSet.getId();
final long[] tileIndex = requestedTile.getTileIndex();
tilePageCalculator.pageIndexForTile(tileSet, tileIndex, pageIndexTarget);
final int pageX = pageIndexTarget[0];
final int pageY = pageIndexTarget[1];
final byte pageZ = (byte) pageIndexTarget[2];
pageIdTarget.setLength(0);
TilePage.computeId(tileSetId, pageX, pageY, pageZ, pageIdTarget);
final String pageKeyForTile = pageIdTarget.toString();
PageStatsPayload timedUpdate = aggregatedPendingUpdates.pages.get(pageKeyForTile);
if (timedUpdate == null) {
/*
* it is the first one for this tile set, lets start the aggregated updates on it
*/
timedUpdate = new PageStatsPayload(new TilePage(tileSetId, pageX, pageY, pageZ));
timedUpdate.setTileSet(tileSet);
aggregatedPendingUpdates.pages.put(pageKeyForTile, timedUpdate);
} else {
timedUpdate.setNumHits(timedUpdate.getNumHits() + 1);
}
timedUpdate.setNumHits(timedUpdate.getNumHits() + 1);
timedUpdate.setLastAccessTime(System.currentTimeMillis());
aggregatedPendingUpdates.numAggregations++;
/*
* now make sure we're not waiting for too long before committing
*/
checkAggregatedTimeout();
}
/**
* Makes sure the given cached updates are held for too long before synchronizing with the
* store, either because it's been held for too long, or because too many updates have happened
* on it since the last time it was saved to the store.
*/
private void checkAggregatedTimeout() {
final long currTime = System.currentTimeMillis();
final long creationTime = aggregatedPendingUpdates.lastCommitTime;
boolean timeout = currTime - creationTime >= DEFAULT_SYNC_TIMEOUT;
final int numAggregations = aggregatedPendingUpdates.numAggregations;
boolean tooManyPendingCommits = numAggregations >= MAX_AGGREGATES_BEFORE_COMMIT;
if (timeout || tooManyPendingCommits) {
if (log.isTraceEnabled()) {
log.trace("Committing "
+ numAggregations
+ " aggregated usage stats to quota store due to "
+ (tooManyPendingCommits ? "too many pending commits"
: "max wait time reached"));
}
commit();
}
}
private void commit() {
Collection<PageStatsPayload> pendingCommits;
pendingCommits = new ArrayList<PageStatsPayload>(aggregatedPendingUpdates.pages.values());
quotaStore.addHitsAndSetAccesTime(pendingCommits);
aggregatedPendingUpdates.lastCommitTime = System.currentTimeMillis();
aggregatedPendingUpdates.numAggregations = 0;
aggregatedPendingUpdates.pages.clear();
}
public void shutdown() {
this.terminate = true;
}
}