package org.geowebcache.diskquota;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
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.Quota;
import org.geowebcache.diskquota.storage.TilePage;
import org.geowebcache.diskquota.storage.TilePageCalculator;
import org.geowebcache.diskquota.storage.TileSet;
import org.springframework.util.Assert;
public class QueuedQuotaUpdatesConsumer implements Callable<Long>, Serializable {
private static final Log log = LogFactory.getLog(QueuedQuotaUpdatesConsumer.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 = 1000;
private final QuotaStore quotaStore;
private final TilePageCalculator tilePageCalculator;
private final BlockingQueue<QuotaUpdate> queue;
/**
* Tracks aggregated quota size diffs per TileSet until committed by
* {@link #commit(TimedQuotaUpdate)} as the result of {@link #checkAggregatedTimeouts()} or
* {@link #checkAggregatedTimeout(TimedQuotaUpdate)} at {@link #call()}
*/
private Map<TileSet, TimedQuotaUpdate> aggregatedDelayedUpdates;
boolean terminate = false;
/**
* Tracks accumulated quota difference for a single TileSet and accumulated number of tiles
* difference for pages in the same TileSet
*
* @author groldan
*
*/
private static class TimedQuotaUpdate {
private final TilePageCalculator tpc;
private final TileSet tileSet;
/**
* tracks the last time the aggregated updates for a given tile set were committed
*/
private final long creationTime;
/**
* tracks how many quota updates this aggregated value is made of
*/
private int numAggregations;
/**
* Tracks accumulated quota difference per TileSet
*/
private Quota accumQuotaDiff;
/**
* Tracks accumulated number of tiles per TilePage id
*/
private Map<String, PageStatsPayload> tilePages;
private StringBuilder pageIdTarget;
private int[] pageIndexTarget;
public TimedQuotaUpdate(TileSet tileSet, TilePageCalculator tpc) {
this.tileSet = tileSet;
this.tpc = tpc;
this.creationTime = System.currentTimeMillis();
tilePages = new HashMap<String, PageStatsPayload>();
pageIndexTarget = new int[3];
pageIdTarget = new StringBuilder(128);
accumQuotaDiff = new Quota();
}
public void add(QuotaUpdate quotaUpdate) {
final String tileSetId = tileSet.getId();
long size = quotaUpdate.getSize();
this.accumQuotaDiff.addBytes(quotaUpdate.getSize());
long[] tileIndex = quotaUpdate.getTileIndex();
tpc.pageIndexForTile(tileSet, tileIndex, pageIndexTarget);
int pageX = pageIndexTarget[0];
int pageY = pageIndexTarget[1];
byte pageZ = (byte) pageIndexTarget[2];
pageIdTarget.setLength(0);
TilePage.computeId(tileSetId, pageX, pageY, pageZ, pageIdTarget);
String pageIdForTile = pageIdTarget.toString();
final int tileCountDiff = size > 0 ? 1 : -1;
PageStatsPayload payload = tilePages.get(pageIdForTile);
if (payload == null) {
TilePage page;
page = new TilePage(tileSetId, pageX, pageY, pageZ);
payload = new PageStatsPayload(page);
tilePages.put(pageIdForTile, payload);
}
int previousCount = payload.getNumTiles();
payload.setNumTiles(previousCount + tileCountDiff);
++numAggregations;
}
public TileSet getTileSet() {
return tileSet;
}
public Quota getAccummulatedQuotaDifference() {
return accumQuotaDiff;
}
public Collection<PageStatsPayload> getAccummulatedTilePageCounts() {
return tilePages.values();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder('[');
sb.append(tileSet);
sb.append(numAggregations).append(" aggregated updates, ");
sb.append(tilePages.size()).append(" different pages, ");
sb.append("accum quota diff: ").append(accumQuotaDiff.toNiceString());
sb.append(", created ").append((System.currentTimeMillis() - creationTime))
.append("ms ago").append(']');
return sb.toString();
}
}
public QueuedQuotaUpdatesConsumer(QuotaStore quotaStore, BlockingQueue<QuotaUpdate> queue) {
Assert.notNull(quotaStore, "quotaStore can't be null");
Assert.notNull(queue, "queue can't be null");
this.quotaStore = quotaStore;
this.tilePageCalculator = quotaStore.getTilePageCalculator();
this.queue = queue;
aggregatedDelayedUpdates = new HashMap<TileSet, TimedQuotaUpdate>();
}
/**
* @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 DEFAULT_SYNC_TIMEOUT for data to become available on
* the queue
*/
QuotaUpdate updateData;
updateData = queue.poll(DEFAULT_SYNC_TIMEOUT, TimeUnit.MILLISECONDS);
if (updateData != null) {
/*
* or perform an aggregated update in case we're really busy
*/
performAggregatedUpdate(updateData);
}
/*
* and check there are no pending aggregated updates for too long if we're idle
*/
checkAggregatedTimeouts();
} catch (InterruptedException e) {
log.info("Shutting down quota update background task due to InterruptedException");
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;
}
/**
*
* @param updateData
* @throws InterruptedException
*/
private void performAggregatedUpdate(final QuotaUpdate updateData) throws InterruptedException {
final TileSet tileSet = updateData.getTileSet();
TimedQuotaUpdate accumulatedUpdate = aggregatedDelayedUpdates.get(tileSet);
if (accumulatedUpdate == null) {
/*
* it is the first one for this tile set, lets start the aggregated updates on it
*/
accumulatedUpdate = new TimedQuotaUpdate(tileSet, tilePageCalculator);
aggregatedDelayedUpdates.put(tileSet, accumulatedUpdate);
}
accumulatedUpdate.add(updateData);
}
/**
* Makes sure no cached updates are held for too long before synchronizing with the store
*
* @throws InterruptedException
*/
private void checkAggregatedTimeouts() throws InterruptedException {
if (aggregatedDelayedUpdates.size() == 0) {
return;
}
List<TileSet> pruneList = null;
for (TimedQuotaUpdate timedUpadte : aggregatedDelayedUpdates.values()) {
if (pruneList == null) {
pruneList = new ArrayList<TileSet>(2);
}
boolean prune = checkAggregatedTimeout(timedUpadte);
if (prune) {
pruneList.add(timedUpadte.getTileSet());
}
}
prune(pruneList);
}
private void prune(List<TileSet> pruneList) {
if (pruneList != null && pruneList.size() > 0) {
for (TileSet ts : pruneList) {
aggregatedDelayedUpdates.remove(ts);
}
}
}
/**
* Makes sure the given cached updates are not 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.
*
* @param timedUpadte
* @return {@code true} if it's ok to prune the timedUpdate from the
* {@link #aggregatedDelayedUpdates local cache}
* @throws InterruptedException
*/
private boolean checkAggregatedTimeout(TimedQuotaUpdate timedUpadte)
throws InterruptedException {
final long creationTime = timedUpadte.creationTime;
long timeSinceLastCommit = System.currentTimeMillis() - creationTime;
boolean timeout = timeSinceLastCommit >= DEFAULT_SYNC_TIMEOUT;
final int numAggregations = timedUpadte.numAggregations;
boolean tooManyPendingCommits = numAggregations >= MAX_AGGREGATES_BEFORE_COMMIT;
boolean canWaitABitLonger = timeSinceLastCommit < 2000
&& timedUpadte.tilePages.size() < 1000;
if (!canWaitABitLonger && (timeout || tooManyPendingCommits)) {
if (log.isDebugEnabled()) {
log.debug("Committing "
+ timedUpadte
+ " to quota store due to "
+ (tooManyPendingCommits ? "too many pending commits"
: "max wait time reached"));
}
commit(timedUpadte);
return true;
}
return false;
}
private void commit(final TimedQuotaUpdate aggregatedUpadte) throws InterruptedException {
final TileSet tileSet = aggregatedUpadte.getTileSet();
final Quota quotaDiff = aggregatedUpadte.getAccummulatedQuotaDifference();
Collection<PageStatsPayload> tileCountDiffs;
tileCountDiffs = new ArrayList<PageStatsPayload>(
aggregatedUpadte.getAccummulatedTilePageCounts());
if (quotaDiff.getBytes().compareTo(BigInteger.ZERO) == 0 && tileCountDiffs.size() == 0) {
return;
}
quotaStore.addToQuotaAndTileCounts(tileSet, quotaDiff, tileCountDiffs);
}
public void shutdown() {
this.terminate = true;
}
}