001package icy.image.cache;
002
003import java.io.File;
004import java.io.FileFilter;
005import java.util.Collection;
006import java.util.HashSet;
007import java.util.Set;
008
009import icy.file.FileUtil;
010import icy.system.IcyExceptionHandler;
011import net.sf.ehcache.Cache;
012import net.sf.ehcache.CacheManager;
013import net.sf.ehcache.Ehcache;
014import net.sf.ehcache.Element;
015import net.sf.ehcache.config.CacheConfiguration;
016import net.sf.ehcache.config.Configuration;
017import net.sf.ehcache.config.DiskStoreConfiguration;
018import net.sf.ehcache.config.MemoryUnit;
019import net.sf.ehcache.config.PersistenceConfiguration;
020import net.sf.ehcache.config.PersistenceConfiguration.Strategy;
021import net.sf.ehcache.event.CacheEventListener;
022import net.sf.ehcache.statistics.StatisticsGateway;
023import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
024
025public class EHCache2 extends AbstractCache
026{
027    private class CustomCacheEventListener implements CacheEventListener
028    {
029        public CustomCacheEventListener()
030        {
031            super();
032        }
033
034        @Override
035        public void dispose()
036        {
037            System.out.println("EHCache disposed");
038        }
039
040        @Override
041        public Object clone() throws CloneNotSupportedException
042        {
043            return super.clone();
044        }
045
046        @Override
047        public void notifyElementEvicted(Ehcache ehCache, Element element)
048        {
049            // Note (Stephane): sometime ehcache evict element from cache even if we have unlimited disk storage tier enabled.
050            // I guess this happen when it try to cache into the heap cache and there is not enough free space to do so.
051            // Ideally it would move some elements from heap to disk or directly save element in disk storage
052            // but it looks like it doesn't work that way (maybe for performance reason), even worse: ehcache can actually evict eternal element
053            // while it keep non eternal elements in cache !!
054
055            // eternal element eviction ?
056            if (element.isEternal())
057            {
058                System.out.println("Warning: eternal element " + element.getObjectKey() + " evicted from cache");
059                System.out.println("Trying to put it back...");
060                // try to force GC and put it back in cache
061                System.gc();
062                cache.put(new Element(element.getObjectKey(), element.getObjectValue(), element.isEternal()));
063            }
064        }
065
066        @Override
067        public void notifyElementExpired(Ehcache ehCache, Element element)
068        {
069            //
070        }
071
072        @Override
073        public void notifyElementPut(Ehcache ehCache, Element element) throws net.sf.ehcache.CacheException
074        {
075            //
076        }
077
078        @Override
079        public void notifyElementRemoved(Ehcache ehCache, Element element) throws net.sf.ehcache.CacheException
080        {
081            //
082        }
083
084        @Override
085        public void notifyElementUpdated(Ehcache ehCache, Element element) throws net.sf.ehcache.CacheException
086        {
087            //
088        }
089
090        @Override
091        public void notifyRemoveAll(Ehcache ehCache)
092        {
093            //
094        }
095    }
096
097    final Set<Integer> eternalStoredKeys;
098    CacheManager cacheManager;
099    Cache cache;
100    boolean enabled;
101
102    public EHCache2(int cacheSizeMB, String path)
103    {
104        super();
105
106        eternalStoredKeys = new HashSet<Integer>();
107
108        // get old ehcache agent JAR files
109        final String[] oldFiles = FileUtil.getFiles(FileUtil.getTempDirectory(), new FileFilter()
110        {
111            @Override
112            public boolean accept(File pathname)
113            {
114                // old ehcache temp agent JAR files
115                return FileUtil.getFileName(pathname.getAbsolutePath(), false).startsWith("ehcache");
116            }
117        }, false, false, false);
118        // delete these files as ehcache don't do it itself
119        for (String file : oldFiles)
120            FileUtil.delete(file, false);
121
122        // delete previous cache file
123        FileUtil.delete(path, true);
124
125        try
126        {
127            final DiskStoreConfiguration diskConfig = new DiskStoreConfiguration().path(path);
128            final Configuration cacheManagerConfig = new Configuration().diskStore(diskConfig);
129
130            final PersistenceConfiguration persistenceConfig = new PersistenceConfiguration()
131                    .strategy(Strategy.LOCALTEMPSWAP);
132            // CacheWriterFactoryConfiguration c = new CacheWriterFactoryConfiguration();
133            // c.setClass(path);
134
135            final long freeBytes = new File(FileUtil.getDrive(path)).getUsableSpace();
136            // subtract 200 MB to available space for safety
137            final long freeMB = (freeBytes <= 0) ? Long.MAX_VALUE : Math.max(0, (freeBytes / (1024 * 1024)) - 200);
138
139            final CacheConfiguration cacheConfig = new CacheConfiguration().name("ehCache2")
140                    .maxBytesLocalHeap(cacheSizeMB, MemoryUnit.MEGABYTES)
141                    // .maxBytesLocalOffHeap(cacheSizeMB, MemoryUnit.MEGABYTES)
142                    .maxBytesLocalDisk(Math.min(freeMB, 500000L), MemoryUnit.MEGABYTES)
143                    // we want the disk write buffer to be at least 32 MB and 256 MB max
144                    .diskSpoolBufferSizeMB(Math.max(32, Math.min(256, cacheSizeMB / 16)))
145                    .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LRU).timeToIdleSeconds(20)
146                    .timeToLiveSeconds(60).diskExpiryThreadIntervalSeconds(120).persistence(persistenceConfig);
147            // .pinning(new PinningConfiguration().store(Store.INCACHE));
148            // .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LRU).timeToIdleSeconds(2).timeToLiveSeconds(2)
149
150            cacheManagerConfig.addCache(cacheConfig);
151
152            // singleton cache manager creation
153            cacheManager = CacheManager.create(cacheManagerConfig);
154
155            // get the cache
156            cache = cacheManager.getCache("ehCache2");
157            // // add the custom Tile cache loader to it
158            // cache.registerCacheLoader(new ImageCacheLoader());
159
160            cache.getCacheEventNotificationService().registerListener(new CustomCacheEventListener());
161            enabled = true;
162        }
163        catch (Exception e)
164        {
165            System.err.println("Error while initialize image cache:");
166            IcyExceptionHandler.showErrorMessage(e, false, true);
167            enabled = false;
168        }
169    }
170
171    @Override
172    public boolean isEnabled()
173    {
174        return enabled;
175    }
176
177    @SuppressWarnings("deprecation")
178    @Override
179    public long usedMemory()
180    {
181        return cache.calculateInMemorySize() + cache.calculateOffHeapSize();
182    }
183
184    @SuppressWarnings("deprecation")
185    @Override
186    public long usedDisk()
187    {
188        return cache.calculateOnDiskSize();
189    }
190
191    @Override
192    public boolean isOnMemoryCache(Integer key)
193    {
194        if (profiling)
195            startProf();
196
197        try
198        {
199            return cache.isElementInMemory(key) | cache.isElementOffHeap(key);
200        }
201        finally
202        {
203            if (profiling)
204                endProf();
205        }
206    }
207
208    @Override
209    public boolean isOnDiskCache(Integer key)
210    {
211        if (profiling)
212            startProf();
213
214        try
215        {
216            return cache.isElementOnDisk(key);
217        }
218        finally
219        {
220            if (profiling)
221                endProf();
222        }
223    }
224
225    @Override
226    public boolean isInCache(Integer key)
227    {
228        if (profiling)
229            startProf();
230
231        try
232        {
233            return cache.isKeyInCache(key);
234        }
235        finally
236        {
237            if (profiling)
238                endProf();
239        }
240    }
241
242    @SuppressWarnings("unchecked")
243    @Override
244    public Collection<Integer> getAllKeys() throws CacheException
245    {
246        if (profiling)
247            startProf();
248
249        try
250        {
251            return cache.getKeys();
252        }
253        catch (Exception e)
254        {
255            throw new CacheException("ImageCache: an error occured while retrieving all keys from cache", e);
256        }
257        finally
258        {
259            if (profiling)
260                endProf();
261        }
262    }
263
264    @Override
265    public Object get(Integer key) throws CacheException
266    {
267        if (profiling)
268            startProf();
269
270        final boolean checkNull;
271        Object result = null;
272
273        // test if we need to check for null result
274        synchronized (eternalStoredKeys)
275        {
276            checkNull = eternalStoredKeys.contains(key);
277        }
278
279        try
280        {
281            final Element e = cache.get(key);
282
283            if (e != null)
284                result = e.getObjectValue();
285
286            // check if eternal data was lost (it seems that sometime EhCache loss data put in eternal state !!)
287            if (checkNull && (result == null))
288                throw new CacheException("ImageCache error: data '" + key + "' couldn't be retrieved (data lost)");
289
290            return result;
291        }
292        finally
293        {
294            if (profiling)
295                endProf();
296        }
297    }
298
299    @Override
300    public void set(Integer key, Object object, boolean eternal) throws CacheException
301    {
302        if (profiling)
303            startProf();
304
305        synchronized (eternalStoredKeys)
306        {
307            // save in keyset (only for non null eternal data)
308            if ((object != null) && eternal)
309                eternalStoredKeys.add(key);
310            else
311                eternalStoredKeys.remove(key);
312        }
313
314        try
315        {
316            cache.put(new Element(key, object, eternal));
317        }
318        catch (Exception e)
319        {
320            throw new CacheException("ImageCache error: data '" + key + "' couldn't be saved in cache", e);
321        }
322        finally
323        {
324            if (profiling)
325                endProf();
326        }
327    }
328
329    @Override
330    public void clear() throws CacheException
331    {
332        if (profiling)
333            startProf();
334
335        eternalStoredKeys.clear();
336
337        try
338        {
339            cache.removeAll();
340        }
341        catch (Exception e)
342        {
343            throw new CacheException("ImageCache: an error occured while clearing cache", e);
344        }
345        finally
346        {
347            if (profiling)
348                endProf();
349        }
350    }
351
352    @Override
353    public void remove(Integer key) throws CacheException
354    {
355        if (profiling)
356            startProf();
357
358        synchronized (eternalStoredKeys)
359        {
360            // remove from keyset
361            eternalStoredKeys.remove(key);
362        }
363
364        try
365        {
366            cache.remove(key);
367        }
368        catch (Exception e)
369        {
370            throw new CacheException("ImageCache: an error occured while removing data '" + key + "' from cache", e);
371        }
372        finally
373        {
374            if (profiling)
375                endProf();
376        }
377    }
378
379    public StatisticsGateway getStats()
380    {
381        return cache.getStatistics();
382    }
383
384    @Override
385    public void end()
386    {
387        eternalStoredKeys.clear();
388        cacheManager.shutdown();
389    }
390
391    @Override
392    public String getName()
393    {
394        return "EHCache 2";
395    }
396}