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"/>
|
<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.
|
||||||
@@ -33,13 +34,14 @@
|
|||||||
<value>${dir.contentstore}</value>
|
<value>${dir.contentstore}</value>
|
||||||
</constructor-arg>
|
</constructor-arg>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
|
||||||
<bean id="cachingContentStoreCache" class="org.alfresco.repo.cache.EhCacheAdapter">
|
<bean id="cachingContentStoreCache" class="org.alfresco.repo.cache.EhCacheAdapter">
|
||||||
<property name="cache">
|
<property name="cache">
|
||||||
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
|
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
|
||||||
@@ -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>
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
|
@@ -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;
|
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,47 +151,62 @@ 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();
|
||||||
|
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;
|
WriteLock writeLock = readWriteLock(url).writeLock();
|
||||||
|
writeLock.lock();
|
||||||
while (triesLeft > 0)
|
try
|
||||||
{
|
{
|
||||||
try
|
if (!cache.contains(url))
|
||||||
{
|
{
|
||||||
return cache.getReader(url);
|
if (cache.put(url, backingStore.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))
|
|
||||||
{
|
{
|
||||||
// Content was empty - probably hasn't been written yet.
|
return cache.getReader(url);
|
||||||
return bsReader.getReader();
|
|
||||||
}
|
}
|
||||||
else
|
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)
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
@@ -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())
|
||||||
{
|
{
|
||||||
@@ -72,26 +116,29 @@ 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
* 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;
|
||||||
|
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
|
* 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;
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -73,8 +79,7 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
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">
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user