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:
Matt Ward
2011-08-25 15:20:08 +00:00
parent bfacbeb0b3
commit 98947fda5c
19 changed files with 1029 additions and 119 deletions

View File

@@ -21,6 +21,7 @@
<property name="cacheOnInbound" value="true"/> <property name="cacheOnInbound" value="true"/>
</bean> </bean>
<!-- <!--
The backingStore should NOT be a FileContentStore. If using a FileContentStore there 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. is no need to use a CachingContentStore and therefore no need for the backingStore.
@@ -37,6 +38,7 @@
<bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl"> <bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl">
<property name="memoryStore" ref="cachingContentStoreCache"/> <property name="memoryStore" ref="cachingContentStoreCache"/>
<property name="cacheRoot" value="${dir.cachedcontent}"/>
</bean> </bean>
@@ -52,4 +54,37 @@
</bean> </bean>
</property> </property>
</bean> </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> </beans>

View File

@@ -9,6 +9,9 @@ dir.root=./alf_data
dir.contentstore=${dir.root}/contentstore dir.contentstore=${dir.root}/contentstore
dir.contentstore.deleted=${dir.root}/contentstore.deleted dir.contentstore.deleted=${dir.root}/contentstore.deleted
# The location of cached content
dir.cachedcontent=${dir.root}/cachedcontent
dir.auditcontentstore=${dir.root}/audit.contentstore dir.auditcontentstore=${dir.root}/audit.contentstore
# The location for lucene index files # The location for lucene index files

View File

@@ -59,7 +59,7 @@ public class EhCacheAdapter<K extends Serializable, V extends Object>
{ {
try try
{ {
return (cache.get(key) != null); return (cache.getQuiet(key) != null);
} }
catch (CacheException e) catch (CacheException e)
{ {

View File

@@ -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");
}
}

View File

@@ -19,6 +19,9 @@
package org.alfresco.repo.content.caching; package org.alfresco.repo.content.caching;
import java.util.Date; 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.ContentContext;
import org.alfresco.repo.content.ContentStore; 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 // 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 int numLocks = 32;
private final static Object[] locks; private final static ReentrantReadWriteLock[] locks;
private ContentStore backingStore; private ContentStore backingStore;
private ContentCache cache; private ContentCache cache;
private boolean cacheOnInbound; private boolean cacheOnInbound;
static static
{ {
locks = new Object[numLocks]; locks = new ReentrantReadWriteLock[numLocks];
for (int i = 0; i < numLocks; i++) for (int i = 0; i < numLocks; i++)
{ {
locks[i] = new Object(); locks[i] = new ReentrantReadWriteLock();
} }
} }
@@ -148,46 +151,61 @@ public class CachingContentStore implements ContentStore
@Override @Override
public ContentReader getReader(String contentUrl) public ContentReader getReader(String contentUrl)
{ {
// Synchronise on one of a pool of locks - which one is determined by a hash of the URL. // Use 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 // This will stop the content from being read/cached multiple times from the backing store
// when it should only be read once and cached versions should be returned after that. // when it should only be read once - cached versions should be returned after that.
synchronized(lock(contentUrl)) ReadLock readLock = readWriteLock(contentUrl).readLock();
{ readLock.lock();
return retryingCacheRead(contentUrl);
}
}
private ContentReader retryingCacheRead(String url)
{
int triesLeft = 15;
while (triesLeft > 0)
{
try try
{ {
return cache.getReader(url); if (cache.contains(contentUrl))
{
return cache.getReader(contentUrl);
}
} }
catch(CacheMissException e) catch(CacheMissException e)
{ {
// Cached content is missing either from memory or disk // Fall through to cacheAndRead(url);
// so try and populate it and retry reading it. }
ContentReader bsReader = backingStore.getReader(url); finally
if (!cache.put(url, bsReader))
{ {
// Content was empty - probably hasn't been written yet. readLock.unlock();
return bsReader.getReader(); }
return cacheAndRead(contentUrl);
}
private ContentReader cacheAndRead(String url)
{
WriteLock writeLock = readWriteLock(url).writeLock();
writeLock.lock();
try
{
if (!cache.contains(url))
{
if (cache.put(url, backingStore.getReader(url)))
{
return cache.getReader(url);
} }
else else
{ {
triesLeft--;
}
}
}
// Give up and use the backing store directly
return backingStore.getReader(url); return backingStore.getReader(url);
} }
}
else
{
return cache.getReader(url);
}
}
catch(CacheMissException e)
{
return backingStore.getReader(url);
}
finally
{
writeLock.unlock();
}
}
/* /*
* @see org.alfresco.repo.content.ContentStore#getWriter(org.alfresco.repo.content.ContentContext) * @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); 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 @Required
public void setBackingStore(ContentStore backingStore) public void setBackingStore(ContentStore backingStore)
{ {
@@ -295,6 +318,4 @@ public class CachingContentStore implements ContentStore
{ {
this.cacheOnInbound = cacheOnInbound; this.cacheOnInbound = cacheOnInbound;
} }
} }

View File

@@ -19,7 +19,6 @@
package org.alfresco.repo.content.caching; package org.alfresco.repo.content.caching;
import java.io.File; import java.io.File;
import java.io.FilenameFilter;
import net.sf.ehcache.CacheManager; import net.sf.ehcache.CacheManager;
@@ -58,12 +57,13 @@ public class CachingContentStoreSpringTest extends AbstractWritableContentStoreT
getName()); getName());
cache = new ContentCacheImpl(); cache = new ContentCacheImpl();
cache.setCacheRoot(TempFileProvider.getLongLifeTempDir("cached_content_test"));
cache.setMemoryStore(createMemoryStore()); cache.setMemoryStore(createMemoryStore());
store = new CachingContentStore(backingStore, cache, false); store = new CachingContentStore(backingStore, cache, false);
} }
private EhCacheAdapter<String, String> createMemoryStore() private EhCacheAdapter<Key, String> createMemoryStore()
{ {
CacheManager manager = CacheManager.getInstance(); CacheManager manager = CacheManager.getInstance();
@@ -76,7 +76,7 @@ public class CachingContentStoreSpringTest extends AbstractWritableContentStoreT
manager.addCache(memoryOnlyCache); 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)); memoryStore.setCache(manager.getCache(EHCACHE_NAME));
return memoryStore; return memoryStore;

View File

@@ -76,7 +76,7 @@ public class CachingContentStoreTest
{ {
ContentReader cachedContentReader = mock(ContentReader.class); ContentReader cachedContentReader = mock(ContentReader.class);
when(cache.getReader("url")).thenReturn(cachedContentReader); when(cache.getReader("url")).thenReturn(cachedContentReader);
when(cache.contains("url")).thenReturn(true);
ContentReader returnedReader = cachingStore.getReader("url"); ContentReader returnedReader = cachingStore.getReader("url");
assertSame(returnedReader, cachedContentReader); assertSame(returnedReader, cachedContentReader);

View File

@@ -19,22 +19,23 @@
package org.alfresco.repo.content.caching; package org.alfresco.repo.content.caching;
import java.io.File; import java.io.File;
import java.util.Calendar;
import java.util.GregorianCalendar;
import org.alfresco.repo.cache.SimpleCache; 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.FileContentReader;
import org.alfresco.repo.content.filestore.FileContentWriter; import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter; 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. * The one and only implementation of the ContentCache class. Binary content data itself
* <p> * is stored on disk in the location specified by {@link cacheRoot}.
* Binary content data itself is stored on disk in temporary files managed
* by Alfresco (see {@link org.alfresco.util.TempFileProvider}).
* <p> * <p>
* The in-memory lookup table is provided by Ehcache. * The in-memory lookup table is provided by Ehcache.
* *
@@ -42,27 +43,70 @@ import org.alfresco.util.TempFileProvider;
*/ */
public class ContentCacheImpl implements ContentCache public class ContentCacheImpl implements ContentCache
{ {
private static final String CACHE_DIR = "caching_cs"; private static final Log log = LogFactory.getLog(ContentCacheImpl.class);
private static final String TMP_FILE_EXTENSION = ".tmp"; private static final String CACHE_FILE_EXT = ".bin";
private final File cacheRoot = TempFileProvider.getLongLifeTempDir(CACHE_DIR); private File cacheRoot;
private SimpleCache<String, String> memoryStore; private SimpleCache<Key, String> memoryStore;
@Override @Override
public boolean contains(String contentUrl) 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 @Override
public ContentReader getReader(String contentUrl) 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); File cacheFile = new File(path);
if (cacheFile.exists()) if (cacheFile.exists())
{ {
@@ -73,25 +117,28 @@ public class ContentCacheImpl implements ContentCache
throw new CacheMissException(contentUrl); throw new CacheMissException(contentUrl);
} }
@Override @Override
public boolean put(String contentUrl, ContentReader source) public boolean put(String contentUrl, ContentReader source)
{ {
File cacheFile = createCacheFile(contentUrl); File cacheFile = createCacheFile();
// Copy the content from the source into a cache file // Copy the content from the source into a cache file
if (source.getSize() > 0L) if (source.getSize() > 0L)
{ {
source.getContent(cacheFile); source.getContent(cacheFile);
// Add a record of the cached file to the in-memory cache. // Add a record of the cached file to the in-memory cache.
memoryStore.put(contentUrl, cacheFile.getAbsolutePath()); recordCacheEntries(contentUrl, cacheFile);
return true; return true;
} }
return false; 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. * Create a File object and makes any intermediate directories in the path.
@@ -99,39 +146,28 @@ public class ContentCacheImpl implements ContentCache
* @param contentUrl * @param contentUrl
* @return File * @return File
*/ */
private File createCacheFile(String contentUrl) private File createCacheFile()
{ {
File path = new File(cacheRoot, pathFromUrl(contentUrl)); File file = new File(cacheRoot, createNewCacheFilePath());
File parentDir = path.getParentFile(); File parentDir = file.getParentFile();
parentDir.mkdirs(); parentDir.mkdirs();
return file;
File cacheFile = TempFileProvider.createTempFile(path.getName(), TMP_FILE_EXTENSION, parentDir);
return cacheFile;
} }
/*
* @see org.alfresco.repo.content.caching.ContentCache#remove(java.lang.String)
*/
@Override @Override
public void remove(String contentUrl) public void remove(String contentUrl)
{ {
// Remove from the in-memory cache, but not from disk. Let the clean-up process do this asynchronously. // 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 @Override
public ContentWriter getWriter(final String url) public ContentWriter getWriter(final String url)
{ {
// Get a writer to a cache file. // Get a writer to a cache file.
final File cacheFile = createCacheFile(url); final File cacheFile = createCacheFile();
ContentWriter writer = new FileContentWriter(cacheFile, url, null); ContentWriter writer = new FileContentWriter(cacheFile, url, null);
// Attach a listener to populate the in-memory store when done writing. // Attach a listener to populate the in-memory store when done writing.
@@ -140,50 +176,112 @@ public class ContentCacheImpl implements ContentCache
@Override @Override
public void contentStreamClosed() throws ContentIOException public void contentStreamClosed() throws ContentIOException
{ {
memoryStore.put(url, cacheFile.getAbsolutePath()); recordCacheEntries(url, cacheFile);
} }
}); });
return writer; return writer;
} }
/** /**
* Converts a content URL to a relative path name where the protocol will * Creates a relative path for a new cache file. The path is based
* be the name of a subdirectory. For example: * upon the current date/time: year/month/day/hour/minute/guid.bin
* <p> * <p>
* store://2011/8/5/15/4/386595e0-3b52-4d5c-a32d-df9d0b9fd56e.bin * e.g. 2011/12/3/13/55/27d56416-bf9f-4d89-8f9e-e0a52de0a59e.bin
* <p> * @return The relative path for the new cache file.
* 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.
*/ */
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. * Configure ContentCache with a memory store - an EhCacheAdapter.
* *
* @param memoryStore the memoryStore to set * @param memoryStore the memoryStore to set
*/ */
public void setMemoryStore(SimpleCache<String, String> memoryStore) public void setMemoryStore(SimpleCache<Key, String> memoryStore)
{ {
this.memoryStore = 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. // Not part of the ContentCache interface as this breaks encapsulation.
// Handy method for tests though, since it allows us to find out where // Handy method for tests though, since it allows us to find out where
// the content was cached. // the content was cached.
protected String cacheFileLocation(String url) 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());
}
} }
} }

View File

@@ -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);
}

View File

@@ -16,7 +16,6 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.alfresco.repo.content.caching; package org.alfresco.repo.content.caching;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -16,7 +16,6 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.alfresco.repo.content.caching.test; package org.alfresco.repo.content.caching.test;

View File

@@ -16,7 +16,6 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.alfresco.repo.content.caching.test; package org.alfresco.repo.content.caching.test;
import java.io.IOException; import java.io.IOException;
@@ -36,26 +35,33 @@ import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; 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 * @author Matt Ward
*/ */
class SlowContentStore extends AbstractContentStore class SlowContentStore extends AbstractContentStore
{ {
private ConcurrentMap<String, AtomicLong> urlHits = new ConcurrentHashMap<String, AtomicLong>(); private ConcurrentMap<String, AtomicLong> urlHits = new ConcurrentHashMap<String, AtomicLong>();
private int pauseMillis = 50;
/*
* @see org.alfresco.repo.content.ContentStore#isWriteSupported()
*/
@Override @Override
public boolean isWriteSupported() public boolean isWriteSupported()
{ {
return true; return true;
} }
/*
* @see org.alfresco.repo.content.ContentStore#getReader(java.lang.String)
*/
@Override @Override
public ContentReader getReader(String contentUrl) public ContentReader getReader(String contentUrl)
{ {
@@ -74,7 +80,6 @@ class SlowContentStore extends AbstractContentStore
return new SlowWriter(newContentUrl, existingContentReader); return new SlowWriter(newContentUrl, existingContentReader);
} }
@Override @Override
public boolean exists(String contentUrl) public boolean exists(String contentUrl)
{ {
@@ -125,7 +130,7 @@ class SlowContentStore extends AbstractContentStore
{ {
try try
{ {
Thread.sleep(50); Thread.sleep(pauseMillis);
} }
catch (InterruptedException error) catch (InterruptedException error)
{ {
@@ -205,7 +210,7 @@ class SlowContentStore extends AbstractContentStore
{ {
try try
{ {
Thread.sleep(50); Thread.sleep(pauseMillis);
} }
catch (InterruptedException error) catch (InterruptedException error)
{ {
@@ -234,12 +239,14 @@ class SlowContentStore extends AbstractContentStore
return this.urlHits; 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(); this.pauseMillis = pauseMillis;
ContentReader reader = scs.getReader("store://something/bin");
String content = reader.getContentString();
System.out.println("Content: " + content);
} }
} }

View File

@@ -16,7 +16,6 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.alfresco.repo.content.caching.test; package org.alfresco.repo.content.caching.test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;

View 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>

View File

@@ -28,6 +28,7 @@
<bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl"> <bean id="contentCache" class="org.alfresco.repo.content.caching.ContentCacheImpl">
<property name="memoryStore" ref="cachingContentStoreCache"/> <property name="memoryStore" ref="cachingContentStoreCache"/>
<property name="cacheRoot" value="${dir.cachedcontent}"/>
</bean> </bean>