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