/** * 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/>. */ package org.geowebcache.storage.blobstore.memory.guava; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.Logger; import org.geowebcache.storage.TileObject; import org.geowebcache.storage.blobstore.memory.CacheConfiguration; import org.geowebcache.storage.blobstore.memory.CacheConfiguration.EvictionPolicy; import org.geowebcache.storage.blobstore.memory.CacheProvider; import org.geowebcache.storage.blobstore.memory.CacheStatistics; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheStats; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.cache.Weigher; /** * This class is an implementation of the {@link CacheProvider} interface using a backing Guava {@link Cache} object. This implementation requires to * be configured with the configure() method. * * @author Nicola Lagomarsini Geosolutions */ public class GuavaCacheProvider implements CacheProvider { /** {@link Logger} object used for logging exceptions */ private final static Log LOGGER = LogFactory.getLog(GuavaCacheProvider.class); /** Separator char used for creating Cache keys */ public final static String SEPARATOR = "_"; /** Constant for multiplying bytes to MB */ public final static long BYTES_TO_MB = 1048576; /** Size of the scheduled thread pool */ public static final int CORE_POOL_SIZE = 1; private static final String GUAVA_NAME = "Guava Cache"; /** Array containing the supported Policies */ public final static List<EvictionPolicy> POLICIES = Collections.unmodifiableList(Arrays.asList( EvictionPolicy.NULL, EvictionPolicy.EXPIRE_AFTER_ACCESS, EvictionPolicy.EXPIRE_AFTER_WRITE)); /** * This class handles the {@link CacheStats} object returned by the guava cache. * * @author Nicola Lagomarsini Geosolutions */ public static class GuavaCacheStatistics extends CacheStatistics { /** serialVersionUID */ private static final long serialVersionUID = 1L; public GuavaCacheStatistics(CacheStats stats, double currentSpace, long actualSize, long totalSize) { this.setEvictionCount(stats.evictionCount()); this.setHitCount(stats.hitCount()); this.setMissCount(stats.missCount()); this.setTotalCount(stats.requestCount()); this.setHitRate((int) (stats.hitRate() * 100)); this.setMissRate(100 - getHitRate()); this.setCurrentMemoryOccupation(currentSpace); this.setActualSize(actualSize); this.setTotalSize(totalSize); } } /** Cache object containing the various {@link TileObject}s */ private Cache<String, TileObject> cache; /** Internal Multimap used for storing the TileObject ids associated to each cached Layer */ private LayerMap multimap; /** {@link AtomicBoolean} used for ensuring that the Cache has already been configured */ private AtomicBoolean configured; /** {@link AtomicLong} used for checking the number of active operations to wait when resetting the cache */ private AtomicLong actualOperations; /** Internal concurrent Set used for saving the names of the Layers that must not be cached */ private final Set<String> layers; /** Cache total memory in Mb */ private long maxMemory = 0L; /** {@link AtomicLong} used for storing the current cache size */ private AtomicLong currentSize = new AtomicLong(0); private ScheduledExecutorService scheduledPool; public GuavaCacheProvider(CacheConfiguration config) { // Initialization of the Layer set and of the Atomic parameters layers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); configured = new AtomicBoolean(false); actualOperations = new AtomicLong(0); configure(config); } /** * This method is used for creating a new cache object, from the defined configuration. * * @param configuration */ private void initCache(CacheConfiguration configuration) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Building new Cache"); } // Initialization step int concurrency = configuration.getConcurrencyLevel(); maxMemory = configuration.getHardMemoryLimit() * BYTES_TO_MB; long evictionTime = configuration.getEvictionTime(); EvictionPolicy policy = configuration.getPolicy(); // If Cache already exists, flush it if (cache != null) { cache.invalidateAll(); } // Create the CacheBuilder CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder(); // Add weigher Weigher<String, TileObject> weigher = new Weigher<String, TileObject>() { @Override public int weigh(String key, TileObject value) { currentSize.addAndGet(value.getBlobSize()); return value.getBlobSize(); } }; // Create the builder CacheBuilder<String, TileObject> newBuilder = builder.maximumWeight(maxMemory) .recordStats().weigher(weigher).concurrencyLevel(concurrency) .removalListener(new RemovalListener<String, TileObject>() { @Override public void onRemoval(RemovalNotification<String, TileObject> notification) { // TODO This operation is not atomic TileObject obj = notification.getValue(); // Update the current size currentSize.addAndGet(-obj.getBlobSize()); final String tileKey = generateTileKey(obj); final String layerName = obj.getLayerName(); multimap.removeTile(layerName, tileKey); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removed tile " + tileKey + " for layer " + layerName + " due to reason:" + notification.getCause().toString()); LOGGER.debug("Removed tile was evicted? " + notification.wasEvicted()); } } }); // Handle eviction policy boolean configuredPolicy = false; if (policy != null && evictionTime > 0) { if (policy == EvictionPolicy.EXPIRE_AFTER_ACCESS) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Configuring Expire After Access eviction policy"); } newBuilder.expireAfterAccess(evictionTime, TimeUnit.SECONDS); configuredPolicy = true; } else if (policy == EvictionPolicy.EXPIRE_AFTER_WRITE) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Configuring Expire After Write eviction policy"); } newBuilder.expireAfterWrite(evictionTime, TimeUnit.SECONDS); configuredPolicy = true; } } // Build the cache cache = newBuilder.build(); // Created a new multimap multimap = new LayerMap(); // Configure a new scheduling task if needed if (configuredPolicy) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Configuring Scheduled Task for cache eviction"); } Runnable command = new Runnable() { @Override public void run() { if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { cache.cleanUp(); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } }; // Initialization of the internal Scheduler task for scheduling cache cleanup scheduledPool = Executors.newScheduledThreadPool(CORE_POOL_SIZE); scheduledPool.scheduleAtFixedRate(command, 10, evictionTime + 1, TimeUnit.SECONDS); } // Update the configured parameter configured.getAndSet(true); } @Override public boolean isImmutable() { return false; } @Override public synchronized void configure(CacheConfiguration configuration) { // NOTE that if the cache has already been configured, the user must always call resetCache() before // setting the new configuration reset(); // Configure a new cache initCache(configuration); } @Override public TileObject getTileObj(TileObject obj) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking if the layer must not be cached"); } // Check if the layer must be cached if (layers.contains(obj.getLayerName())) { // The layer must not be cached return null; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Retrieving TileObject: " + obj + " from cache"); } // Generate the TileObject key String id = generateTileKey(obj); // Get the key from the cache return cache.getIfPresent(id); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } return null; } @Override public void putTileObj(TileObject obj) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking if the layer must not be cached"); } // Check if the layer must be cached if (layers.contains(obj.getLayerName())) { // The layer must not be cached return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Adding TileObject: " + obj + " to cache"); } // Generate the TileObject key String id = generateTileKey(obj); // Add the TileObject to the cache and its id in the multimap cache.put(id, obj); multimap.putTile(obj.getLayerName(), id); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public void removeTileObj(TileObject obj) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking if the layer must not be cached"); } // Check if the layer must be cached if (layers.contains(obj.getLayerName())) { // The layer must not be cached return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing TileObject: " + obj + " from cache"); } // Generate the TileObject key String id = generateTileKey(obj); // Remove the key cache.invalidate(id); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public void removeLayer(String layername) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking if the layer must not be cached"); } // Check if the layer must be cached if (layers.contains(layername)) { // The layer must not be cached return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing Layer: " + layername + " from cache"); } // Get all the TileObject ids associated to the Layer and removes them Set<String> keys = multimap.removeLayer(layername); if (keys != null) { cache.invalidateAll(keys); } } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public void clear() { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Flushing cache"); } // Remove all the elements from the cache if (cache != null) { cache.invalidateAll(); } } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public void reset() { if (configured.getAndSet(false)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Reset Cache internally"); } // Avoid to call the While cycle before having started an operation with configured == false actualOperations.incrementAndGet(); actualOperations.decrementAndGet(); // Wait until all the operations are finished while (actualOperations.get() > 0) { } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Flushing cache"); } // Remove all the elements from the cache if (cache != null) { cache.invalidateAll(); } // Remove all the Layers configured for avoiding caching if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing Layers"); } layers.clear(); // Shutdown the current Executor service if (scheduledPool != null) { scheduledPool.shutdown(); try { scheduledPool.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { if (LOGGER.isErrorEnabled()) { LOGGER.error(e.getMessage(), e); } } finally { scheduledPool = null; } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Cache is already reset"); } } } @Override public CacheStatistics getStatistics() { // Check if the cache has already been configured if (configured.get()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Retrieving statistics"); } // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { // Get cache statistics long actualSize = currentSize .get(); long currentSpace = (long) (100L - (1L) * (100 * ((1.0d) * (maxMemory - actualSize)) / maxMemory)); if (currentSpace < 0) { currentSpace = 0; } // Returns a new Object containing a snapshot of the cache statistics return new GuavaCacheStatistics(cache.stats(), currentSpace, actualSize, maxMemory); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Returning empty statistics"); } // Else returns an empty CacheStatistics object return new CacheStatistics(); } } /*** * Static method for generating the {@link TileObject} cache key to use for caching. * * @param obj * * @return {@link TileObject} key */ public static String generateTileKey(TileObject obj) { Map<String, String> parameters = obj.getParameters(); StringBuilder builder = new StringBuilder(obj.getLayerName()).append(SEPARATOR).append(obj.getGridSetId()) .append(SEPARATOR).append(Arrays.toString(obj.getXYZ())).append(SEPARATOR) .append(obj.getBlobFormat()); // If parameters are present they must be handled if(parameters != null && !parameters.isEmpty()){ for(String key : parameters.keySet()){ builder.append(SEPARATOR).append(key).append(SEPARATOR).append(parameters.get(key)); } } return builder.toString(); } @Override public String getName() { return GUAVA_NAME; } @Override public void addUncachedLayer(String layername) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Adding Layer:" + layername + " to avoid cache"); } // Adds the layer which should not be cached layers.add(layername); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public void removeUncachedLayer(String layername) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing Layer:" + layername + " to avoid cache"); } // Configure a Layer for being cached again layers.remove(layername); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } } @Override public boolean containsUncachedLayer(String layername) { // Check if the cache has already been configured if (configured.get()) { // Increment the number of current operations // This behavior is used in order to wait // the end of all the operations after setting // the configured parameter to false actualOperations.incrementAndGet(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Checking if Layer:" + layername + " must not be cached"); } // Check if the layer must not be cached return layers.contains(layername); } finally { // Decrement the number of current operations. actualOperations.decrementAndGet(); } } else { return false; } } @Override public List<EvictionPolicy> getSupportedPolicies() { return POLICIES; } @Override public boolean isAvailable() { return true; } /** * Internal class representing a concurrent multimap which associates to each Layer name the related {@link TileObject} cache keys. This map is * useful when trying to remove a Layer, because it returns quicly all the cached keys of the selected layer, without having to cycle on the cache * and checking if each TileObject belongs to the selected Layer. * * @author Nicola Lagomarsini, GeoSolutions * */ static class LayerMap { /** {@link ReentrantReadWriteLock} used for handling concurrency */ private final ReentrantReadWriteLock lock; /** {@link WriteLock} used when trying to change the map */ private final WriteLock writeLock; /** {@link ReadLock} used when accessing the map */ private final ReadLock readLock; /** MultiMap containing the {@link TileObject} keys for the Layers */ private final ConcurrentHashMap<String, Set<String>> layerMap = new ConcurrentHashMap<String, Set<String>>(); public LayerMap() { // Lock initialization lock = new ReentrantReadWriteLock(true); writeLock = lock.writeLock(); readLock = lock.readLock(); } /** * Insertion of a {@link TileObject} key in the map for the associated Layer. * * @param layer * @param id */ public void putTile(String layer, String id) { // ReadLock is used because we are only accessing the map readLock.lock(); Set<String> tileKeys = layerMap.get(layer); if (tileKeys == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("No KeySet for Layer: " + layer); } // If the Map is not present, we must add it // So we do the unlock and try to acquire the writeLock readLock.unlock(); writeLock.lock(); try { // Check again if the tileKey has not been added already tileKeys = layerMap.get(layer); if (tileKeys == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Creating new KeySet for Layer: " + layer); } // If no key is present then a new KeySet is created and then added to the multimap tileKeys = new ConcurrentSkipListSet<String>(); layerMap.put(layer, tileKeys); } // Downgrade by acquiring read lock before releasing write lock readLock.lock(); } finally { // Release the writeLock writeLock.unlock(); } } try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Add the TileObject id to the Map"); } // Finally the tile key is added. tileKeys.add(id); } finally { readLock.unlock(); } } /** * Removal of a {@link TileObject} key in the map for the associated Layer. * * @param layer * @param id */ public void removeTile(String layer, String id) { // ReadLock is used because we are only accessing the map readLock.lock(); try { // KeySet associated to the image Set<String> tileKeys = layerMap.get(layer); if (tileKeys != null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Remove TileObject id to the Map"); } // Removal of the keys tileKeys.remove(id); // If the KeySet is empty then it is removed from the multimap if (tileKeys.isEmpty()) { readLock.unlock(); writeLock.lock(); try { if (tileKeys.isEmpty()) { // Here writeLock is acquired again, but it is reentrant removeLayer(layer); } // Downgrade by acquiring read lock before releasing write lock readLock.lock(); } finally { writeLock.unlock(); } } } } finally { readLock.unlock(); } } /** * Removes a layer {@link Set} and returns it to the cache. * * @param layer * * @return the keys associated to the Layer */ public Set<String> removeLayer(String layer) { writeLock.lock(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing KeySet for Layer: " + layer); } // Get the Set from the map Set<String> layers = layerMap.get(layer); // Removes the set from the map layerMap.remove(layer); // Returns the set return layers; } finally { writeLock.unlock(); } } } }