/* // $Id: XmlaOlap4jConcurrentMemoryCache.java 455 2011-05-24 10:01:26Z jhyde $ // This software is subject to the terms of the Eclipse Public License v1.0 // Agreement, available at the following URL: // http://www.eclipse.org/legal/epl-v10.html. // Copyright (C) 2008-2011 Julian Hyde // All Rights Reserved. // You must accept the terms of that agreement to use this software. */ package org.olap4j.driver.olap4ld.cache; import java.net.*; import java.util.*; import java.util.Map.*; import java.util.concurrent.*; import org.olap4j.driver.olap4ld.cache.XmlaOlap4jCacheElement; import org.olap4j.driver.olap4ld.cache.XmlaOlap4jNamedMemoryCache; import org.olap4j.driver.olap4ld.cache.XmlaOlap4jShaEncoder; import org.olap4j.driver.olap4ld.cache.XmlaOlap4jNamedMemoryCache.Mode; import org.olap4j.driver.olap4ld.cache.XmlaOlap4jNamedMemoryCache.Property; import org.olap4j.impl.Olap4jUtil; /** * Thread-safe cache object which supports concurrent access. * * <p>It keeps its cache element objects in memory in an internal hash * table. Instantiate it and use. As simple as that. * * @author Luc Boudreau * @version $Id: XmlaOlap4jConcurrentMemoryCache.java 455 2011-05-24 10:01:26Z jhyde $ */ class XmlaOlap4jConcurrentMemoryCache { /** * Default cache timeout (1 minute). The value is in seconds. */ private final static int DEFAULT_CACHE_TIMEOUT = 60; /** * Default cache size (10). */ private final static int DEFAULT_CACHE_SIZE = 10; /** * Default eviction mode (LFU). */ private final static Mode DEFAULT_EVICTION_MODE = Mode.LFU; /** * Thread-safe hashmap which will be used as a cache. * * <p>The cache is a map structured as follows: * * <ul> * <li>key -> String : SHA-1 encoding of the full URL</li> * </ul> */ private Map<String, XmlaOlap4jCacheElement> cacheEntries = new ConcurrentHashMap<String, XmlaOlap4jCacheElement>(); /** * Cache size. */ private int cacheSize = DEFAULT_CACHE_SIZE; /** * Eviction mode. */ private Mode evictionMode = DEFAULT_EVICTION_MODE; /** * Cache timeout, in seconds. */ private int cacheTimeout = DEFAULT_CACHE_TIMEOUT; /** * Creates an XmlaOlap4jConcurrentMemoryCache. * * @param props Properties * @throws IllegalArgumentException */ public XmlaOlap4jConcurrentMemoryCache( Map<String, String> props) throws IllegalArgumentException { for (Entry<String, String> entry : props.entrySet()) { if (Property.SIZE.name().equalsIgnoreCase( entry.getKey().toString())) { this.setCacheSize( Integer.parseInt(entry.getValue().toString())); } else if (Property.TIMEOUT.name().equalsIgnoreCase( entry.getKey().toString())) { this.setCacheTimeout( Integer.parseInt(entry.getValue().toString())); } else if (Property.MODE.name().equalsIgnoreCase( entry.getKey().toString())) { this.setCacheMode( entry.getValue().toString()); } } } /** * Sets the number of cached entries. * @param size The number of cached entries. */ private void setCacheSize(int size) { if (size <= 0) { throw new IllegalArgumentException( "Cache size must be positive, but was " + size); } this.cacheSize = size; } /** * Sets the eviction mode. * * @param mode Eviction mode */ private void setCacheMode(String mode) { if (Mode.valueOf(mode) == null) { throw new IllegalArgumentException( "The XmlaOlap4jMemoryCache mode has to be one of " + Mode.class.getName()); } this.evictionMode = Mode.valueOf(mode); } /** * Sets the cache expiration timeout. * * @param seconds The number of seconds to hold the entries in cache. */ private void setCacheTimeout(int seconds) { if (seconds <= 0) { throw new IllegalArgumentException( "Cache timeout must be positive, but was " + seconds); } this.cacheTimeout = seconds; } byte[] get( final URL url, final byte[] request) { // Take the cache for ourself synchronized (this.cacheEntries) { // Clean expired values cleanExpired(false); // Extract the data from the cache XmlaOlap4jCacheElement entry = this.cacheEntries.get( XmlaOlap4jShaEncoder.encodeSha1( url.toExternalForm() + new String(request))); // Increment its counter if (entry != null) { entry.incrementHitCount(); entry.refreshTimestamp(); } // Return a copy to prevent corruption return entry != null ? new String(entry.getResponse()).getBytes() : null; } } void put( final URL url, final byte[] request, final byte[] response) { // Take the cache for ourself synchronized (this.cacheEntries) { // Make some cleanup cleanExpired(true); if (this.cacheEntries.size() < cacheSize) { // Create the entry XmlaOlap4jCacheElement entry = new XmlaOlap4jCacheElement(); entry.setResponse(response); this.cacheEntries.put( XmlaOlap4jShaEncoder.encodeSha1( String.valueOf(url.toExternalForm()) + new String(request)), entry); } else { throw new RuntimeException("Concurrency error detected."); } } } /** * Cleans expired cache entries. * * @param makeRoom Whether to make room for later appending by * evicting an entry based on the selected eviction mode. */ private void cleanExpired(boolean makeRoom) { final String toBeEvicted; switch (evictionMode) { case FIFO: case LIFO: toBeEvicted = timeBasedEviction(makeRoom); break; case LFU: case MFU: toBeEvicted = hitBasedEviction(makeRoom); break; default: throw Olap4jUtil.unexpected(evictionMode); } // Make some space if required if (makeRoom && this.cacheEntries.size() >= cacheSize && toBeEvicted != null) { this.cacheEntries.remove(toBeEvicted); } } /** * Scans for the key of the cache entry to be evicted based * on the selected time based eviction mode. * * @param makeRoom Whether to make room for later appending by * evicting an entry. if false is specified, there might not * be an evicted entry if the cache is not full. * @return The key of the entry to remove, null otherwise. */ private String timeBasedEviction(boolean makeRoom) { // This is a flag to find the oldest entry. long currentEvictedTimestamp = evictionMode == Mode.LIFO ? Long.MAX_VALUE : Long.MIN_VALUE; String toBeEvicted = null; // Iterate over entries for (Entry<String, XmlaOlap4jCacheElement> entry : this.cacheEntries.entrySet()) { // Check if not expired if (Calendar.getInstance().getTimeInMillis() > (entry.getValue().getTimestamp().longValue() + (cacheTimeout * 1000))) { // Evicts it. this.cacheEntries.remove(entry.getKey()); continue; } // Checks if this is the oldest entry. if ((makeRoom && (evictionMode == XmlaOlap4jNamedMemoryCache.Mode.LIFO && entry.getValue().getTimestamp().longValue() < currentEvictedTimestamp)) || (makeRoom && (evictionMode == XmlaOlap4jNamedMemoryCache.Mode.FIFO && entry.getValue().getTimestamp().longValue() > currentEvictedTimestamp))) { currentEvictedTimestamp = entry.getValue().getTimestamp().longValue(); toBeEvicted = entry.getKey(); } } return toBeEvicted; } /** * Scans for the key of the cache entry to be evicted based * on the selected hit based eviction mode. * * @param makeRoom Whether to make room for later appending by evicting an * entry. If false, there might not be an evicted entry if the cache is not * full * * @return The key of the entry to remove, null otherwise. */ private String hitBasedEviction(boolean makeRoom) { // Flag to find the oldest entry. long currentEvictedHits = (evictionMode == Mode.LFU) ? Long.MAX_VALUE : Long.MIN_VALUE; String toBeEvicted = null; // Iterates over entries for (Entry<String, XmlaOlap4jCacheElement> entry : this.cacheEntries.entrySet()) { // Checks if not expired if (Calendar.getInstance().getTimeInMillis() > (entry.getValue().getTimestamp().longValue() + (cacheTimeout * 1000))) { // Evicts it this.cacheEntries.remove(entry.getKey()); continue; } // Checks if this is the oldest entry. if ((makeRoom && (evictionMode == Mode.LFU && entry.getValue().getHitCount().longValue() < currentEvictedHits)) || (makeRoom && (evictionMode == XmlaOlap4jNamedMemoryCache.Mode.MFU && entry.getValue().getHitCount().longValue() > currentEvictedHits))) { currentEvictedHits = entry.getValue().getHitCount().longValue(); toBeEvicted = entry.getKey(); } } return toBeEvicted; } } // End XmlaOlap4jConcurrentMemoryCache.java