org.geowebcache.diskquota.bdb.BDBQuotaStore.java Source code

Java tutorial

Introduction

Here is the source code for org.geowebcache.diskquota.bdb.BDBQuotaStore.java

Source

/**
 * 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
 */
package org.geowebcache.diskquota.bdb;

import static org.geowebcache.diskquota.DiskQuotaMonitor.GWC_DISKQUOTA_DISABLED;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.config.ConfigurationException;
import org.geowebcache.diskquota.QuotaStore;
import org.geowebcache.diskquota.storage.PageStats;
import org.geowebcache.diskquota.storage.PageStatsPayload;
import org.geowebcache.diskquota.storage.PageStoreConfig;
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.geowebcache.diskquota.storage.TileSetVisitor;
import org.geowebcache.storage.DefaultStorageFinder;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.util.Assert;

import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.Transaction;
import com.sleepycat.persist.EntityCursor;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;

public class BDBQuotaStore implements QuotaStore {

    private static final Log log = LogFactory.getLog(BDBQuotaStore.class);

    private static final String GLOBAL_QUOTA_NAME = "___GLOBAL_QUOTA___";

    public static final String STORE_VERSION = "1.1";

    private static final String VERSION_FILE = "version.txt";

    private EntityStore entityStore;

    private final String cacheRootDir;

    private final TilePageCalculator tilePageCalculator;

    private static ExecutorService transactionRunner;

    private PrimaryIndex<String, TileSet> tileSetById;

    private PrimaryIndex<Integer, Quota> usedQuotaById;

    private PrimaryIndex<Long, TilePage> pageById;

    private PrimaryIndex<Long, PageStats> pageStatsById;

    private SecondaryIndex<String, String, TileSet> tileSetsByLayer;

    private SecondaryIndex<String, Long, TilePage> pageByKey;

    private SecondaryIndex<String, Long, TilePage> pagesByTileSetId;

    private SecondaryIndex<Long, Long, PageStats> pageStatsByPageId;

    private SecondaryIndex<Float, Long, PageStats> pageStatsByLRU;

    private SecondaryIndex<Float, Long, PageStats> pageStatsByLFU;

    private SecondaryIndex<String, Integer, Quota> usedQuotaByTileSetId;

    private volatile boolean open;

    private boolean diskQuotaEnabled;

    public BDBQuotaStore(final DefaultStorageFinder cacheDirFinder, TilePageCalculator tilePageCalculator)
            throws ConfigurationException {

        Assert.notNull(cacheDirFinder, "cacheDirFinder can't be null");
        Assert.notNull(tilePageCalculator, "tilePageCalculator can't be null");

        this.tilePageCalculator = tilePageCalculator;
        this.cacheRootDir = cacheDirFinder.getDefaultPath();

        boolean disabled = Boolean.valueOf(cacheDirFinder.findEnvVar(GWC_DISKQUOTA_DISABLED)).booleanValue();
        if (disabled) {
            log.warn(" -- Found environment variable " + GWC_DISKQUOTA_DISABLED
                    + " set to true. DiskQuotaMonitor is disabled.");
        }
        this.diskQuotaEnabled = !disabled;
    }

    /**
     * @throws InterruptedException
     * @see {@link #close()}
     */
    public void startUp() throws InterruptedException, IOException {
        if (!diskQuotaEnabled) {
            log.info(getClass().getName() + " won't start, got env variable " + GWC_DISKQUOTA_DISABLED + "=true");
            return;
        }
        open = true;
        File storeDirectory = new File(cacheRootDir, "diskquota_page_store");
        storeDirectory.mkdirs();
        File version = new File(storeDirectory, VERSION_FILE);
        if (storeDirectory.list().length == 0) {
            // Directory is empty
            try {
                FileUtils.write(version, STORE_VERSION);
            } catch (IOException e) {
                throw new IOException("BDB DiskQuota could not write " + VERSION_FILE + " to new database", e);
            }
        } else {
            // Directory not empty
            try {
                String versionString = FileUtils.readFileToString(version);
                if (!versionString.equals(STORE_VERSION)) {
                    throw new IOException("BDB DiskQuota does not support database version " + versionString);
                }
            } catch (IOException e) {
                throw new IOException(
                        "BDB DiskQuota could not read " + VERSION_FILE + " to detemine database version", e);
            }
        }

        CustomizableThreadFactory tf = new CustomizableThreadFactory("GWC DiskQuota Store Writer-");
        transactionRunner = Executors.newFixedThreadPool(1, tf);
        try {
            configure(storeDirectory);

            deleteStaleLayersAndCreateMissingTileSets();

            log.info("Berkeley DB JE Disk Quota page store configured at " + storeDirectory.getAbsolutePath());
        } catch (RuntimeException e) {
            transactionRunner.shutdownNow();
            throw e;
        }
        log.info("Quota Store initialized. Global quota: " + getGloballyUsedQuota().toNiceString());
    }

    public void close() throws Exception {
        if (!diskQuotaEnabled) {
            return;
        }
        open = false;
        log.info("Requesting to close quota store...");
        transactionRunner.shutdown();
        try {
            transactionRunner.awaitTermination(30 * 1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ie) {
            log.error(
                    "Time out shutting down quota store write thread, trying to " + "close the entity store as is.",
                    ie);
        } finally {
            Environment environment = entityStore.getEnvironment();
            entityStore.close();
            environment.close();
        }
        log.info("Quota store closed.");
    }

    private void configure(final File storeDirectory) throws InterruptedException {
        // todo: make config persistent? or just rely on je.properties (I guess so)
        PageStoreConfig config = new PageStoreConfig();
        EntityStoreBuilder builder = new EntityStoreBuilder(config);
        EntityStore entityStore = builder.buildEntityStore(storeDirectory, null);
        this.entityStore = entityStore;

        tileSetById = entityStore.getPrimaryIndex(String.class, TileSet.class);
        pageById = entityStore.getPrimaryIndex(Long.class, TilePage.class);
        pageStatsById = entityStore.getPrimaryIndex(Long.class, PageStats.class);
        usedQuotaById = entityStore.getPrimaryIndex(Integer.class, Quota.class);

        pageByKey = entityStore.getSecondaryIndex(pageById, String.class, "page_key");
        pagesByTileSetId = entityStore.getSecondaryIndex(pageById, String.class, "tileset_id_fk");
        tileSetsByLayer = entityStore.getSecondaryIndex(tileSetById, String.class, "layer");
        pageStatsByLRU = entityStore.getSecondaryIndex(pageStatsById, Float.class, "LRU");
        pageStatsByLFU = entityStore.getSecondaryIndex(pageStatsById, Float.class, "LFU");
        usedQuotaByTileSetId = entityStore.getSecondaryIndex(usedQuotaById, String.class, "tileset_id");
        pageStatsByPageId = entityStore.getSecondaryIndex(pageStatsById, Long.class, "page_stats_by_page_id");

    }

    private class StartUpInitializer implements Callable<Void> {
        public Void call() throws Exception {
            final Transaction transaction = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                if (null == usedQuotaByTileSetId.get(transaction, GLOBAL_QUOTA_NAME, LockMode.DEFAULT)) {
                    log.debug("First time run: creating global quota object");
                    // need a global TileSet cause the Quota->TileSet relationship is enforced
                    TileSet globalTileSet = new TileSet(GLOBAL_QUOTA_NAME);
                    tileSetById.put(transaction, globalTileSet);

                    Quota globalQuota = new Quota();
                    globalQuota.setTileSetId(GLOBAL_QUOTA_NAME);
                    usedQuotaById.put(transaction, globalQuota);
                    log.debug("created Global Quota");
                }

                final Set<String> layerNames = tilePageCalculator.getLayerNames();
                final Set<String> existingLayers = new GetLayerNames().call();

                final Set<String> layersToDelete = new HashSet<String>(existingLayers);
                layersToDelete.removeAll(layerNames);

                for (String layerName : layersToDelete) {
                    log.info("Deleting disk quota information for layer '" + layerName
                            + "' as it does not exist anymore...");
                    // do not call issue since we're already running on the transaction thread here
                    try {
                        new DeleteLayer(layerName).call(transaction);
                    } catch (Exception e) {
                        log.warn("Error deleting disk quota information for layer '" + layerName + "'", e);
                    }
                }

                // add any missing tileset
                for (String layerName : layerNames) {
                    createLayer(layerName, transaction);
                }
                transaction.commit();
            } catch (RuntimeException e) {
                transaction.abort();
                throw e;
            }
            return null;
        }

    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#createLayer(java.lang.String)
     */
    public void createLayer(final String layerName) throws InterruptedException {
        issueSync(new Callable<Void>() {

            public Void call() throws Exception {
                final Transaction transaction = entityStore.getEnvironment().beginTransaction(null, null);
                try {
                    createLayer(layerName, transaction);
                    transaction.commit();
                } catch (RuntimeException e) {
                    transaction.abort();
                }
                return null;
            }
        });
    }

    private void createLayer(String layerName, final Transaction transaction) {
        Set<TileSet> layerTileSets = tilePageCalculator.getTileSetsFor(layerName);
        for (TileSet tset : layerTileSets) {
            getOrCreateTileSet(transaction, tset);
        }
    }

    private TileSet getOrCreateTileSet(final Transaction transaction, final TileSet tset) {
        String id = tset.getId();
        TileSet stored;
        if (null == (stored = tileSetById.get(transaction, id, LockMode.DEFAULT))) {
            log.debug("Creating TileSet for quota tracking: " + tset);
            tileSetById.putNoReturn(transaction, tset);
            stored = tset;
            Quota tileSetUsedQuota = new Quota();
            tileSetUsedQuota.setTileSetId(tset.getId());
            usedQuotaById.putNoReturn(transaction, tileSetUsedQuota);
        }
        return stored;
    }

    /**
     * Asynchronously issues the given {@code command} to the working transactional thread
     */
    private <E> Future<E> issue(final Callable<E> command) {
        if (!open) {
            throw new IllegalStateException("QuotaStore is closed.");
        }
        Future<E> future = transactionRunner.submit(command);
        return future;
    }

    /**
     * Synchronously issues the given {@code command} to the working transactional thread
     * 
     * @throws InterruptedException
     *             in case the calling thread was interrupted while waiting for the command to
     *             complete
     */
    private <E> E issueSync(final Callable<E> command) throws InterruptedException {
        Future<E> result = issue(command);
        try {
            return result.get();
        } catch (RuntimeException e) {
            throw e;
        } catch (InterruptedException e) {
            log.debug(
                    "Caught InterruptedException while waiting for command " + command.getClass().getSimpleName());
            throw e;
        } catch (ExecutionException e) {
            log.warn(e);
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            throw new RuntimeException(cause);
        }
    }

    private void deleteStaleLayersAndCreateMissingTileSets() throws InterruptedException {
        issueSync(new StartUpInitializer());
    }

    private class GetLayerNames implements Callable<Set<String>> {

        public Set<String> call() throws Exception {
            EntityCursor<String> layerNameCursor = tileSetsByLayer.keys(null, CursorConfig.DEFAULT);
            Set<String> names = new HashSet<String>();
            try {
                String name;
                while ((name = layerNameCursor.nextNoDup()) != null) {
                    if (!GLOBAL_QUOTA_NAME.equals(name)) {
                        names.add(name);
                    }
                }
            } finally {
                layerNameCursor.close();
            }
            return names;
        }

    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getGloballyUsedQuota()
     */
    public Quota getGloballyUsedQuota() throws InterruptedException {
        return getUsedQuotaByTileSetId(GLOBAL_QUOTA_NAME);
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getUsedQuotaByTileSetId(java.lang.String)
     */
    public Quota getUsedQuotaByTileSetId(final String tileSetId) throws InterruptedException {
        Quota usedQuota = issueSync(new UsedQuotaByTileSetId(tileSetId));
        return usedQuota;
    }

    private final class UsedQuotaByTileSetId implements Callable<Quota> {
        private final String tileSetId;

        private UsedQuotaByTileSetId(String tileSetId) {
            this.tileSetId = tileSetId;
        }

        public Quota call() throws Exception {
            Quota quota = usedQuotaByTileSetId.get(null, tileSetId, LockMode.READ_COMMITTED);
            if (quota == null) {
                quota = new Quota();
            }
            return quota;
        }
    }

    private class DeleteLayer implements Callable<Void> {

        private final String layerName;

        public DeleteLayer(String layerName) {
            this.layerName = layerName;
        }

        public Void call() throws Exception {
            Transaction transaction = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                call(transaction);
                transaction.commit();
            } catch (RuntimeException e) {
                transaction.abort();
                throw e;
            }
            return null;
        }

        public void call(Transaction transaction) {

            EntityCursor<TileSet> tileSets = tileSetsByLayer.entities(transaction, layerName, true, layerName, true,
                    null);
            try {
                TileSet tileSet;
                Quota freed;
                Quota global;
                while (null != (tileSet = tileSets.next())) {
                    freed = usedQuotaByTileSetId.get(transaction, tileSet.getId(), LockMode.DEFAULT);
                    global = usedQuotaByTileSetId.get(transaction, GLOBAL_QUOTA_NAME, LockMode.DEFAULT);

                    tileSets.delete();
                    global.subtract(freed.getBytes());
                    usedQuotaById.put(transaction, global);
                }
            } finally {
                tileSets.close();
            }
        }

    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#deleteLayer(java.lang.String)
     */
    public void deleteLayer(final String layerName) {
        Assert.notNull(layerName);
        issue(new DeleteLayer(layerName));
    }

    public void deleteGridSubset(String layerName, String gridSetId) {
        issue(new DeleteLayerGridSubset(layerName, gridSetId));
    }

    private class DeleteLayerGridSubset implements Callable<Void> {

        private final String layerName;

        private final String gridSetId;

        public DeleteLayerGridSubset(String layerName, String gridSetId) {
            this.layerName = layerName;
            this.gridSetId = gridSetId;
        }

        public Void call() {
            Transaction transaction = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                EntityCursor<TileSet> tileSets = tileSetsByLayer.entities(transaction, layerName, true, layerName,
                        true, null);
                TileSet tileSet;
                Quota freed;
                Quota global;
                try {
                    while (null != (tileSet = tileSets.next())) {
                        if (tileSet.getGridsetId().equals(gridSetId)) {
                            freed = usedQuotaByTileSetId.get(transaction, tileSet.getId(), LockMode.DEFAULT);
                            global = usedQuotaByTileSetId.get(transaction, GLOBAL_QUOTA_NAME, LockMode.DEFAULT);

                            tileSets.delete();
                            global.subtract(freed.getBytes());
                            usedQuotaById.put(transaction, global);
                        }
                    }
                } finally {
                    tileSets.close();
                }
                transaction.commit();
            } catch (RuntimeException e) {
                transaction.abort();
                throw e;
            }
            return null;
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#renameLayer(java.lang.String, java.lang.String)
     */
    public void renameLayer(String oldLayerName, String newLayerName) throws InterruptedException {
        Assert.notNull(oldLayerName);
        Assert.notNull(newLayerName);
        issueSync(new RenameLayer(oldLayerName, newLayerName));
    }

    private class RenameLayer implements Callable<Void> {

        private final String oldLayerName;

        private final String newLayerName;

        public RenameLayer(final String oldLayerName, final String newLayerName) {
            this.oldLayerName = oldLayerName;
            this.newLayerName = newLayerName;
        }

        /**
         * Copy over old {@link TileSet}s, used {@link Quota}s and {@link TilePage}s from
         * oldLayerName to newLayerName and delete the old ones
         * 
         * @see java.util.concurrent.Callable#call()
         */
        public Void call() throws Exception {
            Transaction transaction = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                copyTileSets(transaction);
                DeleteLayer deleteCommand = new DeleteLayer(oldLayerName);
                deleteCommand.call(transaction);
                transaction.commit();
            } catch (RuntimeException e) {
                transaction.abort();
                throw e;
            }
            return null;
        }

        private void copyTileSets(Transaction transaction) {
            EntityCursor<TileSet> tileSets = tileSetsByLayer.entities(transaction, oldLayerName, true, oldLayerName,
                    true, null);
            try {
                TileSet oldTileSet;
                TileSet newTileSet;
                Quota oldQuota;
                Quota newQuota;
                TilePage oldPage;
                TilePage newPage;
                while (null != (oldTileSet = tileSets.next())) {
                    final String gridsetId = oldTileSet.getGridsetId();
                    final String blobFormat = oldTileSet.getBlobFormat();
                    final String parametersId = oldTileSet.getParametersId();
                    newTileSet = new TileSet(newLayerName, gridsetId, blobFormat, parametersId);
                    // this creates the tileset's empty used Quota too
                    newTileSet = getOrCreateTileSet(transaction, newTileSet);

                    final String oldTileSetId = oldTileSet.getId();
                    final String newTileSetId = newTileSet.getId();

                    oldQuota = usedQuotaByTileSetId.get(transaction, oldTileSetId, LockMode.DEFAULT);
                    newQuota = usedQuotaByTileSetId.get(transaction, newTileSetId, LockMode.DEFAULT);
                    newQuota.setBytes(oldQuota.getBytes());
                    usedQuotaById.putNoReturn(transaction, newQuota);

                    EntityCursor<TilePage> oldPages = pagesByTileSetId.entities(transaction, oldTileSetId, true,
                            oldTileSetId, true, CursorConfig.DEFAULT);
                    try {
                        while (null != (oldPage = oldPages.next())) {
                            long oldPageId = oldPage.getId();
                            newPage = new TilePage(newTileSetId, oldPage.getPageX(), oldPage.getPageY(),
                                    oldPage.getZoomLevel());
                            pageById.put(transaction, newPage);
                            PageStats pageStats = pageStatsByPageId.get(oldPageId);
                            if (pageStats != null) {
                                pageStats.setPageId(newPage.getId());
                                pageStatsById.putNoReturn(transaction, pageStats);
                            }
                        }
                    } finally {
                        oldPages.close();
                    }
                }
            } finally {
                tileSets.close();
            }
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getUsedQuotaByLayerName(java.lang.String)
     */
    public Quota getUsedQuotaByLayerName(final String layerName) throws InterruptedException {
        return issueSync(new UsedQuotaByLayerName(layerName));
    }

    private final class UsedQuotaByLayerName implements Callable<Quota> {
        private final String layerName;

        public UsedQuotaByLayerName(final String layerName) {
            this.layerName = layerName;
        }

        public Quota call() throws Exception {
            Quota aggregated = null;

            EntityCursor<TileSet> layerTileSetsIds;
            layerTileSetsIds = tileSetsByLayer.entities(null, layerName, true, layerName, true,
                    CursorConfig.DEFAULT);
            TileSet tileSet;
            try {
                Quota tileSetUsedQuota;
                while (null != (tileSet = layerTileSetsIds.next())) {
                    if (aggregated == null) {
                        aggregated = new Quota();
                    }
                    tileSetUsedQuota = new UsedQuotaByTileSetId(tileSet.getId()).call();
                    aggregated.add(tileSetUsedQuota);
                }
            } finally {
                layerTileSetsIds.close();
            }
            if (aggregated == null) {
                aggregated = new Quota();
            }

            return aggregated;
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getTilesForPage(org.geowebcache.diskquota.storage.TilePage)
     */
    public long[][] getTilesForPage(TilePage page) throws InterruptedException {
        TileSet tileSet = getTileSetById(page.getTileSetId());
        long[][] gridCoverage = tilePageCalculator.toGridCoverage(tileSet, page);
        return gridCoverage;
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getTileSets()
     */
    public Set<TileSet> getTileSets() {
        Map<String, TileSet> map = new HashMap<String, TileSet>(tileSetById.map());
        map.remove(GLOBAL_QUOTA_NAME);
        HashSet<TileSet> hashSet = new HashSet<TileSet>(map.values());
        return hashSet;
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getTileSetById(java.lang.String)
     */
    public TileSet getTileSetById(final String tileSetId) throws InterruptedException {
        return issueSync(new Callable<TileSet>() {

            public TileSet call() throws Exception {
                TileSet tileSet = tileSetById.get(tileSetId);
                if (tileSet == null) {
                    throw new IllegalArgumentException("TileSet does not exist: " + tileSetId);
                }
                return tileSet;
            }
        });
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#accept(org.geowebcache.diskquota.storage.TileSetVisitor)
     */
    public void accept(TileSetVisitor visitor) {
        EntityCursor<TileSet> cursor = this.tileSetById.entities();
        try {
            TileSet tileSet;
            while ((tileSet = cursor.next()) != null) {
                visitor.visit(tileSet, this);
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getTilePageCalculator()
     */
    public TilePageCalculator getTilePageCalculator() {
        return tilePageCalculator;
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#addToQuotaAndTileCounts(org.geowebcache.diskquota.storage.TileSet,
     *      org.geowebcache.diskquota.storage.Quota, java.util.Collection)
     */
    public void addToQuotaAndTileCounts(final TileSet tileSet, final Quota quotaDiff,
            final Collection<PageStatsPayload> tileCountDiffs) throws InterruptedException {
        issueSync(new AddToQuotaAndTileCounts(tileSet, quotaDiff, tileCountDiffs));
    }

    private class AddToQuotaAndTileCounts implements Callable<Void> {

        private final TileSet tileSet;

        private final Collection<PageStatsPayload> tileCountDiffs;

        private final Quota quotaDiff;

        public AddToQuotaAndTileCounts(final TileSet tileSet, Quota quotaDiff,
                final Collection<PageStatsPayload> tileCountDiffs) {
            this.tileSet = tileSet;
            this.quotaDiff = quotaDiff;
            this.tileCountDiffs = tileCountDiffs;
        }

        public Void call() throws Exception {
            final Transaction tx = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                TileSet storedTileset = getOrCreateTileSet(tx, tileSet);
                // increase the tileset used quota
                addToUsedQuota(tx, storedTileset, quotaDiff);

                // and each page's fillFactor for lru/lfu expiration
                if (tileCountDiffs.size() > 0) {
                    TilePage page;
                    String pageKey;
                    for (PageStatsPayload payload : tileCountDiffs) {
                        page = payload.getPage();
                        pageKey = page.getKey();
                        PageStats pageStats;

                        TilePage storedPage = pageByKey.get(tx, pageKey, LockMode.DEFAULT);
                        if (null == storedPage) {
                            pageById.put(tx, page);
                            storedPage = page;
                            pageStats = new PageStats(storedPage.getId());
                            // pageStatsById.put(tx, pageStats);
                        } else {
                            pageStats = pageStatsByPageId.get(tx, storedPage.getId(), null);
                        }

                        final byte level = page.getZoomLevel();
                        final BigInteger tilesPerPage = tilePageCalculator.getTilesPerPage(tileSet, level);
                        final int tilesAdded = payload.getNumTiles();

                        pageStats.addTiles(tilesAdded, tilesPerPage);
                        pageStatsById.putNoReturn(tx, pageStats);
                    }
                }
                tx.commit();
                return null;
            } catch (RuntimeException e) {
                e.printStackTrace();
                tx.abort();
                throw e;
            }
        }

        private void addToUsedQuota(final Transaction tx, final TileSet tileSet, final Quota quotaDiff) {
            Quota usedQuota = usedQuotaByTileSetId.get(tx, tileSet.getId(), LockMode.DEFAULT);
            Quota globalQuota = usedQuotaByTileSetId.get(tx, GLOBAL_QUOTA_NAME, LockMode.DEFAULT);

            usedQuota.add(quotaDiff);
            globalQuota.add(quotaDiff);

            usedQuotaById.putNoReturn(tx, usedQuota);
            usedQuotaById.putNoReturn(tx, globalQuota);
        }

    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#addHitsAndSetAccesTime(java.util.Collection)
     */
    public Future<List<PageStats>> addHitsAndSetAccesTime(final Collection<PageStatsPayload> statsUpdates) {

        Assert.notNull(statsUpdates);

        return issue(new AddHitsAndSetAccesTime(statsUpdates));
    }

    /**
     * 
     * 
     */
    private class AddHitsAndSetAccesTime implements Callable<List<PageStats>> {

        private final Collection<PageStatsPayload> statsUpdates;

        public AddHitsAndSetAccesTime(Collection<PageStatsPayload> statsUpdates) {
            this.statsUpdates = statsUpdates;
        }

        public List<PageStats> call() throws Exception {
            List<PageStats> allStats = new ArrayList<PageStats>(statsUpdates.size());
            PageStats pageStats = null;
            final Transaction tx = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                for (PageStatsPayload payload : statsUpdates) {
                    TilePage page = payload.getPage();
                    TileSet storedTileset = tileSetById.get(tx, page.getTileSetId(), LockMode.DEFAULT);
                    if (null == storedTileset) {
                        log.info("Can't add usage stats. TileSet does not exist. Was it deleted? "
                                + page.getTileSetId());
                        continue;
                    }

                    TilePage storedPage = pageByKey.get(tx, page.getKey(), null);

                    if (storedPage == null) {
                        pageById.put(tx, page);
                        storedPage = page;
                        pageStats = new PageStats(storedPage.getId());
                    } else {
                        pageStats = pageStatsByPageId.get(tx, storedPage.getId(), null);
                    }

                    final int addedHits = payload.getNumHits();
                    final int lastAccessTimeMinutes = (int) (payload.getLastAccessTime() / 1000 / 60);
                    final int creationTimeMinutes = storedPage.getCreationTimeMinutes();
                    pageStats.addHitsAndAccessTime(addedHits, lastAccessTimeMinutes, creationTimeMinutes);
                    pageStatsById.putNoReturn(tx, pageStats);
                    allStats.add(pageStats);
                }
                tx.commit();
                return allStats;
            } catch (RuntimeException e) {
                tx.abort();
                throw e;
            }
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getLeastFrequentlyUsedPage(java.util.Set)
     */
    public TilePage getLeastFrequentlyUsedPage(final Set<String> layerNames) throws InterruptedException {

        SecondaryIndex<Float, Long, PageStats> expirationPolicyIndex = pageStatsByLFU;
        TilePage nextToExpire = issueSync(new FindPageToExpireByLayer(expirationPolicyIndex, layerNames));

        return nextToExpire;
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#getLeastRecentlyUsedPage(java.util.Set)
     */
    public TilePage getLeastRecentlyUsedPage(final Set<String> layerNames) throws InterruptedException {
        SecondaryIndex<Float, Long, PageStats> expirationPolicyIndex = pageStatsByLRU;
        TilePage nextToExpire = issueSync(new FindPageToExpireByLayer(expirationPolicyIndex, layerNames));

        return nextToExpire;
    }

    /**
     * @param expirationPolicyIndex
     * @param layerNames
     * @return
     */
    private class FindPageToExpireByLayer implements Callable<TilePage> {
        private final SecondaryIndex<Float, Long, PageStats> expirationPolicyIndex;

        private final Set<String> layerNames;

        public FindPageToExpireByLayer(SecondaryIndex<Float, Long, PageStats> expirationPolicyIndex,
                Set<String> layerNames) {
            this.expirationPolicyIndex = expirationPolicyIndex;
            this.layerNames = layerNames;
        }

        public TilePage call() throws Exception {

            // find out the tilesets for the requested layers
            final Set<String> tileSetIds = new HashSet<String>();
            for (String layerName : layerNames) {
                EntityCursor<TileSet> keys = tileSetsByLayer.entities(layerName, true, layerName, true);
                try {
                    TileSet tileSet;
                    while ((tileSet = keys.next()) != null) {
                        tileSetIds.add(tileSet.getId());
                    }
                } finally {
                    keys.close();
                }
            }

            TilePage nextToExpire = null;
            // find out the LRU page that matches a requested tileset
            final EntityCursor<PageStats> pageStatsCursor = expirationPolicyIndex.entities();

            try {
                String tileSetId;
                long pageId;
                PageStats pageStats;
                while ((pageStats = pageStatsCursor.next()) != null) {
                    if (pageStats.getFillFactor() > 0) {
                        pageId = pageStats.getPageId();
                        TilePage tilePage = pageById.get(pageId);
                        tileSetId = tilePage.getTileSetId();
                        if (tileSetIds.contains(tileSetId)) {
                            nextToExpire = tilePage;
                            break;
                        }
                    }
                }
            } finally {
                pageStatsCursor.close();
            }

            return nextToExpire;
        }
    }

    /**
     * @see org.geowebcache.diskquota.QuotaStore#setTruncated(org.geowebcache.diskquota.storage.TilePage)
     */
    public PageStats setTruncated(final TilePage tilePage) throws InterruptedException {
        return issueSync(new TruncatePage(tilePage));
    }

    private class TruncatePage implements Callable<PageStats> {
        private final TilePage tilePage;

        public TruncatePage(TilePage tilePage) {
            this.tilePage = tilePage;
        }

        public PageStats call() throws Exception {
            Transaction tx = entityStore.getEnvironment().beginTransaction(null, null);
            try {
                PageStats pageStats = pageStatsByPageId.get(tx, tilePage.getId(), null);
                if (pageStats != null) {
                    pageStats.setFillFactor(0f);
                    pageStatsById.putNoReturn(tx, pageStats);
                }
                tx.commit();
                return pageStats;
            } catch (Exception e) {
                tx.abort();
                throw e;
            }
        }
    }

}