mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-31 17:39:05 +00:00
ALF-9613: numerous changes including use of ReentrantReadWriteLock for locking and introduction of a custom cleanup job that does not delete files that are in use by the cache.
git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@30066 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
<property name="cacheOnInbound" value="true"/>
|
||||
</bean>
|
||||
|
||||
|
||||
<!--
|
||||
The backingStore should NOT be a FileContentStore. If using a FileContentStore there
|
||||
is no need to use a CachingContentStore and therefore no need for the backingStore.
|
||||
@@ -33,13 +34,14 @@
|
||||
<value>${dir.contentstore}</value>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
|
||||
|
||||
|
||||
<bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl">
|
||||
<property name="memoryStore" ref="cachingContentStoreCache"/>
|
||||
<property name="cacheRoot" value="${dir.cachedcontent}"/>
|
||||
</bean>
|
||||
|
||||
|
||||
|
||||
<bean id="cachingContentStoreCache" class="org.alfresco.repo.cache.EhCacheAdapter">
|
||||
<property name="cache">
|
||||
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
|
||||
@@ -52,4 +54,37 @@
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
|
||||
<bean id="cachingContentStoreCleanerJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
|
||||
<property name="jobClass">
|
||||
<value>org.alfresco.repo.content.caching.cleanup.CachedContentCleanupJob</value>
|
||||
</property>
|
||||
<property name="jobDataAsMap">
|
||||
<map>
|
||||
<entry key="cachedContentCleaner">
|
||||
<ref bean="cachedContentCleaner" />
|
||||
</entry>
|
||||
</map>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="cachedContentCleaner" class="org.alfresco.repo.content.caching.cleanup.CachedContentCleaner">
|
||||
<property name="maxDeleteWatchCount" value="1"/>
|
||||
<property name="cache" ref="contentCache"/>
|
||||
</bean>
|
||||
|
||||
|
||||
<bean id="cachingContentStoreCleanerTrigger" class="org.alfresco.util.CronTriggerBean">
|
||||
<property name="jobDetail">
|
||||
<ref bean="cachingContentStoreCleanerJobDetail" />
|
||||
</property>
|
||||
<property name="scheduler">
|
||||
<ref bean="schedulerFactory" />
|
||||
</property>
|
||||
<property name="cronExpression">
|
||||
<value>${system.content.cachedContentCleanup.cronExpression}</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
@@ -9,6 +9,9 @@ dir.root=./alf_data
|
||||
dir.contentstore=${dir.root}/contentstore
|
||||
dir.contentstore.deleted=${dir.root}/contentstore.deleted
|
||||
|
||||
# The location of cached content
|
||||
dir.cachedcontent=${dir.root}/cachedcontent
|
||||
|
||||
dir.auditcontentstore=${dir.root}/audit.contentstore
|
||||
|
||||
# The location for lucene index files
|
||||
|
@@ -59,7 +59,7 @@ public class EhCacheAdapter<K extends Serializable, V extends Object>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (cache.get(key) != null);
|
||||
return (cache.getQuiet(key) != null);
|
||||
}
|
||||
catch (CacheException e)
|
||||
{
|
||||
|
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Manage a cache file's associated properties.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public class CacheFileProps
|
||||
{
|
||||
private static final String CONTENT_URL = "contentUrl";
|
||||
private static final String DELETE_WATCH_COUNT = "deleteWatchCount";
|
||||
private static final Log log = LogFactory.getLog(CacheFileProps.class);
|
||||
private final Properties properties = new Properties();
|
||||
private final File cacheFile;
|
||||
private final File propsFile;
|
||||
|
||||
/**
|
||||
* Construct a CacheFileProps specifying which cache file the properties belong to.
|
||||
*
|
||||
* @param cachedFile
|
||||
*/
|
||||
public CacheFileProps(File cacheFile)
|
||||
{
|
||||
this.cacheFile = cacheFile;
|
||||
this.propsFile = fileForCacheFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load properties from the cache file's associated properties file.
|
||||
*/
|
||||
public void load()
|
||||
{
|
||||
properties.clear();
|
||||
|
||||
if (propsFile.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
BufferedReader reader = new BufferedReader(new FileReader(propsFile));
|
||||
properties.load(reader);
|
||||
reader.close();
|
||||
}
|
||||
catch (FileNotFoundException error)
|
||||
{
|
||||
log.error("File disappeared after exists() check: " + cacheFile);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Unable to read properties file " + cacheFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save properties to the cache file's associated properties file.
|
||||
*/
|
||||
public void store()
|
||||
{
|
||||
BufferedOutputStream out = null;
|
||||
try
|
||||
{
|
||||
out = new BufferedOutputStream(new FileOutputStream(propsFile));
|
||||
properties.store(out, "Properties for " + cacheFile);
|
||||
}
|
||||
catch(FileNotFoundException e)
|
||||
{
|
||||
throw new RuntimeException("Couldn't create output stream for file: " + propsFile, e);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new RuntimeException("Couldn't write file: " + propsFile, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (out != null) out.close();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
// Couldn't close it, just log that it wasn't possible.
|
||||
if (log.isErrorEnabled())
|
||||
{
|
||||
log.error("Couldn't close file: " + propsFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cache file's associated properties file.
|
||||
*/
|
||||
public void delete()
|
||||
{
|
||||
propsFile.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a properties file exist for the cache file?
|
||||
*
|
||||
* @return true if the file exists
|
||||
*/
|
||||
public boolean exists()
|
||||
{
|
||||
return propsFile.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the contentUrl property.
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
public void setContentUrl(String url)
|
||||
{
|
||||
properties.setProperty(CONTENT_URL, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the contentUrl property.
|
||||
*
|
||||
* @return contentUrl
|
||||
*/
|
||||
public String getContentUrl()
|
||||
{
|
||||
return properties.getProperty(CONTENT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the deleteWatchCount property.
|
||||
*
|
||||
* @param watchCount
|
||||
*/
|
||||
public void setDeleteWatchCount(Integer watchCount)
|
||||
{
|
||||
properties.setProperty(DELETE_WATCH_COUNT, watchCount.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the deleteWatchCount property.
|
||||
*
|
||||
* @return deleteWatchCount
|
||||
*/
|
||||
public Integer getDeleteWatchCount()
|
||||
{
|
||||
String watchCountStr = properties.getProperty(DELETE_WATCH_COUNT, "0");
|
||||
return Integer.parseInt(watchCountStr);
|
||||
}
|
||||
|
||||
// Generate the path for the properties file, based upon the cache file's path.
|
||||
private File fileForCacheFile()
|
||||
{
|
||||
return new File(cacheFile.getAbsolutePath() + ".properties");
|
||||
}
|
||||
}
|
@@ -19,6 +19,9 @@
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
|
||||
|
||||
import org.alfresco.repo.content.ContentContext;
|
||||
import org.alfresco.repo.content.ContentStore;
|
||||
@@ -44,17 +47,17 @@ public class CachingContentStore implements ContentStore
|
||||
{
|
||||
// NUM_LOCKS absolutely must be a power of 2 for the use of locks to be evenly balanced
|
||||
private final static int numLocks = 32;
|
||||
private final static Object[] locks;
|
||||
private final static ReentrantReadWriteLock[] locks;
|
||||
private ContentStore backingStore;
|
||||
private ContentCache cache;
|
||||
private boolean cacheOnInbound;
|
||||
|
||||
static
|
||||
{
|
||||
locks = new Object[numLocks];
|
||||
locks = new ReentrantReadWriteLock[numLocks];
|
||||
for (int i = 0; i < numLocks; i++)
|
||||
{
|
||||
locks[i] = new Object();
|
||||
locks[i] = new ReentrantReadWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,47 +151,62 @@ public class CachingContentStore implements ContentStore
|
||||
@Override
|
||||
public ContentReader getReader(String contentUrl)
|
||||
{
|
||||
// Synchronise on one of a pool of locks - which one is determined by a hash of the URL.
|
||||
// This will stop the content from being read multiple times from the backing store
|
||||
// when it should only be read once and cached versions should be returned after that.
|
||||
synchronized(lock(contentUrl))
|
||||
// Use pool of locks - which one is determined by a hash of the URL.
|
||||
// This will stop the content from being read/cached multiple times from the backing store
|
||||
// when it should only be read once - cached versions should be returned after that.
|
||||
ReadLock readLock = readWriteLock(contentUrl).readLock();
|
||||
readLock.lock();
|
||||
try
|
||||
{
|
||||
return retryingCacheRead(contentUrl);
|
||||
if (cache.contains(contentUrl))
|
||||
{
|
||||
return cache.getReader(contentUrl);
|
||||
}
|
||||
}
|
||||
catch(CacheMissException e)
|
||||
{
|
||||
// Fall through to cacheAndRead(url);
|
||||
}
|
||||
finally
|
||||
{
|
||||
readLock.unlock();
|
||||
}
|
||||
|
||||
return cacheAndRead(contentUrl);
|
||||
}
|
||||
|
||||
private ContentReader retryingCacheRead(String url)
|
||||
private ContentReader cacheAndRead(String url)
|
||||
{
|
||||
int triesLeft = 15;
|
||||
|
||||
while (triesLeft > 0)
|
||||
WriteLock writeLock = readWriteLock(url).writeLock();
|
||||
writeLock.lock();
|
||||
try
|
||||
{
|
||||
try
|
||||
if (!cache.contains(url))
|
||||
{
|
||||
return cache.getReader(url);
|
||||
}
|
||||
catch (CacheMissException e)
|
||||
{
|
||||
// Cached content is missing either from memory or disk
|
||||
// so try and populate it and retry reading it.
|
||||
ContentReader bsReader = backingStore.getReader(url);
|
||||
if (!cache.put(url, bsReader))
|
||||
if (cache.put(url, backingStore.getReader(url)))
|
||||
{
|
||||
// Content was empty - probably hasn't been written yet.
|
||||
return bsReader.getReader();
|
||||
return cache.getReader(url);
|
||||
}
|
||||
else
|
||||
{
|
||||
triesLeft--;
|
||||
return backingStore.getReader(url);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return cache.getReader(url);
|
||||
}
|
||||
}
|
||||
catch(CacheMissException e)
|
||||
{
|
||||
return backingStore.getReader(url);
|
||||
}
|
||||
finally
|
||||
{
|
||||
writeLock.unlock();
|
||||
}
|
||||
|
||||
// Give up and use the backing store directly
|
||||
return backingStore.getReader(url);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* @see org.alfresco.repo.content.ContentStore#getWriter(org.alfresco.repo.content.ContentContext)
|
||||
*/
|
||||
@@ -267,18 +285,23 @@ public class CachingContentStore implements ContentStore
|
||||
return backingStore.delete(contentUrl);
|
||||
}
|
||||
|
||||
|
||||
private Object lock(String s)
|
||||
/**
|
||||
* Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than
|
||||
* per URL, so some contention is expected.
|
||||
*
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
public ReentrantReadWriteLock readWriteLock(String url)
|
||||
{
|
||||
return locks[lockIndex(s)];
|
||||
return locks[lockIndex(url)];
|
||||
}
|
||||
|
||||
private int lockIndex(String s)
|
||||
private int lockIndex(String url)
|
||||
{
|
||||
return s.hashCode() & (numLocks - 1);
|
||||
return url.hashCode() & (numLocks - 1);
|
||||
}
|
||||
|
||||
|
||||
@Required
|
||||
public void setBackingStore(ContentStore backingStore)
|
||||
{
|
||||
@@ -295,6 +318,4 @@ public class CachingContentStore implements ContentStore
|
||||
{
|
||||
this.cacheOnInbound = cacheOnInbound;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -19,7 +19,6 @@
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
|
||||
import net.sf.ehcache.CacheManager;
|
||||
|
||||
@@ -58,12 +57,13 @@ public class CachingContentStoreSpringTest extends AbstractWritableContentStoreT
|
||||
getName());
|
||||
|
||||
cache = new ContentCacheImpl();
|
||||
cache.setCacheRoot(TempFileProvider.getLongLifeTempDir("cached_content_test"));
|
||||
cache.setMemoryStore(createMemoryStore());
|
||||
store = new CachingContentStore(backingStore, cache, false);
|
||||
}
|
||||
|
||||
|
||||
private EhCacheAdapter<String, String> createMemoryStore()
|
||||
private EhCacheAdapter<Key, String> createMemoryStore()
|
||||
{
|
||||
CacheManager manager = CacheManager.getInstance();
|
||||
|
||||
@@ -76,7 +76,7 @@ public class CachingContentStoreSpringTest extends AbstractWritableContentStoreT
|
||||
manager.addCache(memoryOnlyCache);
|
||||
}
|
||||
|
||||
EhCacheAdapter<String, String> memoryStore = new EhCacheAdapter<String, String>();
|
||||
EhCacheAdapter<Key, String> memoryStore = new EhCacheAdapter<Key, String>();
|
||||
memoryStore.setCache(manager.getCache(EHCACHE_NAME));
|
||||
|
||||
return memoryStore;
|
||||
|
@@ -76,7 +76,7 @@ public class CachingContentStoreTest
|
||||
{
|
||||
ContentReader cachedContentReader = mock(ContentReader.class);
|
||||
when(cache.getReader("url")).thenReturn(cachedContentReader);
|
||||
|
||||
when(cache.contains("url")).thenReturn(true);
|
||||
ContentReader returnedReader = cachingStore.getReader("url");
|
||||
|
||||
assertSame(returnedReader, cachedContentReader);
|
||||
|
@@ -19,22 +19,23 @@
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import org.alfresco.repo.cache.SimpleCache;
|
||||
import org.alfresco.repo.content.ContentStore;
|
||||
import org.alfresco.repo.content.filestore.FileContentReader;
|
||||
import org.alfresco.repo.content.filestore.FileContentWriter;
|
||||
import org.alfresco.service.cmr.repository.ContentIOException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.repository.ContentStreamListener;
|
||||
import org.alfresco.service.cmr.repository.ContentWriter;
|
||||
import org.alfresco.util.TempFileProvider;
|
||||
import org.alfresco.util.GUID;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* The one and only implementation of the ContentCache class.
|
||||
* <p>
|
||||
* Binary content data itself is stored on disk in temporary files managed
|
||||
* by Alfresco (see {@link org.alfresco.util.TempFileProvider}).
|
||||
* The one and only implementation of the ContentCache class. Binary content data itself
|
||||
* is stored on disk in the location specified by {@link cacheRoot}.
|
||||
* <p>
|
||||
* The in-memory lookup table is provided by Ehcache.
|
||||
*
|
||||
@@ -42,27 +43,70 @@ import org.alfresco.util.TempFileProvider;
|
||||
*/
|
||||
public class ContentCacheImpl implements ContentCache
|
||||
{
|
||||
private static final String CACHE_DIR = "caching_cs";
|
||||
private static final String TMP_FILE_EXTENSION = ".tmp";
|
||||
private final File cacheRoot = TempFileProvider.getLongLifeTempDir(CACHE_DIR);
|
||||
private SimpleCache<String, String> memoryStore;
|
||||
|
||||
private static final Log log = LogFactory.getLog(ContentCacheImpl.class);
|
||||
private static final String CACHE_FILE_EXT = ".bin";
|
||||
private File cacheRoot;
|
||||
private SimpleCache<Key, String> memoryStore;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean contains(String contentUrl)
|
||||
{
|
||||
return memoryStore.contains(contentUrl);
|
||||
return memoryStore.contains(Key.forUrl(contentUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows caller to perform lookup using a {@link Key}.
|
||||
*
|
||||
* @param key
|
||||
* @return true if the cache contains, false otherwise.
|
||||
*/
|
||||
public boolean contains(Key key)
|
||||
{
|
||||
return memoryStore.contains(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item in the lookup table.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public void putIntoLookup(Key key, String value)
|
||||
{
|
||||
memoryStore.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of a cache file for the given content URL - will return null if there is no entry
|
||||
* in the cache for the specified URL.
|
||||
*
|
||||
* @param contentUrl
|
||||
* @return cache file path
|
||||
*/
|
||||
public String getCacheFilePath(String contentUrl)
|
||||
{
|
||||
return memoryStore.get(Key.forUrl(contentUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a content URL from the cache - keyed by File.
|
||||
*
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public String getContentUrl(File file)
|
||||
{
|
||||
return memoryStore.get(Key.forCacheFile(file));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentReader getReader(String contentUrl)
|
||||
{
|
||||
if (memoryStore.contains(contentUrl))
|
||||
Key url = Key.forUrl(contentUrl);
|
||||
if (memoryStore.contains(url))
|
||||
{
|
||||
String path = memoryStore.get(contentUrl);
|
||||
String path = memoryStore.get(url);
|
||||
File cacheFile = new File(path);
|
||||
if (cacheFile.exists())
|
||||
{
|
||||
@@ -72,26 +116,29 @@ public class ContentCacheImpl implements ContentCache
|
||||
|
||||
throw new CacheMissException(contentUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean put(String contentUrl, ContentReader source)
|
||||
{
|
||||
File cacheFile = createCacheFile(contentUrl);
|
||||
File cacheFile = createCacheFile();
|
||||
|
||||
// Copy the content from the source into a cache file
|
||||
if (source.getSize() > 0L)
|
||||
{
|
||||
source.getContent(cacheFile);
|
||||
// Add a record of the cached file to the in-memory cache.
|
||||
memoryStore.put(contentUrl, cacheFile.getAbsolutePath());
|
||||
recordCacheEntries(contentUrl, cacheFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void recordCacheEntries(String contentUrl, File cacheFile)
|
||||
{
|
||||
memoryStore.put(Key.forUrl(contentUrl), cacheFile.getAbsolutePath());
|
||||
memoryStore.put(Key.forCacheFile(cacheFile), contentUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a File object and makes any intermediate directories in the path.
|
||||
@@ -99,39 +146,28 @@ public class ContentCacheImpl implements ContentCache
|
||||
* @param contentUrl
|
||||
* @return File
|
||||
*/
|
||||
private File createCacheFile(String contentUrl)
|
||||
private File createCacheFile()
|
||||
{
|
||||
File path = new File(cacheRoot, pathFromUrl(contentUrl));
|
||||
File parentDir = path.getParentFile();
|
||||
|
||||
File file = new File(cacheRoot, createNewCacheFilePath());
|
||||
File parentDir = file.getParentFile();
|
||||
parentDir.mkdirs();
|
||||
|
||||
File cacheFile = TempFileProvider.createTempFile(path.getName(), TMP_FILE_EXTENSION, parentDir);
|
||||
return cacheFile;
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* @see org.alfresco.repo.content.caching.ContentCache#remove(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void remove(String contentUrl)
|
||||
{
|
||||
// Remove from the in-memory cache, but not from disk. Let the clean-up process do this asynchronously.
|
||||
memoryStore.remove(contentUrl);
|
||||
String path = getCacheFilePath(contentUrl);
|
||||
memoryStore.remove(Key.forUrl(contentUrl));
|
||||
memoryStore.remove(Key.forCacheFile(path));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* @see org.alfresco.repo.content.caching.ContentCache#getWriter(org.alfresco.repo.content.ContentContext)
|
||||
*/
|
||||
@Override
|
||||
public ContentWriter getWriter(final String url)
|
||||
{
|
||||
// Get a writer to a cache file.
|
||||
final File cacheFile = createCacheFile(url);
|
||||
final File cacheFile = createCacheFile();
|
||||
ContentWriter writer = new FileContentWriter(cacheFile, url, null);
|
||||
|
||||
// Attach a listener to populate the in-memory store when done writing.
|
||||
@@ -140,50 +176,112 @@ public class ContentCacheImpl implements ContentCache
|
||||
@Override
|
||||
public void contentStreamClosed() throws ContentIOException
|
||||
{
|
||||
memoryStore.put(url, cacheFile.getAbsolutePath());
|
||||
recordCacheEntries(url, cacheFile);
|
||||
}
|
||||
});
|
||||
|
||||
return writer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a content URL to a relative path name where the protocol will
|
||||
* be the name of a subdirectory. For example:
|
||||
* Creates a relative path for a new cache file. The path is based
|
||||
* upon the current date/time: year/month/day/hour/minute/guid.bin
|
||||
* <p>
|
||||
* store://2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin
|
||||
* <p>
|
||||
* will become:
|
||||
* <p>
|
||||
* store/2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin
|
||||
*
|
||||
* @param contentUrl
|
||||
* @return String representation of relative path to file.
|
||||
* e.g. 2011/12/3/13/55/27d56416-bf9f-4d89-8f9e-e0a52de0a59e.bin
|
||||
* @return The relative path for the new cache file.
|
||||
*/
|
||||
private String pathFromUrl(String contentUrl)
|
||||
public static String createNewCacheFilePath()
|
||||
{
|
||||
return contentUrl.replaceFirst(ContentStore.PROTOCOL_DELIMITER, "/");
|
||||
Calendar calendar = new GregorianCalendar();
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
|
||||
int day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
int hour = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
int minute = calendar.get(Calendar.MINUTE);
|
||||
// create the URL
|
||||
StringBuilder sb = new StringBuilder(20);
|
||||
sb.append(year).append('/')
|
||||
.append(month).append('/')
|
||||
.append(day).append('/')
|
||||
.append(hour).append('/')
|
||||
.append(minute).append('/')
|
||||
.append(GUID.generate()).append(CACHE_FILE_EXT);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Configure ContentCache with a memory store - an EhCacheAdapter.
|
||||
*
|
||||
* @param memoryStore the memoryStore to set
|
||||
*/
|
||||
public void setMemoryStore(SimpleCache<String, String> memoryStore)
|
||||
public void setMemoryStore(SimpleCache<Key, String> memoryStore)
|
||||
{
|
||||
this.memoryStore = memoryStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the directory where cache files will be written.
|
||||
*
|
||||
* @param cacheRoot
|
||||
*/
|
||||
public void setCacheRoot(File cacheRoot)
|
||||
{
|
||||
this.cacheRoot = cacheRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where cache files will be written (cacheRoot).
|
||||
*
|
||||
* @return cacheRoot
|
||||
*/
|
||||
public File getCacheRoot()
|
||||
{
|
||||
return this.cacheRoot;
|
||||
}
|
||||
|
||||
// Not part of the ContentCache interface as this breaks encapsulation.
|
||||
// Handy method for tests though, since it allows us to find out where
|
||||
// the content was cached.
|
||||
protected String cacheFileLocation(String url)
|
||||
{
|
||||
return memoryStore.get(url);
|
||||
return memoryStore.get(Key.forUrl(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cachedContentCleaner
|
||||
*/
|
||||
public void processFiles(FileHandler handler)
|
||||
{
|
||||
handleDir(cacheRoot, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurse into a directory handling cache files (*.bin) with the supplied
|
||||
* {@link FileHandler}.
|
||||
*
|
||||
* @param dir
|
||||
* @param handler
|
||||
*/
|
||||
private void handleDir(File dir, FileHandler handler)
|
||||
{
|
||||
if (dir.isDirectory())
|
||||
{
|
||||
File[] files = dir.listFiles();
|
||||
for (File file : files)
|
||||
{
|
||||
if (file.isDirectory())
|
||||
{
|
||||
handleDir(file, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (file.getName().endsWith(CACHE_FILE_EXT)) handler.handle(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("handleDir() called with non-directory: " + dir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Callback interface for file-based actions.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public interface FileHandler
|
||||
{
|
||||
void handle(File file);
|
||||
}
|
@@ -16,7 +16,6 @@
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
82
source/java/org/alfresco/repo/content/caching/Key.java
Normal file
82
source/java/org/alfresco/repo/content/caching/Key.java
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Multipurpose key so that data can be cached either by content URL or cache file path.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public class Key implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private enum Type { CONTENT_URL, CACHE_FILE_PATH };
|
||||
private final Type type;
|
||||
private final String value;
|
||||
|
||||
private Key(Type type, String value)
|
||||
{
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static Key forUrl(String url)
|
||||
{
|
||||
return new Key(Type.CONTENT_URL, url);
|
||||
}
|
||||
|
||||
public static Key forCacheFile(String path)
|
||||
{
|
||||
return new Key(Type.CACHE_FILE_PATH, path);
|
||||
}
|
||||
|
||||
public static Key forCacheFile(File file)
|
||||
{
|
||||
return forCacheFile(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((this.type == null) ? 0 : this.type.hashCode());
|
||||
result = prime * result + ((this.value == null) ? 0 : this.value.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
Key other = (Key) obj;
|
||||
if (this.type != other.type) return false;
|
||||
if (this.value == null)
|
||||
{
|
||||
if (other.value != null) return false;
|
||||
}
|
||||
else if (!this.value.equals(other.value)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching.cleanup;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.alfresco.repo.content.caching.CacheFileProps;
|
||||
import org.alfresco.repo.content.caching.ContentCacheImpl;
|
||||
import org.alfresco.repo.content.caching.FileHandler;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.annotation.Required;
|
||||
|
||||
/**
|
||||
* Cleans up redundant cache files from the cached content file store. Once references to cache files are
|
||||
* no longer in the in-memory cache, the binary content files can be removed.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public class CachedContentCleaner implements FileHandler
|
||||
{
|
||||
private static final Log log = LogFactory.getLog(CachedContentCleaner.class);
|
||||
private ContentCacheImpl cache; // impl specific functionality required
|
||||
private Integer maxDeleteWatchCount = 1;
|
||||
|
||||
public void execute()
|
||||
{
|
||||
cache.processFiles(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(File cachedContentFile)
|
||||
{
|
||||
if (log.isDebugEnabled())
|
||||
{
|
||||
log.debug("handle file: " + cachedContentFile);
|
||||
}
|
||||
|
||||
CacheFileProps props = null; // don't load unless required
|
||||
String url = cache.getContentUrl(cachedContentFile);
|
||||
if (url == null)
|
||||
{
|
||||
// Not in the cache, check the properties file
|
||||
props = new CacheFileProps(cachedContentFile);
|
||||
props.load();
|
||||
url = props.getContentUrl();
|
||||
}
|
||||
|
||||
if (url != null && !cache.contains(url))
|
||||
{
|
||||
if (props == null)
|
||||
{
|
||||
props = new CacheFileProps(cachedContentFile);
|
||||
props.load();
|
||||
}
|
||||
markOrDelete(cachedContentFile, props);
|
||||
}
|
||||
else if (url == null)
|
||||
{
|
||||
// It might still be in the cache, but we were unable to determine it from the reverse lookup
|
||||
// or the properties file. Delete the file as it is most likely orphaned. If for some reason it is
|
||||
// still in the cache, cache.getReader(url) must re-cache it.
|
||||
markOrDelete(cachedContentFile, props);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Marks a file for deletion by a future run of the CachedContentCleaner. Each time a file is observed
|
||||
* by the cleaner as being ready for deletion, the deleteWatchCount is incremented until it reaches
|
||||
* maxDeleteWatchCount - in which case the next run of cleaner will really delete it.
|
||||
* <p>
|
||||
* For maxDeleteWatchCount of 1 for example, the first cleaner run will mark the file for deletion and the second
|
||||
* run will really delete it.
|
||||
* <p>
|
||||
* This offers a degree of protection over the fairly unlikely event that a reader will be obtained for a file that
|
||||
* is in the cache but gets removed from the cache and is then deleted by the cleaner before
|
||||
* the reader was consumed. A maxDeleteWatchCount of 1 should normally be fine (recommended), whilst
|
||||
* 0 would result in immediate deletion the first time the cleaner sees it as a candidate
|
||||
* for deletion (not recommended).
|
||||
*
|
||||
* @param file
|
||||
* @param props
|
||||
*/
|
||||
private void markOrDelete(File file, CacheFileProps props)
|
||||
{
|
||||
Integer deleteWatchCount = props.getDeleteWatchCount();
|
||||
|
||||
// Just in case the value has been corrupted somehow.
|
||||
if (deleteWatchCount < 0)
|
||||
deleteWatchCount = 0;
|
||||
|
||||
if (deleteWatchCount < maxDeleteWatchCount)
|
||||
{
|
||||
deleteWatchCount++;
|
||||
props.setDeleteWatchCount(deleteWatchCount);
|
||||
props.store();
|
||||
}
|
||||
else
|
||||
{
|
||||
deleteFilesNow(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes both the cached content file and its peer properties file that contains the
|
||||
* original content URL and deletion marker information.
|
||||
*
|
||||
* @param cacheFile Location of cached content file.
|
||||
*/
|
||||
private void deleteFilesNow(File cacheFile)
|
||||
{
|
||||
CacheFileProps props = new CacheFileProps(cacheFile);
|
||||
props.delete();
|
||||
cacheFile.delete();
|
||||
}
|
||||
|
||||
@Required
|
||||
public void setCache(ContentCacheImpl cache)
|
||||
{
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
public void setMaxDeleteWatchCount(Integer maxDeleteWatchCount)
|
||||
{
|
||||
if (maxDeleteWatchCount < 0)
|
||||
{
|
||||
throw new IllegalArgumentException("maxDeleteWatchCount cannot be negative [value=" + maxDeleteWatchCount + "]");
|
||||
}
|
||||
this.maxDeleteWatchCount = maxDeleteWatchCount;
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching.cleanup;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/**
|
||||
* Quartz job to remove cached content files from disk once they are no longer
|
||||
* held in the in-memory cache.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public class CachedContentCleanupJob implements Job
|
||||
{
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException
|
||||
{
|
||||
JobDataMap jobData = context.getJobDetail().getJobDataMap();
|
||||
CachedContentCleaner cachedContentCleaner = cachedContentCleaner(jobData);
|
||||
cachedContentCleaner.execute();
|
||||
}
|
||||
|
||||
|
||||
private CachedContentCleaner cachedContentCleaner(JobDataMap jobData)
|
||||
{
|
||||
Object cleanerObj = jobData.get("cachedContentCleaner");
|
||||
if (cleanerObj == null || !(cleanerObj instanceof CachedContentCleaner))
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"CachedContentCleanupJob requires a valid 'cachedContentCleaner' reference");
|
||||
}
|
||||
CachedContentCleaner cleaner = (CachedContentCleaner) cleanerObj;
|
||||
return cleaner;
|
||||
}
|
||||
}
|
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
||||
*
|
||||
* This file is part of Alfresco
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.alfresco.repo.content.caching.cleanup;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import org.alfresco.repo.content.caching.CacheFileProps;
|
||||
import org.alfresco.repo.content.caching.CachingContentStore;
|
||||
import org.alfresco.repo.content.caching.ContentCacheImpl;
|
||||
import org.alfresco.repo.content.caching.Key;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.util.ApplicationContextHelper;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* Tests for the CachedContentCleanupJob
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
public class CachedContentCleanupJobTest
|
||||
{
|
||||
private enum UrlSource { PROPS_FILE, REVERSE_CACHE_LOOKUP, NOT_PRESENT };
|
||||
private ApplicationContext ctx;
|
||||
private CachingContentStore cachingStore;
|
||||
private ContentCacheImpl cache;
|
||||
private File cacheRoot;
|
||||
private CachedContentCleaner cleaner;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
String conf = "classpath:cachingstore/test-context.xml";
|
||||
String cleanerConf = "classpath:cachingstore/test-cleaner-context.xml";
|
||||
ctx = ApplicationContextHelper.getApplicationContext(new String[] { conf, cleanerConf });
|
||||
|
||||
cachingStore = (CachingContentStore) ctx.getBean("cachingContentStore");
|
||||
|
||||
cache = (ContentCacheImpl) ctx.getBean("contentCache");
|
||||
cacheRoot = cache.getCacheRoot();
|
||||
|
||||
cleaner = (CachedContentCleaner) ctx.getBean("cachedContentCleaner");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void filesNotInCacheAreDeleted()
|
||||
{
|
||||
cleaner.setMaxDeleteWatchCount(0);
|
||||
int numFiles = 300; // Must be a multiple of number of UrlSource types being tested
|
||||
File[] files = new File[300];
|
||||
for (int i = 0; i < numFiles; i++)
|
||||
{
|
||||
// Testing with a number of files. The cached file cleaner will be able to determine the 'original'
|
||||
// content URL for each file by either retrieving from the companion properties file, or performing
|
||||
// a 'reverse lookup' in the cache (i.e. cache.contains(Key.forCacheFile(...))), or there will be no
|
||||
// URL determinable for the file.
|
||||
UrlSource urlSource = UrlSource.values()[i % UrlSource.values().length];
|
||||
File cacheFile = createCacheFile(urlSource, i);
|
||||
files[i] = cacheFile;
|
||||
}
|
||||
|
||||
// Run cleaner
|
||||
cleaner.execute();
|
||||
|
||||
// check all files deleted
|
||||
for (File file : files)
|
||||
{
|
||||
assertFalse("File should have been deleted: " + file, file.exists());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void markedFilesHaveDeletionDeferredUntilCorrectPassOfCleaner()
|
||||
{
|
||||
// A non-advisable setting but useful for testing, maxDeleteWatchCount of zero
|
||||
// which should result in immediate deletion upon discovery of content no longer in the cache.
|
||||
cleaner.setMaxDeleteWatchCount(0);
|
||||
File file = createCacheFile(UrlSource.NOT_PRESENT, 0);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkFilesDeleted(file);
|
||||
|
||||
// Anticipated to be the most common setting: maxDeleteWatchCount of 1.
|
||||
cleaner.setMaxDeleteWatchCount(1);
|
||||
file = createCacheFile(UrlSource.NOT_PRESENT, 0);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkWatchCountForCacheFile(file, 1);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkFilesDeleted(file);
|
||||
|
||||
// Check that some other arbitrary figure for maxDeleteWatchCount works correctly.
|
||||
cleaner.setMaxDeleteWatchCount(3);
|
||||
file = createCacheFile(UrlSource.NOT_PRESENT, 0);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkWatchCountForCacheFile(file, 1);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkWatchCountForCacheFile(file, 2);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkWatchCountForCacheFile(file, 3);
|
||||
|
||||
cleaner.handle(file);
|
||||
checkFilesDeleted(file);
|
||||
}
|
||||
|
||||
|
||||
private void checkFilesDeleted(File file)
|
||||
{
|
||||
assertFalse("File should have been deleted: " + file, file.exists());
|
||||
CacheFileProps props = new CacheFileProps(file);
|
||||
assertFalse("Properties file should have been deleted, cache file: " + file, props.exists());
|
||||
}
|
||||
|
||||
|
||||
private void checkWatchCountForCacheFile(File file, Integer expectedWatchCount)
|
||||
{
|
||||
assertTrue("File should still exist: " + file, file.exists());
|
||||
CacheFileProps props = new CacheFileProps(file);
|
||||
props.load();
|
||||
assertEquals("File should contain correct deleteWatchCount", expectedWatchCount, props.getDeleteWatchCount());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void filesInCacheAreNotDeleted()
|
||||
{
|
||||
cleaner.setMaxDeleteWatchCount(0);
|
||||
|
||||
// The SlowContentStore will always give out content when asked,
|
||||
// so asking for any content will cause something to be cached.
|
||||
int numFiles = 50;
|
||||
for (int i = 0; i < numFiles; i++)
|
||||
{
|
||||
ContentReader reader = cachingStore.getReader(String.format("store://caching/store/url-%03d.bin", i));
|
||||
reader.getContentString();
|
||||
}
|
||||
|
||||
cleaner.execute();
|
||||
|
||||
for (int i = 0; i < numFiles; i++)
|
||||
{
|
||||
File cacheFile = new File(cache.getCacheFilePath(String.format("store://caching/store/url-%03d.bin", i)));
|
||||
assertTrue("File should exist", cacheFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private File createCacheFile(UrlSource urlSource, int fileNum)
|
||||
{
|
||||
File file = new File(cacheRoot, ContentCacheImpl.createNewCacheFilePath());
|
||||
file.getParentFile().mkdirs();
|
||||
writeSampleContent(file);
|
||||
String contentUrl = String.format("protocol://some/made/up/url-%03d.bin", fileNum);
|
||||
|
||||
switch(urlSource)
|
||||
{
|
||||
case NOT_PRESENT:
|
||||
// cache won't be able to determine original content URL for the file
|
||||
break;
|
||||
case PROPS_FILE:
|
||||
// file with content URL in properties file
|
||||
CacheFileProps props = new CacheFileProps(file);
|
||||
props.setContentUrl(contentUrl);
|
||||
props.store();
|
||||
break;
|
||||
case REVERSE_CACHE_LOOKUP:
|
||||
// file with content URL in reverse lookup cache - but not 'in the cache' (forward lookup).
|
||||
cache.putIntoLookup(Key.forCacheFile(file), contentUrl);
|
||||
}
|
||||
assertTrue("File should exist", file.exists());
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
private void writeSampleContent(File file)
|
||||
{
|
||||
try
|
||||
{
|
||||
PrintWriter writer = new PrintWriter(file);
|
||||
writer.println("Content for sample file in " + getClass().getName());
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
throw new RuntimeException("Couldn't write file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,6 @@
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.alfresco.repo.content.caching.test;
|
||||
|
||||
|
||||
|
@@ -16,7 +16,6 @@
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.alfresco.repo.content.caching.test;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -36,26 +35,33 @@ import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.repository.ContentWriter;
|
||||
|
||||
/**
|
||||
* Package-private class - only for testing the CachingContentStore.
|
||||
* Package-private class used only for testing the CachingContentStore.
|
||||
* <p>
|
||||
* Simulates a slow content store such as Amazon S3 or XAM. The ContentStore does not provide
|
||||
* genuine facilities to store or retrieve content.
|
||||
* <p>
|
||||
* Asking for content using {@link #getReader(String)} will result in (generated) content
|
||||
* being retrieved for any URL. A counter records how many times each arbitrary URL has been asked for.
|
||||
* <p>
|
||||
* Attempts to write content using any of the getWriter() methods will succeed. Though the content does not actually
|
||||
* get stored anywhere.
|
||||
* <p>
|
||||
* Both reads and writes are slow - the readers and writers returned by this class sleep for {@link pauseMillis} after
|
||||
* each operation.
|
||||
*
|
||||
* @author Matt Ward
|
||||
*/
|
||||
class SlowContentStore extends AbstractContentStore
|
||||
{
|
||||
private ConcurrentMap<String, AtomicLong> urlHits = new ConcurrentHashMap<String, AtomicLong>();
|
||||
private int pauseMillis = 50;
|
||||
|
||||
/*
|
||||
* @see org.alfresco.repo.content.ContentStore#isWriteSupported()
|
||||
*/
|
||||
@Override
|
||||
public boolean isWriteSupported()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see org.alfresco.repo.content.ContentStore#getReader(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public ContentReader getReader(String contentUrl)
|
||||
{
|
||||
@@ -73,8 +79,7 @@ class SlowContentStore extends AbstractContentStore
|
||||
|
||||
return new SlowWriter(newContentUrl, existingContentReader);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean exists(String contentUrl)
|
||||
{
|
||||
@@ -125,7 +130,7 @@ class SlowContentStore extends AbstractContentStore
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(50);
|
||||
Thread.sleep(pauseMillis);
|
||||
}
|
||||
catch (InterruptedException error)
|
||||
{
|
||||
@@ -205,7 +210,7 @@ class SlowContentStore extends AbstractContentStore
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(50);
|
||||
Thread.sleep(pauseMillis);
|
||||
}
|
||||
catch (InterruptedException error)
|
||||
{
|
||||
@@ -234,12 +239,14 @@ class SlowContentStore extends AbstractContentStore
|
||||
return this.urlHits;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
/**
|
||||
* Length of time in milliseconds that ReadableByteChannel and WriteableByteChannel objects returned
|
||||
* by SlowContentStore will pause for during read and write operations respectively.
|
||||
*
|
||||
* @param pauseMillis
|
||||
*/
|
||||
public void setPauseMillis(int pauseMillis)
|
||||
{
|
||||
SlowContentStore scs = new SlowContentStore();
|
||||
|
||||
ContentReader reader = scs.getReader("store://something/bin");
|
||||
String content = reader.getContentString();
|
||||
System.out.println("Content: " + content);
|
||||
this.pauseMillis = pauseMillis;
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.alfresco.repo.content.caching.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
31
source/test-resources/cachingstore/test-cleaner-context.xml
Normal file
31
source/test-resources/cachingstore/test-cleaner-context.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
|
||||
|
||||
<beans>
|
||||
|
||||
<!-- Override the backing store bean for the cleaner tests -->
|
||||
<bean id="backingStore" class="org.alfresco.repo.content.caching.test.SlowContentStore">
|
||||
<!-- set pauseMillis to 0 since we're not really after a SLOW backing store -->
|
||||
<property name="pauseMillis" value="0"/>
|
||||
</bean>
|
||||
|
||||
|
||||
<bean id="cachingContentStoreCleanerJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
|
||||
<property name="jobClass">
|
||||
<value>org.alfresco.repo.content.caching.cleanup.CachedContentCleanupJob</value>
|
||||
</property>
|
||||
<property name="jobDataAsMap">
|
||||
<map>
|
||||
<entry key="cachedContentCleaner">
|
||||
<ref bean="cachedContentCleaner" />
|
||||
</entry>
|
||||
</map>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="cachedContentCleaner" class="org.alfresco.repo.content.caching.cleanup.CachedContentCleaner">
|
||||
<property name="maxDeleteWatchCount" value="1"/>
|
||||
<property name="cache" ref="contentCache"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
@@ -28,6 +28,7 @@
|
||||
|
||||
<bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl">
|
||||
<property name="memoryStore" ref="cachingContentStoreCache"/>
|
||||
<property name="cacheRoot" value="${dir.cachedcontent}"/>
|
||||
</bean>
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user