/** * 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 Andrea Aime - GeoSolutions */ package org.geowebcache.diskquota.jdbc; import java.io.Closeable; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; 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.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geowebcache.diskquota.QuotaStore; import org.geowebcache.diskquota.storage.PageStats; 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.geowebcache.diskquota.storage.TileSetVisitor; import org.geowebcache.storage.DefaultStorageFinder; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DeadlockLoserDataAccessException; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /** * An abstract quota store based on a JDBC reachable database, and configurable via a dialect class * * @author Andrea Aime - GeoSolutions */ public class JDBCQuotaStore implements QuotaStore { private static final Log log = LogFactory.getLog(JDBCQuotaStore.class); /** * The constant identifying the global quota tile set key */ public static final String GLOBAL_QUOTA_NAME = "___GLOBAL_QUOTA___"; /** * The dialect accounting for database specific differences */ SQLDialect dialect; /** * The template used to execute commands */ SimpleJdbcTemplate jt; /** * The template used to run transactions */ TransactionTemplate tt; /** * The database schema (optional) */ String schema; /** * The storage finder, used to locate the data directory */ DefaultStorageFinder finder; /** * The tile page calculator, serving as the source or layers, tile sets, tile pages */ TilePageCalculator calculator; /** * Max number of attempts we do to insert/update page stats in race-free mode */ int maxLoops = 100; /** * The executor used for asynch requests */ ExecutorService executor; private DataSource dataSource; public JDBCQuotaStore(DefaultStorageFinder finder, TilePageCalculator tilePageCalculator) { this.finder = finder; this.calculator = tilePageCalculator; this.executor = Executors.newFixedThreadPool(1); } /** * Gets the SQL dialect used by this quota store * * @return */ public SQLDialect getDialect() { return dialect; } /** * Returns the SQL dialect used by this quota store * * @return */ public void setDialect(SQLDialect dialect) { this.dialect = dialect; } /** * Returns he database schema used by this store * * @return */ public String getSchema() { return schema; } /** * Sets the database schema used by this store * * @param schema */ public void setSchema(String schema) { this.schema = schema; } /** * Sets the connection pool provider and initializes the tables in the dbms if missing */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; DataSourceTransactionManager dsTransactionManager = new DataSourceTransactionManager( dataSource); this.tt = new TransactionTemplate(dsTransactionManager); this.jt = new SimpleJdbcTemplate(dsTransactionManager.getDataSource()); } /** * Called to initialize the database structure and the layers */ public void initialize() { if (dialect == null || jt == null || tt == null) { throw new IllegalStateException("Please provide both the sql dialect and the data " + "source before calling inizialize"); } tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { // setup the tables if necessary dialect.initializeTables(schema, jt); // get the existing table names List<String> existingLayers = jt.query(dialect.getAllLayersQuery(schema), new RowMapper<String>() { public String mapRow(ResultSet rs, int rowNum) throws SQLException { return rs.getString(1); } }); // compare with the ones available in the config final Set<String> layerNames = calculator.getLayerNames(); final Set<String> layersToDelete = new HashSet<String>(existingLayers); layersToDelete.removeAll(layerNames); // remove all the layers we don't need for (String layerName : layersToDelete) { deleteLayer(layerName); } // add any missing tileset for (String layerName : layerNames) { createLayerInternal(layerName); } // create the global quota if necessary Quota global = getUsedQuotaByTileSetIdInternal(GLOBAL_QUOTA_NAME); if (global == null) { createLayerInternal(GLOBAL_QUOTA_NAME); } } }); } public void createLayer(String layerName) throws InterruptedException { createLayerInternal(layerName); } private void createLayerInternal(final String layerName) { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { Set<TileSet> layerTileSets; if (!GLOBAL_QUOTA_NAME.equals(layerName)) { layerTileSets = calculator.getTileSetsFor(layerName); } else { layerTileSets = Collections.singleton(new TileSet(GLOBAL_QUOTA_NAME)); } for (TileSet tset : layerTileSets) { // other nodes in the cluster might be trying to create the same layer, // so use getOrCreate getOrCreateTileSet(tset); } } }); } public Quota getGloballyUsedQuota() throws InterruptedException { return nonNullQuota(getUsedQuotaByTileSetIdInternal(GLOBAL_QUOTA_NAME)); } public Quota getUsedQuotaByTileSetId(String tileSetId) { return nonNullQuota(getUsedQuotaByTileSetIdInternal(tileSetId)); } public Quota getUsedQuotaByLayerName(String layerName) { String sql = dialect.getUsedQuotaByLayerName(schema, "layerName"); return nonNullQuota(jt.queryForOptionalObject(sql, new DiskQuotaMapper(), Collections.singletonMap("layerName", layerName))); } public Quota getUsedQuotaByGridsetid(String gridsetId) { String sql = dialect.getUsedQuotaByGridSetId(schema, "gridSetId"); return nonNullQuota(jt.queryForOptionalObject(sql, new DiskQuotaMapper(), Collections.singletonMap("gridSetId", gridsetId))); } /** * Utility method that retrieves the disk quota used by a layer gridset. */ public Quota getUsedQuotaByLayerGridset(String layerName, String gridsetId) { // getting the sql query for the current database String sql = dialect.getUsedQuotaByLayerGridset(schema, "layerName", "gridSetId"); // setup the parameters for the sql query Map<String, String> parameters = new HashMap<>(); parameters.put("layerName", layerName); parameters.put("gridSetId", gridsetId); // execute the sql query return nonNullQuota(jt.queryForOptionalObject(sql, new DiskQuotaMapper(), parameters)); } public Quota getUsedQuotaByParametersId(String parametersId) { String sql = dialect.getUsedQuotaByParametersId(schema, "parametersId"); return nonNullQuota(jt.queryForOptionalObject(sql, new DiskQuotaMapper(), Collections.singletonMap("parametersId", parametersId))); } protected Quota getUsedQuotaByTileSetIdInternal(final String tileSetId) { String sql = dialect.getUsedQuotaByTileSetId(schema, "key"); return jt.queryForOptionalObject(sql, new RowMapper<Quota>() { public Quota mapRow(ResultSet rs, int rowNum) throws SQLException { BigDecimal bytes = rs.getBigDecimal(1); Quota quota = new Quota(bytes.toBigInteger()); quota.setTileSetId(tileSetId); return quota; } }, Collections.singletonMap("key", tileSetId)); } /** * Return a empty quota object in case a null value is passed, otherwise return the passed value * @param optionalQuota * @return */ private Quota nonNullQuota(Quota optionalQuota) { if(optionalQuota == null) { return new Quota(); } else { return optionalQuota; } } public void deleteLayer(final String layerName) { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { deleteLayerInternal(layerName); } }); } public void deleteGridSubset(final String layerName, final String gridSetId) { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { // get the disk quota used by the layer gridset Quota quota = getUsedQuotaByLayerGridset(layerName, gridSetId); // we will subtracting the current disk quota value quota.setBytes(quota.getBytes().negate()); // update the global disk quota by subtracting the value above String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); Map<String, Object> params = new HashMap<>(); params.put("tileSetId", GLOBAL_QUOTA_NAME); params.put("bytes", new BigDecimal(quota.getBytes())); jt.update(updateQuota, params); // delete layer gridset String statement = dialect.getLayerGridDeletionStatement(schema, "layerName", "gridSetId"); params = new HashMap<String, Object>(); params.put("layerName", layerName); params.put("gridSetId", gridSetId); jt.update(statement, params); } }); } public void deleteLayerInternal(final String layerName) { getUsedQuotaByLayerName(layerName); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { // update the global quota Quota quota = getUsedQuotaByLayerName(layerName); quota.setBytes(quota.getBytes().negate()); String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); Map<String, Object> params = new HashMap<String, Object>(); params.put("tileSetId", GLOBAL_QUOTA_NAME); params.put("bytes", new BigDecimal(quota.getBytes())); jt.update(updateQuota, params); // delete the layer log.info("Deleting disk quota information for layer '" + layerName + "'"); String statement = dialect.getLayerDeletionStatement(schema, "layerName"); jt.update(statement, Collections.singletonMap("layerName", layerName)); } }); } public void renameLayer(final String oldLayerName, final String newLayerName) throws InterruptedException { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { String sql = dialect.getRenameLayerStatement(schema, "oldName", "newName"); Map<String, Object> params = new HashMap<String, Object>(); params.put("oldName", oldLayerName); params.put("newName", newLayerName); int updated = jt.update(sql, params); log.info("Updated " + updated + " tile sets after layer rename"); } }); } public Set<TileSet> getTileSets() { String getTileSet = dialect.getTileSetsQuery(schema); List<TileSet> tilesets = jt.query(getTileSet, new TileSetRowMapper()); // collect in a set and remove the global quota one Set<TileSet> result = new HashSet<TileSet>(); for (TileSet ts : tilesets) { if (!GLOBAL_QUOTA_NAME.equals(ts.getId())) { result.add(ts); } } return result; } public void accept(final TileSetVisitor visitor) { String getTileSet = dialect.getTileSetsQuery(schema); final TileSetRowMapper tileSetMapper = new TileSetRowMapper(); jt.query(getTileSet, new RowCallbackHandler() { public void processRow(ResultSet rs) throws SQLException { TileSet tileSet = tileSetMapper.mapRow(rs, 0); if (!GLOBAL_QUOTA_NAME.equals(tileSet.getId())) { visitor.visit(tileSet, JDBCQuotaStore.this); } } }); } public TileSet getTileSetById(String tileSetId) throws InterruptedException { // locate the tileset TileSet result = getTileSetByIdInternal(tileSetId); // make it compatible with BDB quota store behavior if (result == null) { throw new IllegalArgumentException("Could not find a tile set with id: " + tileSetId); } return result; } private TileSet getTileSetByIdInternal(String tileSetId) { String getTileSet = dialect.getTileSetQuery(schema, "key"); final TileSetRowMapper tileSetMapper = new TileSetRowMapper(); return jt.queryForOptionalObject(getTileSet, tileSetMapper, Collections.singletonMap("key", tileSetId)); } private boolean createTileSet(TileSet tset) { if (log.isDebugEnabled()) { log.debug("Creating tileset " + tset); } String createTileSet = dialect.getCreateTileSetQuery(schema, "key", "layerName", "gridSetId", "blobFormat", "parametersId"); Map<String, Object> params = new HashMap<String, Object>(); params.put("key", tset.getId()); params.put("layerName", tset.getLayerName()); params.put("gridSetId", tset.getGridsetId()); params.put("blobFormat", tset.getBlobFormat()); params.put("parametersId", tset.getParametersId()); // run the insert, if that creates a record then also create the quota return jt.update(createTileSet, params) > 0; } protected TileSet getOrCreateTileSet(TileSet tileSet) { Exception lastException = null; for (int i = 0; i < maxLoops; i++) { TileSet tset = getTileSetByIdInternal(tileSet.getId()); if (tset != null) { return tset; } try { if (createTileSet(tileSet)) { return tileSet; } } catch (DataAccessException e) { // fine, it might be another node created this table lastException = e; } } throw new ConcurrencyFailureException("Failed to create or locate tileset " + tileSet + " after " + maxLoops + " attempts", lastException); } public TilePageCalculator getTilePageCalculator() { return calculator; } public void addToQuotaAndTileCounts(final TileSet tileSet, final Quota quotaDiff, final Collection<PageStatsPayload> tileCountDiffs) throws InterruptedException { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { getOrCreateTileSet(tileSet); updateQuotas(tileSet, quotaDiff); if (tileCountDiffs != null) { // sort the payloads by page id as a deadlock avoidance measure, out // of order updates may result in deadlock with the addHitsAndSetAccessTime method List<PageStatsPayload> sorted = sortPayloads(tileCountDiffs); for (PageStatsPayload payload : sorted) { upsertTilePageFillFactor(payload); } } } private void updateQuotas(final TileSet tileSet, final Quota quotaDiff) { if (log.isDebugEnabled()) { log.info("Applying quota diff " + quotaDiff.getBytes() + " on tileset " + tileSet); } String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); Map<String, Object> params = new HashMap<String, Object>(); params.put("tileSetId", tileSet.getId()); params.put("bytes", new BigDecimal(quotaDiff.getBytes())); jt.update(updateQuota, params); params.put("tileSetId", GLOBAL_QUOTA_NAME); jt.update(updateQuota, params); } private void upsertTilePageFillFactor(PageStatsPayload payload) { if (log.isDebugEnabled()) { log.info("Applying page stats payload " + payload); } // see http://en.wikipedia.org/wiki/Merge_(SQL) // Even the Merge command that some databases support is prone to race conditions // under concurrent load, but we don't want to lose data and it's difficult to // tell apart the race conditions from other failures, so we use tolerant commands // and loop over them. // Loop conditions: we find the page stats, but they are deleted before we can // update // them, we don't find the page stats, but they are inserted before we can do so, in // both cases we re-start from zero TilePage page = payload.getPage(); final byte level = page.getZoomLevel(); final BigInteger tilesPerPage = calculator.getTilesPerPage(tileSet, level); int modified = 0; int count = 0; while (modified == 0 && count < maxLoops) { try { count++; PageStats stats = getPageStats(page.getKey()); if (stats != null) { float oldFillFactor = stats.getFillFactor(); stats.addTiles(payload.getNumTiles(), tilesPerPage); // if no change, bail out early if (oldFillFactor == stats.getFillFactor()) { return; } // update the record in the db modified = updatePageFillFactor(page, stats, oldFillFactor); } else { // create the stats and update the fill factor stats = new PageStats(0); stats.addTiles(payload.getNumTiles(), tilesPerPage); modified = createNewPageStats(stats, page); } } catch(DeadlockLoserDataAccessException e) { if(log.isDebugEnabled()) { log.debug("Deadlock while updating page stats, will retry", e); } } } if (modified == 0) { throw new ConcurrencyFailureException( "Failed to create or update page stats for page " + payload.getPage() + " after " + count + " attempts"); } } }); } /** * Sorts the payloads by page key * @param tileCountDiffs * @return */ protected List<PageStatsPayload> sortPayloads(Collection<PageStatsPayload> tileCountDiffs) { List<PageStatsPayload> result = new ArrayList<PageStatsPayload>(tileCountDiffs); Collections.sort(result, new Comparator<PageStatsPayload>() { public int compare(PageStatsPayload pl1, PageStatsPayload pl2) { TilePage p1 = pl1.getPage(); TilePage p2 = pl2.getPage(); return p1.getKey().compareTo(p2.getKey()); }}); return result; } private int updatePageFillFactor(TilePage page, PageStats stats, float oldFillFactor) { if (log.isDebugEnabled()) { log.info("Updating page " + page + " fill factor from " + oldFillFactor + " to " + stats.getFillFactor()); } String update = dialect.conditionalUpdatePageStatsFillFactor(schema, "key", "fillFactor", "oldFillFactor"); Map<String, Object> params = new HashMap<String, Object>(); params.put("key", page.getKey()); params.put("fillFactor", stats.getFillFactor()); params.put("oldFillFactor", oldFillFactor); return jt.update(update, params); } private int setPageFillFactor(TilePage page, PageStats stats) { if (log.isDebugEnabled()) { log.info("Setting page " + page + " fill factor to " + stats.getFillFactor()); } String update = dialect.updatePageStatsFillFactor(schema, "key", "fillFactor"); Map<String, Object> params = new HashMap<String, Object>(); params.put("key", page.getKey()); params.put("fillFactor", stats.getFillFactor()); return jt.update(update, params); } private int createNewPageStats(PageStats stats, TilePage page) { if (log.isDebugEnabled()) { log.info("Creating new page stats: " + stats); } // for the moment we don't have the page in the db, we have to create it String insert = dialect.contionalTilePageInsertStatement(schema, "key", "tileSetId", "pageZ", "pageX", "pageY", "creationTime", "frequencyOfUse", "lastAccessTime", "fillFactor", "numHits"); Map<String, Object> params = new HashMap<String, Object>(); params.put("key", page.getKey()); params.put("tileSetId", page.getTileSetId()); params.put("pageZ", page.getZoomLevel()); params.put("pageX", page.getPageX()); params.put("pageY", page.getPageY()); params.put("creationTime", page.getCreationTimeMinutes()); params.put("frequencyOfUse", stats.getFrequencyOfUsePerMinute()); params.put("lastAccessTime", stats.getLastAccessTimeMinutes()); params.put("fillFactor", stats.getFillFactor()); params.put("numHits", new BigDecimal(stats.getNumHits())); // try the insert, mind, someone else might have done it as well, in such // case the insert will fail and return 0 record modified return jt.update(insert, params); } private PageStats getPageStats(String pageStatsKey) { String getPageStats = dialect.getPageStats(schema, "key"); return jt.queryForOptionalObject(getPageStats, new RowMapper<PageStats>() { public PageStats mapRow(ResultSet rs, int rowNum) throws SQLException { PageStats ps = new PageStats(0); // FREQUENCY_OF_USE, LAST_ACCESS_TIME, FILL_FACTOR, NUM_HITS FROM ps.setFrequencyOfUsePerMinute(rs.getFloat(1)); ps.setLastAccessMinutes(rs.getInt(2)); ps.setFillFactor(rs.getFloat(3)); ps.setNumHits(rs.getBigDecimal(4).toBigInteger()); return ps; } }, Collections.singletonMap("key", pageStatsKey)); } @SuppressWarnings("unchecked") public Future<List<PageStats>> addHitsAndSetAccesTime( final Collection<PageStatsPayload> statsUpdates) { return executor.submit(new Callable<List<PageStats>>() { public List<PageStats> call() throws Exception { return (List<PageStats>) tt.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus status) { List<PageStats> result = new ArrayList<PageStats>(); if (statsUpdates != null) { // sort the payloads by page id as a deadlock avoidance measure, out // of order updates may result in deadlock with the addHitsAndSetAccessTime method List<PageStatsPayload> sorted = sortPayloads(statsUpdates); for (PageStatsPayload payload : sorted) { // verify the stats are referring to an existing tile set id TileSet tset = payload.getTileSet(); if (tset == null) { String tileSetId = payload.getPage().getTileSetId(); tset = getTileSetByIdInternal(tileSetId); if (tset == null) { log.warn("Could not locate tileset with id " + tileSetId + ", skipping page stats update: " + payload); } } else { getOrCreateTileSet(tset); } // update the stats PageStats stats = upsertTilePageHitAccessTime(payload); result.add(stats); } } return result; } private PageStats upsertTilePageHitAccessTime(PageStatsPayload payload) { TilePage page = payload.getPage(); if (log.isDebugEnabled()) { log.info("Updating page " + page + " with payload " + payload); } int modified = 0; int count = 0; PageStats stats = null; while (modified == 0 && count < maxLoops) { try { count++; stats = getPageStats(page.getKey()); if (stats != null) { // gather the old values, we'll use them for the optimistic locking final BigInteger oldHits = stats.getNumHits(); final float oldFrequency = stats.getFrequencyOfUsePerMinute(); final int oldAccessTime = stats.getLastAccessTimeMinutes(); // update the page so that it computes the new stats updatePageStats(payload, page, stats); // update the record in the db String update = dialect.updatePageStats(schema, "key", "newHits", "oldHits", "newFrequency", "oldFrequency", "newAccessTime", "oldAccessTime"); Map<String, Object> params = new HashMap<String, Object>(); params.put("key", page.getKey()); params.put("newHits", new BigDecimal(stats.getNumHits())); params.put("oldHits", new BigDecimal(oldHits)); params.put("newFrequency", stats.getFrequencyOfUsePerMinute()); params.put("oldFrequency", oldFrequency); params.put("newAccessTime", stats.getLastAccessTimeMinutes()); params.put("oldAccessTime", oldAccessTime); modified = jt.update(update, params); } else { // create the new stats and insert it stats = new PageStats(0); updatePageStats(payload, page, stats); modified = createNewPageStats(stats, page); } } catch(DeadlockLoserDataAccessException e) { if(log.isDebugEnabled()) { log.debug("Deadlock while updating page stats, will retry", e); } } } if (modified == 0) { throw new ConcurrencyFailureException( "Failed to create or update page stats for page " + payload.getPage() + " after " + count + " attempts"); } return stats; } private void updatePageStats(PageStatsPayload payload, TilePage page, PageStats stats) { final int addedHits = payload.getNumHits(); final int lastAccessTimeMinutes = (int) (payload.getLastAccessTime() / 1000 / 60); final int creationTimeMinutes = page.getCreationTimeMinutes(); stats.addHitsAndAccessTime(addedHits, lastAccessTimeMinutes, creationTimeMinutes); } }); } }); } public long[][] getTilesForPage(TilePage page) throws InterruptedException { TileSet tileSet = getTileSetById(page.getTileSetId()); long[][] gridCoverage = calculator.toGridCoverage(tileSet, page); return gridCoverage; } public TilePage getLeastFrequentlyUsedPage(Set<String> layerNames) throws InterruptedException { return getSinglePage(layerNames, true); } public TilePage getLeastRecentlyUsedPage(Set<String> layerNames) throws InterruptedException { return getSinglePage(layerNames, false); } private TilePage getSinglePage(Set<String> layerNames, boolean leastFrequentlyUsed) { Map<String, Object> params = new HashMap<String, Object>(); List<String> layerParamNames = new ArrayList<String>(); int i = 0; for (String layer : layerNames) { i++; String param = "Layer" + i; params.put(param, layer); layerParamNames.add(param); } String select; if (leastFrequentlyUsed) { select = dialect.getLeastFrequentlyUsedPage(schema, layerParamNames); } else { select = dialect.getLeastRecentlyUsedPage(schema, layerParamNames); } TilePageRowMapper mapper = new TilePageRowMapper(); return jt.queryForOptionalObject(select, mapper, params); } public PageStats setTruncated(final TilePage page) throws InterruptedException { return (PageStats) tt.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus status) { if (log.isDebugEnabled()) { log.info("Truncating page " + page); } PageStats stats = getPageStats(page.getKey()); if (stats != null) { stats.setFillFactor(0); // update the record in the db int modified = setPageFillFactor(page, stats); // if no record updated the page has been deleted by another instance if (modified == 0) { return null; } } return stats; } }); } public void close() throws Exception { log.info("Closing up the JDBC quota store "); // try to close the data source if possible if (dataSource instanceof BasicDataSource) { ((BasicDataSource) dataSource).close(); } else if (dataSource instanceof Closeable) { ((Closeable) dataSource).close(); } // release the templates tt = null; jt = null; } /** * Maps a BigDecimal column into a Quota object * * @author Andrea Aime - GeoSolutions * */ static class DiskQuotaMapper implements RowMapper<Quota> { public Quota mapRow(ResultSet rs, int rowNum) throws SQLException { BigDecimal bytes = rs.getBigDecimal(1); if (bytes == null) { bytes = BigDecimal.ZERO; } return new Quota(bytes.toBigInteger()); } } /** * Maps a result set into {@link TileSet} objects * * @author Andrea Aime - GeoSolutions * */ static class TileSetRowMapper implements RowMapper<TileSet> { public TileSet mapRow(ResultSet rs, int rowNum) throws SQLException { String key = rs.getString(1); String layerName = rs.getString(2); String gridSetId = rs.getString(3); String blobFormat = rs.getString(4); String parametersId = rs.getString(5); if (GLOBAL_QUOTA_NAME.equals(key)) { return new TileSet(key); } else { return new TileSet(layerName, gridSetId, blobFormat, parametersId); } } } /** * Maps a result set into {@link TilePage} objects * * @author Andrea Aime - GeoSolutions * */ static class TilePageRowMapper implements RowMapper<TilePage> { public TilePage mapRow(ResultSet rs, int rowNum) throws SQLException { String tileSetId = rs.getString(1); int pageX = rs.getInt(2); int pageY = rs.getInt(3); int pageZ = rs.getInt(4); int creationTimeMinutes = rs.getInt(5); return new TilePage(tileSetId, pageX, pageY, pageZ, creationTimeMinutes); } } @Override public void deleteParameters(final String layerName, final String parametersId) { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { // first gather the disk quota used by the gridset, and update the global quota Quota quota = getUsedQuotaByParametersId(parametersId); quota.setBytes(quota.getBytes().negate()); String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); Map<String, Object> params = new HashMap<String, Object>(); params.put("tileSetId", GLOBAL_QUOTA_NAME); params.put("bytes", new BigDecimal(quota.getBytes())); jt.update(updateQuota, params); // then delete all the gridsets with the specified id String statement = dialect.getLayerParametersDeletionStatement(schema, "layerName", "parametersId"); params = new HashMap<String, Object>(); params.put("layerName", layerName); params.put("parametersId", parametersId); jt.update(statement, params); } }); } }