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