source: josm/trunk/src/org/openstreetmap/josm/data/cache/JCSCacheManager.java @ 13647

Last change on this file since 13647 was 13647, checked in by Don-vip, 11 months ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

  • Property svn:eol-style set to native
File size: 12.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.cache;
3
4import java.io.File;
5import java.io.IOException;
6import java.nio.channels.FileChannel;
7import java.nio.channels.FileLock;
8import java.nio.file.StandardOpenOption;
9import java.util.Arrays;
10import java.util.Properties;
11import java.util.logging.Handler;
12import java.util.logging.Level;
13import java.util.logging.LogRecord;
14import java.util.logging.Logger;
15import java.util.logging.SimpleFormatter;
16
17import org.apache.commons.jcs.access.CacheAccess;
18import org.apache.commons.jcs.auxiliary.AuxiliaryCache;
19import org.apache.commons.jcs.auxiliary.AuxiliaryCacheFactory;
20import org.apache.commons.jcs.auxiliary.disk.behavior.IDiskCacheAttributes;
21import org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheAttributes;
22import org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheFactory;
23import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes;
24import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheFactory;
25import org.apache.commons.jcs.engine.CompositeCacheAttributes;
26import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes.DiskUsagePattern;
27import org.apache.commons.jcs.engine.control.CompositeCache;
28import org.apache.commons.jcs.engine.control.CompositeCacheManager;
29import org.apache.commons.jcs.utils.serialization.StandardSerializer;
30import org.openstreetmap.josm.data.preferences.BooleanProperty;
31import org.openstreetmap.josm.data.preferences.IntegerProperty;
32import org.openstreetmap.josm.spi.preferences.Config;
33import org.openstreetmap.josm.tools.Logging;
34import org.openstreetmap.josm.tools.Utils;
35
36/**
37 * Wrapper class for JCS Cache. Sets some sane environment and returns instances of cache objects.
38 * Static configuration for now assumes some small LRU cache in memory and larger LRU cache on disk
39 *
40 * @author Wiktor Niesiobędzki
41 * @since 8168
42 */
43public final class JCSCacheManager {
44    private static volatile CompositeCacheManager cacheManager;
45    private static final long maxObjectTTL = -1;
46    private static final String PREFERENCE_PREFIX = "jcs.cache";
47    public static final BooleanProperty USE_BLOCK_CACHE = new BooleanProperty(PREFERENCE_PREFIX + ".use_block_cache", true);
48
49    private static final AuxiliaryCacheFactory DISK_CACHE_FACTORY =
50            USE_BLOCK_CACHE.get() ? new BlockDiskCacheFactory() : new IndexedDiskCacheFactory();
51    private static FileLock cacheDirLock;
52
53    /**
54     * default objects to be held in memory by JCS caches (per region)
55     */
56    public static final IntegerProperty DEFAULT_MAX_OBJECTS_IN_MEMORY = new IntegerProperty(PREFERENCE_PREFIX + ".max_objects_in_memory", 1000);
57
58    private static final Logger jcsLog;
59
60    static {
61        // raising logging level gives ~500x performance gain
62        // http://westsworld.dk/blog/2008/01/jcs-and-performance/
63        jcsLog = Logger.getLogger("org.apache.commons.jcs");
64        try {
65            jcsLog.setLevel(Level.INFO);
66            jcsLog.setUseParentHandlers(false);
67            // we need a separate handler from Main's, as we downgrade LEVEL.INFO to DEBUG level
68            Arrays.stream(jcsLog.getHandlers()).forEach(jcsLog::removeHandler);
69            jcsLog.addHandler(new Handler() {
70                final SimpleFormatter formatter = new SimpleFormatter();
71
72                @Override
73                public void publish(LogRecord record) {
74                    String msg = formatter.formatMessage(record);
75                    if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
76                        Logging.error(msg);
77                    } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
78                        Logging.warn(msg);
79                        // downgrade INFO level to debug, as JCS is too verbose at INFO level
80                    } else if (record.getLevel().intValue() >= Level.INFO.intValue()) {
81                        Logging.debug(msg);
82                    } else {
83                        Logging.trace(msg);
84                    }
85                }
86
87                @Override
88                public void flush() {
89                    // nothing to be done on flush
90                }
91
92                @Override
93                public void close() {
94                    // nothing to be done on close
95                }
96            });
97        } catch (SecurityException e) {
98            Logging.log(Logging.LEVEL_ERROR, "Unable to configure JCS logs", e);
99        }
100    }
101
102    private JCSCacheManager() {
103        // Hide implicit public constructor for utility classes
104    }
105
106    @SuppressWarnings("resource")
107    private static void initialize() {
108        File cacheDir = new File(Config.getDirs().getCacheDirectory(true), "jcs");
109
110        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
111            Logging.warn("Cache directory " + cacheDir.toString() + " does not exists and could not create it");
112        } else {
113            File cacheDirLockPath = new File(cacheDir, ".lock");
114            try {
115                if (!cacheDirLockPath.exists() && !cacheDirLockPath.createNewFile()) {
116                    Logging.warn("Cannot create cache dir lock file");
117                }
118                cacheDirLock = FileChannel.open(cacheDirLockPath.toPath(), StandardOpenOption.WRITE).tryLock();
119
120                if (cacheDirLock == null)
121                    Logging.warn("Cannot lock cache directory. Will not use disk cache");
122            } catch (IOException e) {
123                Logging.warn("Cannot create cache dir \"" + cacheDirLockPath.toString() + "\" lock file: " + e.toString());
124                Logging.warn("Will not use disk cache");
125            }
126        }
127        // this could be moved to external file
128        Properties props = new Properties();
129        // these are default common to all cache regions
130        // use of auxiliary cache and sizing of the caches is done with giving proper getCache(...) params
131        // CHECKSTYLE.OFF: SingleSpaceSeparator
132        props.setProperty("jcs.default.cacheattributes",                      CompositeCacheAttributes.class.getCanonicalName());
133        props.setProperty("jcs.default.cacheattributes.MaxObjects",           DEFAULT_MAX_OBJECTS_IN_MEMORY.get().toString());
134        props.setProperty("jcs.default.cacheattributes.UseMemoryShrinker",    "true");
135        props.setProperty("jcs.default.cacheattributes.DiskUsagePatternName", "UPDATE"); // store elements on disk on put
136        props.setProperty("jcs.default.elementattributes",                    CacheEntryAttributes.class.getCanonicalName());
137        props.setProperty("jcs.default.elementattributes.IsEternal",          "false");
138        props.setProperty("jcs.default.elementattributes.MaxLife",            Long.toString(maxObjectTTL));
139        props.setProperty("jcs.default.elementattributes.IdleTime",           Long.toString(maxObjectTTL));
140        props.setProperty("jcs.default.elementattributes.IsSpool",            "true");
141        // CHECKSTYLE.ON: SingleSpaceSeparator
142        CompositeCacheManager cm = CompositeCacheManager.getUnconfiguredInstance();
143        cm.configure(props);
144        cacheManager = cm;
145    }
146
147    /**
148     * Returns configured cache object for named cache region
149     * @param <K> key type
150     * @param <V> value type
151     * @param cacheName region name
152     * @return cache access object
153     */
154    public static <K, V> CacheAccess<K, V> getCache(String cacheName) {
155        return getCache(cacheName, DEFAULT_MAX_OBJECTS_IN_MEMORY.get().intValue(), 0, null);
156    }
157
158    /**
159     * Returns configured cache object with defined limits of memory cache and disk cache
160     * @param <K> key type
161     * @param <V> value type
162     * @param cacheName         region name
163     * @param maxMemoryObjects  number of objects to keep in memory
164     * @param maxDiskObjects    maximum size of the objects stored on disk in kB
165     * @param cachePath         path to disk cache. if null, no disk cache will be created
166     * @return cache access object
167     */
168    public static <K, V> CacheAccess<K, V> getCache(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) {
169        if (cacheManager != null)
170            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath);
171
172        synchronized (JCSCacheManager.class) {
173            if (cacheManager == null)
174                initialize();
175            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath);
176        }
177    }
178
179    @SuppressWarnings("unchecked")
180    private static <K, V> CacheAccess<K, V> getCacheInner(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) {
181        CompositeCache<K, V> cc = cacheManager.getCache(cacheName, getCacheAttributes(maxMemoryObjects));
182
183        if (cachePath != null && cacheDirLock != null) {
184            IDiskCacheAttributes diskAttributes = getDiskCacheAttributes(maxDiskObjects, cachePath, cacheName);
185            try {
186                if (cc.getAuxCaches().length == 0) {
187                    cc.setAuxCaches(new AuxiliaryCache[]{DISK_CACHE_FACTORY.createCache(
188                            diskAttributes, cacheManager, null, new StandardSerializer())});
189                }
190            } catch (Exception e) { // NOPMD
191                // in case any error in setting auxiliary cache, do not use disk cache at all - only memory
192                cc.setAuxCaches(new AuxiliaryCache[0]);
193            }
194        }
195        return new CacheAccess<>(cc);
196    }
197
198    /**
199     * Close all files to ensure, that all indexes and data are properly written
200     */
201    public static void shutdown() {
202        // use volatile semantics to get consistent object
203        CompositeCacheManager localCacheManager = cacheManager;
204        if (localCacheManager != null) {
205            localCacheManager.shutDown();
206        }
207    }
208
209    private static IDiskCacheAttributes getDiskCacheAttributes(int maxDiskObjects, String cachePath, String cacheName) {
210        IDiskCacheAttributes ret;
211        removeStaleFiles(cachePath + File.separator + cacheName, USE_BLOCK_CACHE.get() ? "_INDEX_v2" : "_BLOCK_v2");
212        String newCacheName = cacheName + (USE_BLOCK_CACHE.get() ? "_BLOCK_v2" : "_INDEX_v2");
213
214        if (USE_BLOCK_CACHE.get()) {
215            BlockDiskCacheAttributes blockAttr = new BlockDiskCacheAttributes();
216            /*
217             * BlockDiskCache never optimizes the file, so when file size is reduced, it will never be truncated to desired size.
218             *
219             * If for some mysterious reason, file size is greater than the value set in preferences, just use the whole file. If the user
220             * wants to reduce the file size, (s)he may just go to preferences and there it should be handled (by removing old file)
221             */
222            File diskCacheFile = new File(cachePath + File.separator + newCacheName + ".data");
223            if (diskCacheFile.exists()) {
224                blockAttr.setMaxKeySize((int) Math.max(maxDiskObjects, diskCacheFile.length()/1024));
225            } else {
226                blockAttr.setMaxKeySize(maxDiskObjects);
227            }
228            blockAttr.setBlockSizeBytes(4096); // use 4k blocks
229            ret = blockAttr;
230        } else {
231            IndexedDiskCacheAttributes indexAttr = new IndexedDiskCacheAttributes();
232            indexAttr.setMaxKeySize(maxDiskObjects);
233            ret = indexAttr;
234        }
235        ret.setDiskLimitType(IDiskCacheAttributes.DiskLimitType.SIZE);
236        File path = new File(cachePath);
237        if (!path.exists() && !path.mkdirs()) {
238            Logging.warn("Failed to create cache path: {0}", cachePath);
239        } else {
240            ret.setDiskPath(cachePath);
241        }
242        ret.setCacheName(newCacheName);
243
244        return ret;
245    }
246
247    private static void removeStaleFiles(String basePathPart, String suffix) {
248        deleteCacheFiles(basePathPart + suffix);
249    }
250
251    private static void deleteCacheFiles(String basePathPart) {
252        Utils.deleteFileIfExists(new File(basePathPart + ".key"));
253        Utils.deleteFileIfExists(new File(basePathPart + ".data"));
254    }
255
256    private static CompositeCacheAttributes getCacheAttributes(int maxMemoryElements) {
257        CompositeCacheAttributes ret = new CompositeCacheAttributes();
258        ret.setMaxObjects(maxMemoryElements);
259        ret.setDiskUsagePattern(DiskUsagePattern.UPDATE);
260        return ret;
261    }
262}
Note: See TracBrowser for help on using the repository browser.