001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.cache; 003 004import java.io.File; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.nio.channels.FileLock; 008import java.util.Properties; 009import java.util.logging.Handler; 010import java.util.logging.Level; 011import java.util.logging.LogRecord; 012import java.util.logging.Logger; 013import java.util.logging.SimpleFormatter; 014 015import org.apache.commons.jcs.access.CacheAccess; 016import org.apache.commons.jcs.auxiliary.AuxiliaryCache; 017import org.apache.commons.jcs.auxiliary.AuxiliaryCacheFactory; 018import org.apache.commons.jcs.auxiliary.disk.behavior.IDiskCacheAttributes; 019import org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheAttributes; 020import org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheFactory; 021import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes; 022import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheFactory; 023import org.apache.commons.jcs.engine.CompositeCacheAttributes; 024import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes.DiskUsagePattern; 025import org.apache.commons.jcs.engine.control.CompositeCache; 026import org.apache.commons.jcs.engine.control.CompositeCacheManager; 027import org.apache.commons.jcs.utils.serialization.StandardSerializer; 028import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.data.preferences.BooleanProperty; 031import org.openstreetmap.josm.data.preferences.IntegerProperty; 032import org.openstreetmap.josm.tools.Utils; 033 034/** 035 * @author Wiktor Niesiobędzki 036 * 037 * Wrapper class for JCS Cache. Sets some sane environment and returns instances of cache objects. 038 * Static configuration for now assumes some small LRU cache in memory and larger LRU cache on disk 039 * @since 8168 040 */ 041public final class JCSCacheManager { 042 private static final Logger LOG = FeatureAdapter.getLogger(JCSCacheManager.class.getCanonicalName()); 043 044 private static volatile CompositeCacheManager cacheManager; 045 private static long maxObjectTTL = -1; 046 private static final String PREFERENCE_PREFIX = "jcs.cache"; 047 public static BooleanProperty USE_BLOCK_CACHE = new BooleanProperty(PREFERENCE_PREFIX + ".use_block_cache", true); 048 049 private static final AuxiliaryCacheFactory diskCacheFactory = 050 USE_BLOCK_CACHE.get() ? new BlockDiskCacheFactory() : new IndexedDiskCacheFactory(); 051 private static FileLock cacheDirLock; 052 053 /** 054 * default objects to be held in memory by JCS caches (per region) 055 */ 056 public static final IntegerProperty DEFAULT_MAX_OBJECTS_IN_MEMORY = new IntegerProperty(PREFERENCE_PREFIX + ".max_objects_in_memory", 1000); 057 058 private JCSCacheManager() { 059 // Hide implicit public constructor for utility classes 060 } 061 062 @SuppressWarnings("resource") 063 private static void initialize() throws IOException { 064 File cacheDir = new File(Main.pref.getCacheDirectory(), "jcs"); 065 066 if (!cacheDir.exists() && !cacheDir.mkdirs()) 067 throw new IOException("Cannot access cache directory"); 068 069 File cacheDirLockPath = new File(cacheDir, ".lock"); 070 if (!cacheDirLockPath.exists() && !cacheDirLockPath.createNewFile()) { 071 LOG.log(Level.WARNING, "Cannot create cache dir lock file"); 072 } 073 cacheDirLock = new FileOutputStream(cacheDirLockPath).getChannel().tryLock(); 074 075 if (cacheDirLock == null) 076 LOG.log(Level.WARNING, "Cannot lock cache directory. Will not use disk cache"); 077 078 // raising logging level gives ~500x performance gain 079 // http://westsworld.dk/blog/2008/01/jcs-and-performance/ 080 final Logger jcsLog = Logger.getLogger("org.apache.commons.jcs"); 081 jcsLog.setLevel(Level.INFO); 082 jcsLog.setUseParentHandlers(false); 083 // we need a separate handler from Main's, as we downgrade LEVEL.INFO to DEBUG level 084 jcsLog.addHandler(new Handler() { 085 final SimpleFormatter formatter = new SimpleFormatter(); 086 087 @Override 088 public void publish(LogRecord record) { 089 String msg = formatter.formatMessage(record); 090 if (record.getLevel().intValue() >= Level.SEVERE.intValue()) { 091 Main.error(msg); 092 } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) { 093 Main.warn(msg); 094 // downgrade INFO level to debug, as JCS is too verbose at INFO level 095 } else if (record.getLevel().intValue() >= Level.INFO.intValue()) { 096 Main.debug(msg); 097 } else { 098 Main.trace(msg); 099 } 100 } 101 102 @Override 103 public void flush() { 104 // nothing to be done on flush 105 } 106 107 @Override 108 public void close() { 109 // nothing to be done on close 110 } 111 }); 112 113 // this could be moved to external file 114 Properties props = new Properties(); 115 // these are default common to all cache regions 116 // use of auxiliary cache and sizing of the caches is done with giving proper geCache(...) params 117 // CHECKSTYLE.OFF: SingleSpaceSeparator 118 props.setProperty("jcs.default.cacheattributes", CompositeCacheAttributes.class.getCanonicalName()); 119 props.setProperty("jcs.default.cacheattributes.MaxObjects", DEFAULT_MAX_OBJECTS_IN_MEMORY.get().toString()); 120 props.setProperty("jcs.default.cacheattributes.UseMemoryShrinker", "true"); 121 props.setProperty("jcs.default.cacheattributes.DiskUsagePatternName", "UPDATE"); // store elements on disk on put 122 props.setProperty("jcs.default.elementattributes", CacheEntryAttributes.class.getCanonicalName()); 123 props.setProperty("jcs.default.elementattributes.IsEternal", "false"); 124 props.setProperty("jcs.default.elementattributes.MaxLife", Long.toString(maxObjectTTL)); 125 props.setProperty("jcs.default.elementattributes.IdleTime", Long.toString(maxObjectTTL)); 126 props.setProperty("jcs.default.elementattributes.IsSpool", "true"); 127 // CHECKSTYLE.ON: SingleSpaceSeparator 128 CompositeCacheManager cm = CompositeCacheManager.getUnconfiguredInstance(); 129 cm.configure(props); 130 cacheManager = cm; 131 } 132 133 /** 134 * Returns configured cache object for named cache region 135 * @param <K> key type 136 * @param <V> value type 137 * @param cacheName region name 138 * @return cache access object 139 * @throws IOException if directory is not found 140 */ 141 public static <K, V> CacheAccess<K, V> getCache(String cacheName) throws IOException { 142 return getCache(cacheName, DEFAULT_MAX_OBJECTS_IN_MEMORY.get().intValue(), 0, null); 143 } 144 145 /** 146 * Returns configured cache object with defined limits of memory cache and disk cache 147 * @param <K> key type 148 * @param <V> value type 149 * @param cacheName region name 150 * @param maxMemoryObjects number of objects to keep in memory 151 * @param maxDiskObjects maximum size of the objects stored on disk in kB 152 * @param cachePath path to disk cache. if null, no disk cache will be created 153 * @return cache access object 154 * @throws IOException if directory is not found 155 */ 156 public static <K, V> CacheAccess<K, V> getCache(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) 157 throws IOException { 158 if (cacheManager != null) 159 return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath); 160 161 synchronized (JCSCacheManager.class) { 162 if (cacheManager == null) 163 initialize(); 164 return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath); 165 } 166 } 167 168 @SuppressWarnings("unchecked") 169 private static <K, V> CacheAccess<K, V> getCacheInner(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) 170 throws IOException { 171 CompositeCache<K, V> cc = cacheManager.getCache(cacheName, getCacheAttributes(maxMemoryObjects)); 172 173 if (cachePath != null && cacheDirLock != null) { 174 IDiskCacheAttributes diskAttributes = getDiskCacheAttributes(maxDiskObjects, cachePath, cacheName); 175 try { 176 if (cc.getAuxCaches().length == 0) { 177 AuxiliaryCache<K, V> diskCache = diskCacheFactory.createCache(diskAttributes, cacheManager, null, new StandardSerializer()); 178 cc.setAuxCaches(new AuxiliaryCache[]{diskCache}); 179 } 180 } catch (IOException e) { 181 throw e; 182 } catch (Exception e) { 183 throw new IOException(e); 184 } 185 } 186 return new CacheAccess<>(cc); 187 } 188 189 /** 190 * Close all files to ensure, that all indexes and data are properly written 191 */ 192 public static void shutdown() { 193 // use volatile semantics to get consistent object 194 CompositeCacheManager localCacheManager = cacheManager; 195 if (localCacheManager != null) { 196 localCacheManager.shutDown(); 197 } 198 } 199 200 private static IDiskCacheAttributes getDiskCacheAttributes(int maxDiskObjects, String cachePath, String cacheName) { 201 IDiskCacheAttributes ret; 202 if (USE_BLOCK_CACHE.get()) { 203 BlockDiskCacheAttributes blockAttr = new BlockDiskCacheAttributes(); 204 blockAttr.setMaxKeySize(maxDiskObjects); 205 blockAttr.setBlockSizeBytes(4096); // use 4k blocks 206 ret = blockAttr; 207 } else { 208 IndexedDiskCacheAttributes indexAttr = new IndexedDiskCacheAttributes(); 209 indexAttr.setMaxKeySize(maxDiskObjects); 210 ret = indexAttr; 211 } 212 ret.setDiskLimitType(IDiskCacheAttributes.DiskLimitType.SIZE); 213 File path = new File(cachePath); 214 if (!path.exists() && !path.mkdirs()) { 215 LOG.log(Level.WARNING, "Failed to create cache path: {0}", cachePath); 216 } else { 217 ret.setDiskPath(cachePath); 218 } 219 ret.setCacheName(cacheName + (USE_BLOCK_CACHE.get() ? "_BLOCK_v2" : "_INDEX_v2")); 220 221 removeStaleFiles(cachePath + File.separator + cacheName, (USE_BLOCK_CACHE.get() ? "_INDEX_v2" : "_BLOCK_v2")); 222 return ret; 223 } 224 225 private static void removeStaleFiles(String basePathPart, String suffix) { 226 deleteCacheFiles(basePathPart); // TODO: this can be removed around 2016.09 227 deleteCacheFiles(basePathPart + "_BLOCK"); // TODO: this can be removed around 2016.09 228 deleteCacheFiles(basePathPart + "_INDEX"); // TODO: this can be removed around 2016.09 229 deleteCacheFiles(basePathPart + suffix); 230 } 231 232 private static void deleteCacheFiles(String basePathPart) { 233 Utils.deleteFile(new File(basePathPart + ".key")); 234 Utils.deleteFile(new File(basePathPart + ".data")); 235 } 236 237 private static CompositeCacheAttributes getCacheAttributes(int maxMemoryElements) { 238 CompositeCacheAttributes ret = new CompositeCacheAttributes(); 239 ret.setMaxObjects(maxMemoryElements); 240 ret.setDiskUsagePattern(DiskUsagePattern.UPDATE); 241 return ret; 242 } 243}